From 1e2fe029b5479f348481f40a839e7d1233b29f1f Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:03:17 +0000 Subject: [PATCH 1/8] sign the desktop app in through the browser The desktop app (next commit) can't sign in itself: Google turns embedded browsers away, and a magic link opens in the browser anyway. So it keeps a PKCE verifier and opens the browser at /desktop-auth?challenge=, where the person signs in as usual. The page mints a one-time code bound to the challenge (POST /auth/desktop/code) and hands it over through the app's personalagentproxy:// link; the app trades code and verifier for a session cookie of its own (POST /auth/desktop/session). Codes last five minutes, are kept only as their hash, and are taken only once the verifier checks out, so a wrong one leaves the code to the app that holds the right one. --- packages/api/src/auth/desktop.test.ts | 191 ++++++++++++++++++ packages/api/src/auth/desktop.ts | 108 ++++++++++ packages/api/src/server/create-api-router.ts | 6 + packages/contracts/src/auth.ts | 20 ++ packages/db/package.json | 1 + .../v039_add_desktop_auth_codes/migration.sql | 19 ++ packages/db/prisma/schema.prisma | 19 ++ packages/db/src/desktop-auth.ts | 55 +++++ packages/web/src/client/auth-client.ts | 14 +- packages/web/src/lib/desktop.ts | 9 + packages/web/src/loaders.ts | 18 +- .../web/src/pages/desktop-auth-page.test.tsx | 31 +++ packages/web/src/pages/desktop-auth-page.tsx | 32 +++ packages/web/src/router.tsx | 3 + 14 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/auth/desktop.test.ts create mode 100644 packages/api/src/auth/desktop.ts create mode 100644 packages/db/prisma/migrations/v039_add_desktop_auth_codes/migration.sql create mode 100644 packages/db/src/desktop-auth.ts create mode 100644 packages/web/src/lib/desktop.ts create mode 100644 packages/web/src/pages/desktop-auth-page.test.tsx create mode 100644 packages/web/src/pages/desktop-auth-page.tsx diff --git a/packages/api/src/auth/desktop.test.ts b/packages/api/src/auth/desktop.test.ts new file mode 100644 index 00000000..8de5be4f --- /dev/null +++ b/packages/api/src/auth/desktop.test.ts @@ -0,0 +1,191 @@ +import {createHash} from 'node:crypto'; + +import {beforeEach, describe, expect, mock, test} from 'bun:test'; +import {Err, Ok} from 'ts-results-es'; + +import {ApiErr} from '@proxy/utils'; + +// How the api stores a code or a session's token: the hex of its SHA-256. +function sha256(text: string): string { + return createHash('sha256').update(text).digest('hex'); +} + +const createDesktopAuthCode = mock(); +const getDesktopAuthCode = mock(); +const takeDesktopAuthCode = mock(); + +mock.module('@proxy/db/desktop-auth', () => ({ + createDesktopAuthCode, + getDesktopAuthCode, + takeDesktopAuthCode, +})); + +const createSession = mock(); + +mock.module('@proxy/db/auth', () => ({createSession})); + +const logWarn = mock(); + +mock.module('../observability/log', () => ({ + log: {debug: mock(), info: mock(), warn: logWarn, error: mock()}, + serializeError: mock(() => ({})), +})); + +mock.module('../utils/env', () => ({ + env: {NODE_ENV: 'production', APP_URL: 'https://app.example.com'}, +})); + +// The app's side of PKCE: a verifier it keeps, and its challenge, as the page sends it. +const verifier = 'v'.repeat(43); +const challenge = createHash('sha256').update(verifier).digest('base64url'); + +type RecordedCookie = {name: string; value: string; options: Record}; + +function makeRes() { + const cookies: RecordedCookie[] = []; + const res = { + cookies, + cookie: mock((name: string, value: string, options: RecordedCookie['options']) => { + cookies.push({name, value, options}); + }), + status: mock(() => res), + json: mock(() => res), + }; + return res; +} + +function liveCode() { + return {userId: 'user-1', challenge, expires: new Date(Date.now() + 60_000)}; +} + +beforeEach(() => { + mock.clearAllMocks(); + createDesktopAuthCode.mockResolvedValue(Ok(undefined)); + getDesktopAuthCode.mockResolvedValue(Ok(liveCode())); + takeDesktopAuthCode.mockResolvedValue(Ok(true)); + createSession.mockResolvedValue(Ok(undefined)); +}); + +describe('handleDesktopCodeRoute', () => { + test('mints a code for the signed-in person, bound to the challenge, and stores only its hash', async () => { + const {handleDesktopCodeRoute} = await import('./desktop'); + const before = Date.now(); + + const result = await handleDesktopCodeRoute({body: {challenge}, user: {userId: 'user-1'}} as never); + + expect(result.isOk()).toBe(true); + const {code} = result.unwrap(); + expect(code).toMatch(/^[0-9a-f]{64}$/); + const stored = createDesktopAuthCode.mock.calls[0]?.[0] as {codeHash: string; challenge: string; userId: string; expires: Date}; + expect(stored.codeHash).toBe(sha256(code)); + expect(stored.challenge).toBe(challenge); + expect(stored.userId).toBe('user-1'); + // Minutes, not a session's weeks. + expect(stored.expires.valueOf() - before).toBeGreaterThanOrEqual(5 * 60 * 1000 - 1000); + expect(stored.expires.valueOf() - before).toBeLessThanOrEqual(5 * 60 * 1000 + 1000); + }); + + test('refuses a challenge that is not a base64url SHA-256', async () => { + const {handleDesktopCodeRoute} = await import('./desktop'); + + const result = await handleDesktopCodeRoute({body: {challenge: 'short'}, user: {userId: 'user-1'}} as never); + + expect(result.isErr()).toBe(true); + expect(result.unwrapErr().kind).toBe('parse_error'); + expect(createDesktopAuthCode).not.toHaveBeenCalled(); + }); +}); + +describe('handleDesktopSessionRoute', () => { + test('trades a live code and its verifier for a session cookie, taking the code', async () => { + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1', verifier}} as never, res as never); + + expect(getDesktopAuthCode).toHaveBeenCalledWith(sha256('code-1')); + expect(takeDesktopAuthCode).toHaveBeenCalledWith(sha256('code-1')); + const session = createSession.mock.calls[0]?.[0] as {tokenHash: string; userId: string}; + expect(session.userId).toBe('user-1'); + // The cookie has the token, the database its hash, as any sign-in's. + expect(res.cookies).toHaveLength(1); + expect(res.cookies[0]?.name).toBe('__Secure-proxy.session-token'); + expect(sha256(res.cookies[0]?.value ?? '')).toBe(session.tokenHash); + expect(res.cookies[0]?.options).toMatchObject({httpOnly: true, sameSite: 'lax', path: '/', secure: true}); + expect(res.status).not.toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ok: true}); + }); + + test('a wrong verifier is refused without taking the code, so the right app can still use it', async () => { + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1', verifier: 'w'.repeat(43)}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({error: 'invalid_code'}); + expect(takeDesktopAuthCode).not.toHaveBeenCalled(); + expect(createSession).not.toHaveBeenCalled(); + expect(res.cookies).toHaveLength(0); + expect(logWarn).toHaveBeenCalled(); + }); + + test('an unknown code is refused with the same answer', async () => { + getDesktopAuthCode.mockResolvedValue(Ok(null)); + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'nope', verifier}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({error: 'invalid_code'}); + expect(createSession).not.toHaveBeenCalled(); + }); + + test('an expired code is refused', async () => { + getDesktopAuthCode.mockResolvedValue(Ok({...liveCode(), expires: new Date(Date.now() - 1000)})); + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1', verifier}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({error: 'invalid_code'}); + expect(takeDesktopAuthCode).not.toHaveBeenCalled(); + }); + + test('a code taken by another request at the same moment signs no one in', async () => { + takeDesktopAuthCode.mockResolvedValue(Ok(false)); + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1', verifier}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({error: 'invalid_code'}); + expect(createSession).not.toHaveBeenCalled(); + }); + + test('a body without code and verifier is a 400', async () => { + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1'}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({error: 'invalid_body'}); + expect(getDesktopAuthCode).not.toHaveBeenCalled(); + }); + + test('a database failure is a 500, with no cookie', async () => { + createSession.mockResolvedValue(Err(ApiErr.dbError(new Error('boom')))); + const {handleDesktopSessionRoute} = await import('./desktop'); + const res = makeRes(); + + await handleDesktopSessionRoute({body: {code: 'code-1', verifier}} as never, res as never); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({error: 'internal_error'}); + expect(res.cookies).toHaveLength(0); + }); +}); diff --git a/packages/api/src/auth/desktop.ts b/packages/api/src/auth/desktop.ts new file mode 100644 index 00000000..d45672a3 --- /dev/null +++ b/packages/api/src/auth/desktop.ts @@ -0,0 +1,108 @@ +import {createHash, randomBytes} from 'node:crypto'; + +import type {Request, Response} from 'express'; +import type {Result} from 'ts-results-es'; + +import {createDesktopAuthCode, getDesktopAuthCode, takeDesktopAuthCode} from '@proxy/db/desktop-auth'; +import {desktopCodeBodySchema, desktopSessionBodySchema, type DesktopCode} from '@proxy/contracts/auth'; +import {type ApiError, Do, parseSchema} from '@proxy/utils'; + +import {log, serializeError} from '../observability/log'; +import type {AuthenticatedRequest} from '../server/middleware/require-auth'; +import {establishSession, hashToken, timingSafeEqualString} from './session'; + +/** + * Signing the desktop app in. The app can't sign in itself: Google turns embedded browsers away, + * and a magic link opens in the browser anyway. So it makes a PKCE verifier, keeps it, and opens + * the browser at `/desktop-auth?challenge=`, where the person signs in as they would + * anywhere. That page mints a code bound to the challenge (`handleDesktopCodeRoute`) and hands it + * to the app through its `personalagentproxy://` link; the app trades the code and the verifier + * for a session cookie of its own (`handleDesktopSessionRoute`), and is signed in as the browser + * is, with the same kind of session, for as long. + */ + +// Long enough to click through the browser's "open the app?" prompt, no longer. +const codeMaxAgeMs = 5 * 60 * 1000; + +/** + * `POST /auth/desktop/code` `{challenge}` — the browser's, signed in: a one-time code for the + * person, bound to the app's challenge. The code travels once, through the app's link. + */ +export function handleDesktopCodeRoute(request: AuthenticatedRequest): Promise> { + return Do(async ($) => { + const {challenge} = $(parseSchema(desktopCodeBodySchema, request.body)); + const code = randomBytes(32).toString('hex'); + $( + await createDesktopAuthCode({ + codeHash: hashToken(code), + challenge, + userId: request.user.userId, + expires: new Date(Date.now() + codeMaxAgeMs), + }), + ); + return {code}; + }); +} + +function verifierMatches(verifier: string, challenge: string): boolean { + const computed = createHash('sha256').update(verifier).digest('base64url'); + return timingSafeEqualString(computed, challenge); +} + +/** + * `POST /auth/desktop/session` `{code, verifier}` — the app's, from its own process, with no + * session and no browser Origin: the code and the verifier are what lets it in. A code that is + * unknown, expired or taken, or a verifier that isn't the challenge's, is one answer, `invalid_code`, + * so nothing is learnt by trying. The verifier is checked before the code is taken, so a wrong one + * leaves the code to the app that holds the right one. + */ +export async function handleDesktopSessionRoute(req: Request, res: Response): Promise { + const parsed = desktopSessionBodySchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({error: 'invalid_body'}); + return; + } + + const {code, verifier} = parsed.data; + const codeHash = hashToken(code); + const codeResult = await getDesktopAuthCode(codeHash); + if (codeResult.isErr()) { + log.error('Desktop sign-in failed to read the code', serializeError(codeResult.error)); + res.status(500).json({error: 'internal_error'}); + return; + } + + const authCode = codeResult.value; + if (!authCode || authCode.expires.valueOf() < Date.now()) { + res.status(400).json({error: 'invalid_code'}); + return; + } + + if (!verifierMatches(verifier, authCode.challenge)) { + log.warn('Desktop sign-in with a verifier that is not the code’s', {userId: authCode.userId}); + res.status(400).json({error: 'invalid_code'}); + return; + } + + const takenResult = await takeDesktopAuthCode(codeHash); + if (takenResult.isErr()) { + log.error('Desktop sign-in failed to take the code', serializeError(takenResult.error)); + res.status(500).json({error: 'internal_error'}); + return; + } + + if (!takenResult.value) { + res.status(400).json({error: 'invalid_code'}); + return; + } + + const sessionResult = await establishSession(res, authCode.userId); + if (sessionResult.isErr()) { + log.error('Desktop sign-in failed to create the session', serializeError(sessionResult.error)); + res.status(500).json({error: 'internal_error'}); + return; + } + + log.info('Desktop app signed in', {userId: authCode.userId}); + res.json({ok: true}); +} diff --git a/packages/api/src/server/create-api-router.ts b/packages/api/src/server/create-api-router.ts index 6d1a6cf4..1d4620a7 100644 --- a/packages/api/src/server/create-api-router.ts +++ b/packages/api/src/server/create-api-router.ts @@ -25,6 +25,7 @@ import { handleSetAgentRevokedRoute, handleShareAgentRoute, } from '../agents/route'; +import {handleDesktopCodeRoute, handleDesktopSessionRoute} from '../auth/desktop'; import {handleDevLoginRoute} from '../auth/dev-login'; import {handleEmailSignInRoute, handleEmailVerifyRoute} from '../auth/email'; import {handleGoogleCallbackRoute, handleGoogleStartRoute, handleSignInMethodsRoute} from '../auth/google'; @@ -98,6 +99,11 @@ export function createApiRouter(): express.Router { router.get('/auth/email/verify', wrapAsyncRoute('Email verify route', handleEmailVerifyRoute)); router.post('/auth/signout', requireBrowserOrigin, wrapAsyncRoute('Sign-out route', handleSignOutRoute)); router.post('/auth/dev/login', requireBrowserOrigin, wrapAsyncRoute('Dev login route', handleDevLoginRoute)); + // Signing the desktop app in (auth/desktop.ts): the code is minted by the signed-in browser, a + // write from the web app; the session is asked for by the app's own process, which has no + // session and sends no Origin, so the code and its verifier are the guard there. + router.post('/auth/desktop/code', requireBrowserOrigin, withAuthResult('Desktop code route', handleDesktopCodeRoute)); + router.post('/auth/desktop/session', wrapAsyncRoute('Desktop session route', handleDesktopSessionRoute)); router.use('/api/me', requireBrowserOrigin); router.get('/api/me', withAuthResult('Me route', handleMeRoute)); diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 0992889f..894aa47e 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -27,3 +27,23 @@ export const signInMethodsSchema = z.object({ }); export type SignInMethods = z.infer; + +/** + * Signing the desktop app in. The app opens the browser at `/desktop-auth?challenge=` with the + * SHA-256 of a verifier it keeps to itself (base64url, 43 characters); the page, signed in, asks + * `POST /auth/desktop/code` for a one-time code bound to the challenge and hands it to the app + * through its `personalagentproxy://` link. The app trades the code and the verifier at + * `POST /auth/desktop/session` for a session cookie of its own. + */ +export const desktopChallengeSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/); + +export const desktopCodeBodySchema = z.object({challenge: desktopChallengeSchema}); + +export const desktopCodeSchema = z.object({code: z.string()}); + +export type DesktopCode = z.infer; + +export const desktopSessionBodySchema = z.object({ + code: z.string().min(1).max(256), + verifier: z.string().min(1).max(256), +}); diff --git a/packages/db/package.json b/packages/db/package.json index cd1bcca4..6a75477c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,6 +9,7 @@ "./audit": "./src/audit.ts", "./auth": "./src/auth.ts", "./connection": "./src/connection.ts", + "./desktop-auth": "./src/desktop-auth.ts", "./feedback": "./src/feedback.ts", "./harness": "./src/harness.ts", "./vault": "./src/vault.ts", diff --git a/packages/db/prisma/migrations/v039_add_desktop_auth_codes/migration.sql b/packages/db/prisma/migrations/v039_add_desktop_auth_codes/migration.sql new file mode 100644 index 00000000..12e476ee --- /dev/null +++ b/packages/db/prisma/migrations/v039_add_desktop_auth_codes/migration.sql @@ -0,0 +1,19 @@ +-- One-time codes for signing the desktop app in: minted in the browser, bound to the app's PKCE +-- challenge, and traded by the app for a session. Kept only as their hash. +-- CreateTable +CREATE TABLE "DesktopAuthCode" ( + "id" TEXT NOT NULL, + "codeHash" TEXT NOT NULL, + "challenge" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DesktopAuthCode_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DesktopAuthCode_codeHash_key" ON "DesktopAuthCode"("codeHash"); + +-- CreateIndex +CREATE INDEX "DesktopAuthCode_userId_idx" ON "DesktopAuthCode"("userId"); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index cf2a7035..63ec14f3 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -42,6 +42,7 @@ model User { agents Agent[] threads AgentThread[] writeTargets WriteTarget[] + desktopCodes DesktopAuthCode[] } // A sign-in provider identity linked to a user, such as their Google account. Only who it is: @@ -78,6 +79,24 @@ model Session { @@index([userId]) } +// A one-time code for signing the desktop app in. The browser, signed in, mints one at +// /desktop-auth, bound to the PKCE challenge the app made, and the app trades it, with the +// verifier, for a session of its own (`POST /auth/desktop/session`). Minutes long and single-use, +// and kept only as its SHA-256, like a session's token. +model DesktopAuthCode { + id String @id @default(cuid()) + codeHash String @unique + challenge String + userId String + expires DateTime + + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + // A magic link waiting to be clicked. `token` is a hash, never the token from the email. model VerificationToken { identifier String diff --git a/packages/db/src/desktop-auth.ts b/packages/db/src/desktop-auth.ts new file mode 100644 index 00000000..391831c6 --- /dev/null +++ b/packages/db/src/desktop-auth.ts @@ -0,0 +1,55 @@ +import {Ok, type Result} from 'ts-results-es'; + +import {type ApiError, wrapDb} from '@proxy/utils'; + +import {db} from '.'; + +// The one-time codes that sign the desktop app in. Minted in the browser, signed in, for the PKCE +// challenge the app made; traded once by the app, with the verifier. Only the hash of a code is +// kept, so the table signs no one in. + +export type DesktopAuthCodeRow = { + userId: string; + challenge: string; + expires: Date; +}; + +/** Keeps a code; the expired ones go with it, so the table never holds more than the sign-ins under way. */ +export async function createDesktopAuthCode( + data: DesktopAuthCodeRow & {codeHash: string}, +): Promise> { + return ( + await wrapDb(() => + db.$transaction([ + db.desktopAuthCode.deleteMany({where: {expires: {lt: new Date()}}}), + db.desktopAuthCode.create({data}), + ]), + ) + ).map(() => undefined); +} + +/** + * A code as it is, left where it is: `Ok(null)` when it is unknown or taken already. An expired + * one is returned for the caller to refuse. Taking it is `takeDesktopAuthCode`, once the caller + * has checked the verifier, so a wrong one does not use the code up on the app that holds it. + */ +export async function getDesktopAuthCode( + codeHash: string, +): Promise> { + return wrapDb(() => + db.desktopAuthCode.findUnique({ + where: {codeHash}, + select: {userId: true, challenge: true, expires: true}, + }), + ); +} + +/** Takes a code, once: `Ok(false)` when it was gone already, taken by another request at the same moment. */ +export async function takeDesktopAuthCode(codeHash: string): Promise> { + const result = await wrapDb(() => db.desktopAuthCode.deleteMany({where: {codeHash}})); + if (result.isErr()) { + return result; + } + + return Ok(result.value.count > 0); +} diff --git a/packages/web/src/client/auth-client.ts b/packages/web/src/client/auth-client.ts index 7e9ace6d..c6c606e8 100644 --- a/packages/web/src/client/auth-client.ts +++ b/packages/web/src/client/auth-client.ts @@ -1,4 +1,8 @@ -import {magicLinkResponseSchema, signInMethodsSchema} from '@proxy/contracts/auth'; +import { + desktopCodeSchema, + magicLinkResponseSchema, + signInMethodsSchema, +} from '@proxy/contracts/auth'; import {httpRequest} from '@proxy/utils'; import {apiRequest, apiSend} from '@/client/request'; @@ -52,3 +56,11 @@ export async function signOut(): Promise { export function devLogin() { return apiSend('POST', '/auth/dev/login'); } + +/** + * A one-time code that signs the desktop app in as the person signed in here, bound to the + * challenge the app made (`POST /auth/desktop/code`). The /desktop-auth page hands it to the app. + */ +export function createDesktopCode(challenge: string) { + return apiRequest('POST', '/auth/desktop/code', desktopCodeSchema, {challenge}); +} diff --git a/packages/web/src/lib/desktop.ts b/packages/web/src/lib/desktop.ts new file mode 100644 index 00000000..1cd88f72 --- /dev/null +++ b/packages/web/src/lib/desktop.ts @@ -0,0 +1,9 @@ +// The desktop app, a window around this web app (packages/desktop). It signs in through the +// browser, which hands what it needs back through the app's own links, `personalagentproxy://…`. + +const DESKTOP_SCHEME = 'personalagentproxy'; + +/** The link that hands the desktop app the one-time code it trades for a session. */ +export function desktopAuthLink(code: string): string { + return `${DESKTOP_SCHEME}://auth-callback?code=${encodeURIComponent(code)}`; +} diff --git a/packages/web/src/loaders.ts b/packages/web/src/loaders.ts index 063526f9..bbd57935 100644 --- a/packages/web/src/loaders.ts +++ b/packages/web/src/loaders.ts @@ -1,5 +1,5 @@ import type {ActivityFilter} from '@proxy/contracts/activity'; -import {getSignInMethods} from '@/client/auth-client'; +import {createDesktopCode, getSignInMethods} from '@/client/auth-client'; import { getActivityTimeline, getAgent, @@ -15,6 +15,8 @@ import {getAuthorizationRequest} from '@/client/oauth-client'; import {getInvitation, getOrg, listMembers, listOrgs} from '@/client/orgs-client'; import {listRecords} from '@/client/records-client'; import {VAULT_INTEGRATION_ID} from '@proxy/integrations'; +import {desktopChallengeSchema} from '@proxy/contracts/auth'; +import {desktopAuthLink} from '@/lib/desktop'; import {LOADED_MINUTES, windowStart} from '@/lib/timeline-window'; import {unwrapLoaderResult, unwrapOrNull} from '@/lib/loader-utils'; import {pendingAgent} from '@/lib/pending-agent'; @@ -280,6 +282,20 @@ export async function inviteLoader({params}: {params: {token?: string}}) { return {me: unwrapLoaderResult(me), invitation: unwrapOrNull(invitation)}; } +// The desktop app's sign-in, `/desktop-auth?challenge=`: the one-time code for the signed-in +// person, bound to the app's challenge, in the link that hands it over. Signed out, the person +// signs in first and comes back here; a challenge that isn't one opens the app's root instead. +export async function desktopAuthLoader({request}: {request: Request}) { + const url = new URL(request.url); + const challenge = desktopChallengeSchema.safeParse(url.searchParams.get('challenge')); + if (!challenge.success) { + throw redirect('/'); + } + const signIn = `/login?callbackUrl=${encodeURIComponent(url.pathname + url.search)}`; + const {code} = unwrapLoaderResult(await createDesktopCode(challenge.data), signIn); + return {link: desktopAuthLink(code)}; +} + // The welcome flow, shown until the organization finishes or skips it: what the person has // connected so far, so each step can say what is done, and what the server has set up for the // catalog. Once finished, the app itself. diff --git a/packages/web/src/pages/desktop-auth-page.test.tsx b/packages/web/src/pages/desktop-auth-page.test.tsx new file mode 100644 index 00000000..698dba38 --- /dev/null +++ b/packages/web/src/pages/desktop-auth-page.test.tsx @@ -0,0 +1,31 @@ +/// + +import {afterEach, describe, expect, test} from 'bun:test'; +import {cleanup, render, screen} from '@testing-library/react'; +import {createMemoryRouter, RouterProvider} from 'react-router'; +import {DesktopAuthPage} from '@/pages/desktop-auth-page'; + +afterEach(() => { + cleanup(); +}); + +describe('DesktopAuthPage', () => { + test('offers the app’s link with the code, for a browser that asks before opening it', async () => { + const router = createMemoryRouter( + [ + { + path: '/desktop-auth', + loader: () => ({link: 'personalagentproxy://auth-callback?code=code-1'}), + element: , + }, + ], + {initialEntries: ['/desktop-auth?challenge=x']}, + ); + render(); + + // Base UI renders the link as a button, in role as in looks. + const open = await screen.findByRole('button', {name: 'Open Personal Agent Proxy'}); + expect(open.getAttribute('href')).toBe('personalagentproxy://auth-callback?code=code-1'); + expect(screen.getByText('Opening Personal Agent Proxy…')).toBeTruthy(); + }); +}); diff --git a/packages/web/src/pages/desktop-auth-page.tsx b/packages/web/src/pages/desktop-auth-page.tsx new file mode 100644 index 00000000..92914264 --- /dev/null +++ b/packages/web/src/pages/desktop-auth-page.tsx @@ -0,0 +1,32 @@ +import {useEffect} from 'react'; +import {useLoaderData} from 'react-router'; +import {Button} from '@proxy/ui/components/button'; +import {StepHeading} from '@/components/step-heading'; +import type {desktopAuthLoader} from '@/loaders'; + +// Where the desktop app sends the browser to sign it in, with its challenge. The loader has sent +// anyone signed out to sign in first, and back here, and minted the code; all that is left is to +// hand it to the app through its link. Browsers ask before opening another app, and some only on a +// click, so the link is on the page too. +export function DesktopAuthPage() { + const {link} = useLoaderData(); + + useEffect(() => { + window.location.assign(link); + }, [link]); + + return ( +
+
+ + You’re signed in. The app opens with this account; you can close this tab once it has. + +
+ +
+
+
+ ); +} diff --git a/packages/web/src/router.tsx b/packages/web/src/router.tsx index a39b9ef7..016c19bb 100644 --- a/packages/web/src/router.tsx +++ b/packages/web/src/router.tsx @@ -29,6 +29,7 @@ import { connectAgentLoader, connectionLoader, connectionsLoader, + desktopAuthLoader, homeLoader, inviteLoader, membersLoader, @@ -61,6 +62,7 @@ import {ChatPage} from '@/pages/chat-page'; import {ConnectAgentPage} from '@/pages/connect-agent-page'; import {ConnectionPage} from '@/pages/connection-page'; import {ConnectionsPage} from '@/pages/connections-page'; +import {DesktopAuthPage} from '@/pages/desktop-auth-page'; import {HomePage} from '@/pages/home-page'; import {InvitePage} from '@/pages/invite-page'; import {LoginPage} from '@/pages/login-page'; @@ -180,6 +182,7 @@ async function loginLoader({request}: {request: Request}): Promise}, {path: '/login', loader: loginLoader, element: }, + {path: '/desktop-auth', loader: desktopAuthLoader, element: }, {path: '/agent/login', element: }, // An MCP client, such as Claude, signing in as one of the person's agents. { From 1a1decd38baf83d52ab9ac7c01cdcdb118c3b77b Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:14:24 +0000 Subject: [PATCH 2/8] add the desktop app, a window around the hosted web app packages/desktop: Electron, with nothing of the web app or the api in it. The main process keeps the window to the app's own origin (links out of it open in the browser), shows a page of its own where the app would show its login page, with a button that signs in through the browser (previous commit), and takes the personalagentproxy:// link the browser comes back with. A page for no connection, and the window's last size and place. `bun run desktop` runs it on this checkout's web app, using a `bun run dev` already running or starting the stack itself. CI skips the Electron download its install would make. --- .github/workflows/ci.yml | 3 + bun.lock | 95 ++++++++++ package.json | 10 +- packages/desktop/.gitignore | 1 + packages/desktop/.oxlintrc.json | 5 + packages/desktop/README.md | 31 ++++ packages/desktop/build.ts | 25 +++ packages/desktop/dev.ts | 110 ++++++++++++ packages/desktop/package.json | 29 ++++ packages/desktop/src/auth.ts | 56 ++++++ packages/desktop/src/deep-link.test.ts | 53 ++++++ packages/desktop/src/deep-link.ts | 43 +++++ packages/desktop/src/env.ts | 18 ++ packages/desktop/src/main.ts | 103 +++++++++++ packages/desktop/src/templates.d.ts | 11 ++ .../desktop/src/templates/offline.tpl.html | 19 ++ .../desktop/src/templates/sign-in.tpl.html | 21 +++ packages/desktop/src/templates/splash.css | 64 +++++++ packages/desktop/src/window-state.ts | 66 +++++++ packages/desktop/src/window.ts | 163 ++++++++++++++++++ packages/desktop/tsconfig.json | 17 ++ 21 files changed, 940 insertions(+), 3 deletions(-) create mode 100644 packages/desktop/.gitignore create mode 100644 packages/desktop/.oxlintrc.json create mode 100644 packages/desktop/README.md create mode 100644 packages/desktop/build.ts create mode 100644 packages/desktop/dev.ts create mode 100644 packages/desktop/package.json create mode 100644 packages/desktop/src/auth.ts create mode 100644 packages/desktop/src/deep-link.test.ts create mode 100644 packages/desktop/src/deep-link.ts create mode 100644 packages/desktop/src/env.ts create mode 100644 packages/desktop/src/main.ts create mode 100644 packages/desktop/src/templates.d.ts create mode 100644 packages/desktop/src/templates/offline.tpl.html create mode 100644 packages/desktop/src/templates/sign-in.tpl.html create mode 100644 packages/desktop/src/templates/splash.css create mode 100644 packages/desktop/src/window-state.ts create mode 100644 packages/desktop/src/window.ts create mode 100644 packages/desktop/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e898220e..80062584 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,10 @@ jobs: - uses: oven-sh/setup-bun@v2 with: bun-version: latest + # The desktop app's checks need no Electron binary, which its install would download. - run: bun install --frozen-lockfile + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - run: bun run typecheck - run: bun run test - run: bun run build diff --git a/bun.lock b/bun.lock index e993a601..c9e7a4ea 100644 --- a/bun.lock +++ b/bun.lock @@ -66,6 +66,22 @@ "typescript": "~6.0.2", }, }, + "packages/desktop": { + "name": "desktop", + "version": "0.1.0", + "dependencies": { + "ts-results-es": "^7.1.0", + "zod": "^4.5.4", + }, + "devDependencies": { + "@types/bun": "latest", + "dotenv": "^16.6.1", + "electron": "^41.0.0", + "esbuild": "^0.27.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2", + }, + }, "packages/docs": { "name": "docs", "version": "0.0.0", @@ -240,6 +256,9 @@ }, }, }, + "trustedDependencies": [ + "electron", + ], "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.3.0", "", {}, "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="], @@ -321,8 +340,64 @@ "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.3", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.3" } }, "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg=="], + "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.8", "", {}, "sha512-8iajeLFbzNwM0m3AsQZSZgYREWglAImyGvvFSWU34FgMW8sHlzdwj9QKvv9dgN7jMP591EFUF/sLOC3Jl9YIjQ=="], + + "@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], @@ -901,6 +976,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "desktop": ["desktop@workspace:packages/desktop"], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], @@ -941,6 +1018,8 @@ "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + "electron": ["electron@41.10.7", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js" } }, "sha512-AqIiefddlf5i+HYCGatr8VlBuEbWJpad4/yloBFYcMB2r57j7xhW1ogiJ+BfA5kxQyfmSs5yjwy54Me+wJn1jw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.446", "", {}, "sha512-kWlGMPHzuitCKx0O+MbY1ZGvQmP14JtckrnGjD4JsusTxh2cX9wu4bb4rOYZqLSvENjFFrHzlqY8kXhseucR/w=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -969,6 +1048,8 @@ "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], @@ -1523,6 +1604,8 @@ "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -1719,6 +1802,8 @@ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="], "systeminformation": ["systeminformation@5.33.15", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-s5RloL0F7xhN0ZjfHtLGnbf7EW74EI0N8NFL+1xR7Oxz9QA33obNZsl7lyfnJOhN04E/kGFPoJPlvY7TZ5fzmw=="], @@ -1889,6 +1974,10 @@ "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "@electron/get/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@electron/get/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -2005,6 +2094,8 @@ "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "sumchecker/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], "three-stdlib/fflate": ["fflate@0.6.11", "", {}, "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg=="], @@ -2033,6 +2124,8 @@ "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + "@electron/get/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], @@ -2095,6 +2188,8 @@ "stats-gl/@types/three/meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "sumchecker/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "website/@types/three/meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], diff --git a/package.json b/package.json index 1ea780dd..7b91d653 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,14 @@ "seed": "bun run --cwd packages/api seed", "build": "bun run --cwd packages/web build && bun run --cwd packages/docs build && bun run --cwd packages/website build", "typecheck": "bun run --filter './packages/*' typecheck", - "lint": "bun run --cwd packages/web lint && bun run --cwd packages/docs lint && bun run --cwd packages/website lint && bun run --cwd packages/map lint", - "test": "bun run --filter './packages/*' test" + "lint": "bun run --cwd packages/web lint && bun run --cwd packages/docs lint && bun run --cwd packages/website lint && bun run --cwd packages/map lint && bun run --cwd packages/desktop lint", + "test": "bun run --filter './packages/*' test", + "desktop": "bun run --cwd packages/desktop desktop" }, "devDependencies": { "prettier": "^3.5.3" - } + }, + "trustedDependencies": [ + "electron" + ] } diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 00000000..151080d1 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1 @@ +release/ diff --git a/packages/desktop/.oxlintrc.json b/packages/desktop/.oxlintrc.json new file mode 100644 index 00000000..c717bade --- /dev/null +++ b/packages/desktop/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "oxc"], + "ignorePatterns": ["dist", "release"] +} diff --git a/packages/desktop/README.md b/packages/desktop/README.md new file mode 100644 index 00000000..5bfc1516 --- /dev/null +++ b/packages/desktop/README.md @@ -0,0 +1,31 @@ +# Personal Agent Proxy for the desktop + +A window around the hosted web app, https://app.personalagentproxy.com, with nothing of the web +app or the api in it. The app gives the window, signing in through the browser, and keeps +everything but the app's own origin out of the window: links out of the app open in the browser. + +## How it works + +- `src/main.ts` starts the app, adds `PersonalAgentProxyDesktop/` to the user agent so the + web app can tell where it is, and takes the app's links, `personalagentproxy://…`. +- `src/window.ts` loads the web app and keeps the window to it. Where the web app would show its + login page, the window shows a page of its own with a button that opens the browser; with no + connection, one that says so. +- Signing in (`src/auth.ts`, `packages/api/src/auth/desktop.ts`): the app keeps a PKCE verifier and + opens the browser at `/login?callbackUrl=/desktop-auth?challenge=…`. Signed in there, the + `/desktop-auth` page mints a one-time code bound to the challenge and hands it back through + `personalagentproxy://auth-callback?code=…`. The app trades code and verifier at + `POST /auth/desktop/session` for a session cookie, which its window keeps, and is signed in as a + browser is. +- The main process is bundled by `build.ts` into `dist/main.cjs`, the splash pages as text in it. + +## Development + +```bash +bun run desktop +``` + +A `bun run dev` already running is used as it is; without one, the stack is started and stopped +again when the window closes. The app loads the root `.env`'s `APP_URL`, the one origin the api +takes sign-ins from. `DESKTOP_APP_URL` names another and skips the stack. A packaged app always +loads hosted Personal Agent Proxy. diff --git a/packages/desktop/build.ts b/packages/desktop/build.ts new file mode 100644 index 00000000..9c81ff74 --- /dev/null +++ b/packages/desktop/build.ts @@ -0,0 +1,25 @@ +// Bundles the main process into one file, dist/main.cjs, with every dependency in it; only +// `electron` stays outside, since the runtime is it. The splash pages and their stylesheet are +// bundled as text. +import {build} from 'esbuild'; + +import pkg from './package.json'; + +const production = process.env.NODE_ENV === 'production'; + +await build({ + entryPoints: ['src/main.ts'], + bundle: true, + platform: 'node', + format: 'cjs', + outdir: 'dist', + outExtension: {'.js': '.cjs'}, + external: ['electron'], + loader: {'.html': 'text', '.css': 'text'}, + minify: production, + sourcemap: !production, + define: { + 'process.env.DESKTOP_VERSION': JSON.stringify(pkg.version), + 'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'), + }, +}); diff --git a/packages/desktop/dev.ts b/packages/desktop/dev.ts new file mode 100644 index 00000000..57d0397d --- /dev/null +++ b/packages/desktop/dev.ts @@ -0,0 +1,110 @@ +// `bun run desktop`: the desktop app on this checkout's web app. A `bun run dev` already running +// is used as it is; without one the stack is started here, and stopped again when the window +// closes. (Not a `dev` script: `bun run dev` runs every package's, and this one would start a +// second stack under the first.) The app loads the root .env's APP_URL, the one origin the api takes sign-ins from; +// DESKTOP_APP_URL names another and skips the stack. +import {spawn, spawnSync, type ChildProcess} from 'node:child_process'; +import {existsSync, readFileSync} from 'node:fs'; +import path from 'node:path'; +import {setTimeout as sleep} from 'node:timers/promises'; + +import {parse} from 'dotenv'; +import {Result} from 'ts-results-es'; + +const packageDir = import.meta.dir; +const root = path.resolve(packageDir, '../..'); +const START_TIMEOUT_MS = 180_000; + +let stack: ChildProcess | null = null; +let electron: ChildProcess | null = null; + +function fail(message: string): never { + console.error(`[desktop] ${message}`); + stack?.kill('SIGTERM'); + process.exit(1); +} + +function configuredAppUrl(): string { + const env = Result.wrap(() => parse(readFileSync(path.join(root, '.env'), 'utf8'))); + return env.unwrapOr>({}).APP_URL ?? 'http://localhost:5173'; +} + +async function isUp(url: string): Promise { + const response = await Result.wrapAsync(() => fetch(url, {signal: AbortSignal.timeout(2_000)})); + return response.isOk(); +} + +async function waitUntilUp(url: string): Promise { + const deadline = Date.now() + START_TIMEOUT_MS; + while (!(await isUp(url))) { + if (stack && stack.exitCode !== null) { + fail('The dev stack stopped before the web app came up.'); + } + if (Date.now() > deadline) { + fail(`${url} did not come up in time.`); + } + await sleep(500); + } +} + +async function resolveAppUrl(): Promise { + const override = process.env.DESKTOP_APP_URL; + if (override) { + return override; + } + + const appUrl = configuredAppUrl(); + if (await isUp(appUrl)) { + console.log(`[desktop] Using the dev stack running at ${appUrl}`); + return appUrl; + } + + if (!existsSync(path.join(root, '.env'))) { + fail( + `Nothing answers at ${appUrl}, and there is no .env to start the dev stack with. Copy .env.example to .env, or run the stack elsewhere and bind its port here.`, + ); + } + + console.log('[desktop] Starting the dev stack (bun run dev)'); + stack = spawn('node', [path.join(root, 'scripts/dev.mjs')], {cwd: root, stdio: 'inherit'}); + stack.on('exit', () => { + // A window on a stack that is gone is no use. + electron?.kill('SIGTERM'); + }); + await waitUntilUp(appUrl); + return appUrl; +} + +const appUrl = await resolveAppUrl(); +console.log(`[desktop] App at ${appUrl}`); + +const built = spawnSync('bun', ['run', 'build.ts'], {cwd: packageDir, stdio: 'inherit'}); +if (built.status !== 0) { + fail('The build failed.'); +} + +electron = spawn( + path.join(packageDir, 'node_modules/.bin/electron'), + ['.', ...process.argv.slice(2)], + { + cwd: packageDir, + env: {...process.env, DESKTOP_APP_URL: appUrl}, + stdio: 'inherit', + }, +); + +electron.on('exit', (code) => { + // Only a stack started here is stopped; one found running belongs to the terminal that ran it. + if (!stack || stack.exitCode !== null) { + process.exit(code ?? 0); + } + stack.on('exit', () => process.exit(code ?? 0)); + stack.kill('SIGTERM'); +}); + +for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.on(signal, () => { + electron?.kill('SIGTERM'); + stack?.kill('SIGTERM'); + }); +} diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 00000000..fcba1bad --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,29 @@ +{ + "name": "desktop", + "productName": "Personal Agent Proxy", + "private": true, + "version": "0.1.0", + "description": "Personal Agent Proxy", + "author": "Personal Agent Proxy ", + "type": "module", + "main": "dist/main.cjs", + "scripts": { + "desktop": "bun run dev.ts", + "build": "bun run build.ts", + "lint": "oxlint", + "test": "bun test", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "ts-results-es": "^7.1.0", + "zod": "^4.5.4" + }, + "devDependencies": { + "@types/bun": "latest", + "dotenv": "^16.6.1", + "electron": "^41.0.0", + "esbuild": "^0.27.0", + "oxlint": "^1.79.0", + "typescript": "~6.0.2" + } +} diff --git a/packages/desktop/src/auth.ts b/packages/desktop/src/auth.ts new file mode 100644 index 00000000..e3c35297 --- /dev/null +++ b/packages/desktop/src/auth.ts @@ -0,0 +1,56 @@ +import {createHash, randomBytes} from 'node:crypto'; + +import {session} from 'electron'; +import {Err, Ok, Result} from 'ts-results-es'; + +import {APP_URL} from './env'; + +// Signing in, with the browser (packages/api/src/auth/desktop.ts): the app keeps a PKCE verifier +// and sends the browser its challenge; the browser comes back, through the app's link, with a +// one-time code bound to it; the app trades code and verifier for a session cookie. The verifier +// never leaves this process, so another app that got hold of the link could not redeem the code. + +let verifier: string | null = null; + +/** Where the browser signs in: the login page, coming back to /desktop-auth with the challenge. */ +export function beginSignIn(): string { + verifier = randomBytes(32).toString('base64url'); + const challenge = createHash('sha256').update(verifier).digest('base64url'); + const callbackUrl = `/desktop-auth?challenge=${challenge}`; + + return `${APP_URL}/login?callbackUrl=${encodeURIComponent(callbackUrl)}`; +} + +/** + * Trades the code from the link, with the verifier, for a session: the api answers with the + * cookie, which the window's session keeps, so the app is signed in as a browser is. `Err` has + * what to tell the person. + */ +export async function finishSignIn(code: string): Promise> { + if (!verifier) { + return Err('This sign-in was begun by another run of the app. Sign in again.'); + } + + // The window's session makes the request, so the cookie the api sets lands where the app's + // pages read it. It follows the system's proxy settings too, as the window does. + const response = await Result.wrapAsync(() => + session.defaultSession.fetch(`${APP_URL}/auth/desktop/session`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({code, verifier}), + credentials: 'include', + }), + ); + if (response.isErr()) { + // The api was never reached, so the code is still good: the verifier stays for a retry. + return Err('Personal Agent Proxy could not be reached. Check your connection and try again.'); + } + + // The api has seen the code, and decided; the verifier is spent either way. + verifier = null; + if (!response.value.ok) { + return Err('Signing in did not work. Sign in again.'); + } + + return Ok(undefined); +} diff --git a/packages/desktop/src/deep-link.test.ts b/packages/desktop/src/deep-link.test.ts new file mode 100644 index 00000000..dc3a8b45 --- /dev/null +++ b/packages/desktop/src/deep-link.test.ts @@ -0,0 +1,53 @@ +import {describe, expect, test} from 'bun:test'; + +import {findDeepLink, parseDeepLink} from './deep-link'; + +describe('parseDeepLink', () => { + test('reads the host form, as macOS delivers it', () => { + expect(parseDeepLink('personalagentproxy://auth-callback?code=abc123')).toEqual({ + kind: 'auth-callback', + code: 'abc123', + }); + }); + + test('reads the path form', () => { + expect(parseDeepLink('personalagentproxy:///auth-callback?code=abc123')).toEqual({ + kind: 'auth-callback', + code: 'abc123', + }); + }); + + test('is only for the app’s own scheme', () => { + expect(parseDeepLink('https://auth-callback?code=abc123')).toBeNull(); + expect(parseDeepLink('personalagentproxy-evil://auth-callback?code=abc123')).toBeNull(); + }); + + test('knows no other target', () => { + expect(parseDeepLink('personalagentproxy://open-project?id=123')).toBeNull(); + }); + + test('needs a code', () => { + expect(parseDeepLink('personalagentproxy://auth-callback')).toBeNull(); + expect(parseDeepLink('personalagentproxy://auth-callback?code=')).toBeNull(); + }); + + test('is not a link at all', () => { + expect(parseDeepLink('not a url')).toBeNull(); + }); +}); + +describe('findDeepLink', () => { + test('finds the link among a second instance’s arguments', () => { + expect( + findDeepLink([ + '/Applications/App', + '--no-sandbox', + 'personalagentproxy://auth-callback?code=x', + ]), + ).toEqual({kind: 'auth-callback', code: 'x'}); + }); + + test('finds none in a plain launch', () => { + expect(findDeepLink(['/Applications/App', '.'])).toBeNull(); + }); +}); diff --git a/packages/desktop/src/deep-link.ts b/packages/desktop/src/deep-link.ts new file mode 100644 index 00000000..3349d3df --- /dev/null +++ b/packages/desktop/src/deep-link.ts @@ -0,0 +1,43 @@ +// The app's own links, `personalagentproxy://…`, which the browser hands things back through: +// the one-time code of a sign-in (`auth-callback?code=`). macOS delivers them as the host form, +// `personalagentproxy://auth-callback?code=…`; the path form, with a third slash, is accepted too. + +export const DEEP_LINK_SCHEME = 'personalagentproxy'; + +export type DeepLink = {kind: 'auth-callback'; code: string}; + +/** The link as what it asks for, or null for anything that isn't one of the app's. */ +export function parseDeepLink(raw: string): DeepLink | null { + if (!URL.canParse(raw)) { + return null; + } + + const url = new URL(raw); + if (url.protocol !== `${DEEP_LINK_SCHEME}:`) { + return null; + } + + const target = url.hostname || url.pathname.replace(/^\/+/, ''); + if (target !== 'auth-callback') { + return null; + } + + const code = url.searchParams.get('code'); + if (!code) { + return null; + } + + return {kind: 'auth-callback', code}; +} + +/** The app's link among a command line's arguments, as Windows and Linux hand it to a second instance. */ +export function findDeepLink(argv: string[]): DeepLink | null { + for (const argument of argv) { + const link = parseDeepLink(argument); + if (link) { + return link; + } + } + + return null; +} diff --git a/packages/desktop/src/env.ts b/packages/desktop/src/env.ts new file mode 100644 index 00000000..f977c165 --- /dev/null +++ b/packages/desktop/src/env.ts @@ -0,0 +1,18 @@ +import {app} from 'electron'; + +const PRODUCTION_APP_URL = 'https://app.personalagentproxy.com'; + +// A packaged app is hosted Personal Agent Proxy's. In development, DESKTOP_APP_URL, which dev.ts +// sets from the root .env's APP_URL: the api refuses sign-ins from any other origin. +function resolveAppUrl(): string { + if (app.isPackaged) { + return PRODUCTION_APP_URL; + } + + return process.env.DESKTOP_APP_URL ?? 'http://localhost:5173'; +} + +export const APP_URL = resolveAppUrl(); +export const APP_ORIGIN = new URL(APP_URL).origin; +// build.ts puts the package's version in. +export const DESKTOP_VERSION = process.env.DESKTOP_VERSION ?? '0.0.0'; diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts new file mode 100644 index 00000000..03a6ca0e --- /dev/null +++ b/packages/desktop/src/main.ts @@ -0,0 +1,103 @@ +import path from 'node:path'; + +import {app, dialog, type BrowserWindow} from 'electron'; + +import {finishSignIn} from './auth'; +import {DEEP_LINK_SCHEME, findDeepLink, parseDeepLink, type DeepLink} from './deep-link'; +import {DESKTOP_VERSION} from './env'; +import {createMainWindow, loadApp, showSignIn} from './window'; + +// The desktop app: a window around the web app, hosted, with nothing of it bundled. The main +// process keeps the window, signs in through the browser, and takes the app's links. + +// So the web app can tell it is in the desktop app, and which version. +app.userAgentFallback = `${app.userAgentFallback} PersonalAgentProxyDesktop/${DESKTOP_VERSION}`; + +let mainWindow: BrowserWindow | null = null; + +function window(): BrowserWindow { + if (mainWindow && !mainWindow.isDestroyed()) { + return mainWindow; + } + + mainWindow = createMainWindow(); + return mainWindow; +} + +function focusWindow(): BrowserWindow { + const win = window(); + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + return win; +} + +async function onDeepLink(link: DeepLink): Promise { + await app.whenReady(); + const win = focusWindow(); + + const signedIn = await finishSignIn(link.code); + if (signedIn.isErr()) { + showSignIn(win); + await dialog.showMessageBox(win, { + type: 'error', + message: 'Could not sign in', + detail: signedIn.error, + }); + return; + } + + loadApp(win); +} + +// One instance: a link opened while the app runs reaches it, not a second copy. +if (!app.requestSingleInstanceLock()) { + app.quit(); +} else { + app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME); + + // macOS hands the app its links as they are opened, running or not. + app.on('open-url', (event, url) => { + event.preventDefault(); + const link = parseDeepLink(url); + if (link) { + void onDeepLink(link); + } + }); + + // Windows and Linux start a second instance with the link among its arguments, which comes here. + app.on('second-instance', (_event, argv) => { + const link = findDeepLink(argv); + if (!link) { + focusWindow(); + return; + } + void onDeepLink(link); + }); + + // On macOS the app stays open without a window, and the Dock icon opens one again. + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); + app.on('activate', () => { + if (mainWindow && !mainWindow.isDestroyed()) { + return; + } + loadApp(window()); + }); + + void app.whenReady().then(() => { + // A packaged app has its icon; a development run would show Electron's in the Dock. + if (!app.isPackaged && process.platform === 'darwin') { + app.dock?.setIcon(path.join(__dirname, '../build/icon.png')); + } + loadApp(window()); + const link = findDeepLink(process.argv); + if (link) { + void onDeepLink(link); + } + }); +} diff --git a/packages/desktop/src/templates.d.ts b/packages/desktop/src/templates.d.ts new file mode 100644 index 00000000..6b1da054 --- /dev/null +++ b/packages/desktop/src/templates.d.ts @@ -0,0 +1,11 @@ +// build.ts bundles the splash pages, and their stylesheet, as text. (`*.html` is Bun's own +// `HTMLBundle` to @types/bun, so the pages are `.tpl.html`.) +declare module '*.tpl.html' { + const content: string; + export default content; +} + +declare module '*.css' { + const content: string; + export default content; +} diff --git a/packages/desktop/src/templates/offline.tpl.html b/packages/desktop/src/templates/offline.tpl.html new file mode 100644 index 00000000..03a9c4b7 --- /dev/null +++ b/packages/desktop/src/templates/offline.tpl.html @@ -0,0 +1,19 @@ + + + + + Personal Agent Proxy + + + +
+
+

Personal Agent Proxy can’t be reached

+

Check your connection and try again.

+
+ +
+ + diff --git a/packages/desktop/src/templates/sign-in.tpl.html b/packages/desktop/src/templates/sign-in.tpl.html new file mode 100644 index 00000000..dd52997b --- /dev/null +++ b/packages/desktop/src/templates/sign-in.tpl.html @@ -0,0 +1,21 @@ + + + + + Personal Agent Proxy + + + +
+
+

Sign in to Personal Agent Proxy

+

+ Signing in happens in your browser. Once you’re signed in there, you’ll be sent back here. +

+
+ +
+ + diff --git a/packages/desktop/src/templates/splash.css b/packages/desktop/src/templates/splash.css new file mode 100644 index 00000000..06be6cda --- /dev/null +++ b/packages/desktop/src/templates/splash.css @@ -0,0 +1,64 @@ +/* The splash pages, in the web app's colours (packages/ui/src/styles.css). Light unless window.ts + puts class="dark" on , as the system's appearance says. */ +:root { + color-scheme: light; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --muted-foreground: oklch(0.556 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); +} +:root.dark { + color-scheme: dark; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --muted-foreground: oklch(0.708 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); +} +body { + margin: 0; + display: flex; + justify-content: center; + min-height: 100vh; + padding: 12vh 1rem 1rem; + box-sizing: border-box; + background: var(--background); + color: var(--foreground); + font-family: + ui-sans-serif, + system-ui, + -apple-system, + sans-serif; + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +main { + display: grid; + gap: 1.5rem; + align-content: start; + width: 100%; + max-width: 32rem; +} +h1 { + margin: 0 0 0.25rem; + font-size: 1rem; + font-weight: 500; +} +p { + margin: 0; + color: var(--muted-foreground); +} +a { + display: inline-block; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: var(--primary); + color: var(--primary-foreground); + font-weight: 500; + text-decoration: none; +} +a:hover { + opacity: 0.8; +} diff --git a/packages/desktop/src/window-state.ts b/packages/desktop/src/window-state.ts new file mode 100644 index 00000000..08a6956d --- /dev/null +++ b/packages/desktop/src/window-state.ts @@ -0,0 +1,66 @@ +import {readFileSync, writeFileSync} from 'node:fs'; +import path from 'node:path'; + +import {app, screen, type BrowserWindow, type Rectangle} from 'electron'; +import {Result} from 'ts-results-es'; +import {z} from 'zod'; + +// The window as it was last closed, so it opens the same size in the same place. + +const DEFAULT_BOUNDS = {width: 1440, height: 900}; + +const boundsSchema = z.object({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number(), +}); + +function stateFile(): string { + return path.join(app.getPath('userData'), 'window-state.json'); +} + +// No file yet, or not what the app wrote, is the default size. +function readSavedBounds(): Rectangle | null { + const saved = Result.wrap(() => JSON.parse(readFileSync(stateFile(), 'utf8'))); + if (saved.isErr()) { + return null; + } + + const parsed = boundsSchema.safeParse(saved.value); + if (!parsed.success) { + return null; + } + + // A display since unplugged would put the window off every screen. + const bounds = parsed.data; + const visible = screen.getAllDisplays().some((display) => { + const area = display.workArea; + return ( + bounds.x < area.x + area.width && + bounds.x + bounds.width > area.x && + bounds.y < area.y + area.height && + bounds.y + bounds.height > area.y + ); + }); + if (!visible) { + return null; + } + + return bounds; +} + +export function initialWindowBounds(): Electron.BrowserWindowConstructorOptions { + return readSavedBounds() ?? DEFAULT_BOUNDS; +} + +export function keepWindowBounds(win: BrowserWindow): void { + win.on('close', () => { + if (win.isFullScreen() || win.isMinimized()) { + return; + } + + // Opening at the default size next time is no loss worth reporting. + Result.wrap(() => writeFileSync(stateFile(), JSON.stringify(win.getNormalBounds()))); + }); +} diff --git a/packages/desktop/src/window.ts b/packages/desktop/src/window.ts new file mode 100644 index 00000000..f82dd72d --- /dev/null +++ b/packages/desktop/src/window.ts @@ -0,0 +1,163 @@ +import path from 'node:path'; + +import {app, BrowserWindow, nativeTheme, net, shell} from 'electron'; + +import {beginSignIn} from './auth'; +import {APP_ORIGIN, APP_URL} from './env'; +import offlineHtml from './templates/offline.tpl.html'; +import signInHtml from './templates/sign-in.tpl.html'; +import splashCss from './templates/splash.css'; +import {initialWindowBounds, keepWindowBounds} from './window-state'; + +// The window: the web app, and in its place, when there is no one signed in or no connection, +// one of two pages of the app's own. Links out of the app, and anything but the app's own +// origin, open in the browser. + +// What the splash pages' buttons ask: intercepted as navigations, never loaded. +const SIGN_IN_URL = 'personalagentproxy-desktop://sign-in'; +const RETRY_URL = 'personalagentproxy-desktop://retry'; + +const EXTERNAL_SCHEMES = new Set(['http:', 'https:', 'mailto:']); + +function urlOf(raw: string): URL | null { + if (!URL.canParse(raw)) { + return null; + } + + return new URL(raw); +} + +// A load that fails, or is cut short by the next one, is reported by did-fail-load; the promise's +// rejection says nothing more. +function load(win: BrowserWindow, url: string): void { + win.loadURL(url).catch(() => undefined); +} + +// The splash pages share a stylesheet, in a comment the page keeps a place for, and follow the +// system's appearance: the app's own theme setting lives in its pages, out of reach here. +function splashUrl(html: string): string { + const styled = html.replace('/* splash.css */', splashCss); + const themed = nativeTheme.shouldUseDarkColors + ? styled.replace('', '') + : styled; + return `data:text/html;charset=utf-8,${encodeURIComponent(themed)}`; +} + +// The app's own origin, and the splash pages. +function isAllowed(url: URL): boolean { + return url.protocol === 'data:' || url.origin === APP_ORIGIN; +} + +// The person's sign-in page. Not the agents' (/agent/login), which works in the window as it is. +function isLoginPage(url: URL): boolean { + return url.origin === APP_ORIGIN && url.pathname === '/login'; +} + +export function openExternal(raw: string): void { + const url = urlOf(raw); + if (!url || !EXTERNAL_SCHEMES.has(url.protocol)) { + return; + } + + void shell.openExternal(raw); +} + +/** The page that offers signing in: shown where the app would show its login page. */ +export function showSignIn(win: BrowserWindow): void { + load(win, splashUrl(signInHtml)); +} + +/** Opens the browser on the sign-in, with a fresh challenge, and waits for it to come back. */ +export function startSignIn(win: BrowserWindow): void { + showSignIn(win); + openExternal(beginSignIn()); +} + +/** The web app, or, with no connection, the page that says so. */ +export function loadApp(win: BrowserWindow, path = '/'): void { + if (!net.isOnline()) { + load(win, splashUrl(offlineHtml)); + return; + } + + load(win, `${APP_URL}${path}`); +} + +export function createMainWindow(): BrowserWindow { + const win = new BrowserWindow({ + ...initialWindowBounds(), + show: false, + minWidth: 800, + minHeight: 600, + // The window's icon where the window carries one, Windows and Linux, in a development run; + // a packaged app has its own, and no build/ beside it. + ...(app.isPackaged ? {} : {icon: path.join(__dirname, '../build/icon.png')}), + backgroundColor: nativeTheme.shouldUseDarkColors ? '#0a0a0a' : '#ffffff', + }); + + keepWindowBounds(win); + win.once('ready-to-show', () => win.show()); + + // Links that open a new window, such as the docs', open in the browser. + win.webContents.setWindowOpenHandler(({url}) => { + openExternal(url); + return {action: 'deny'}; + }); + + win.webContents.on('will-navigate', (event, raw) => { + if (raw === SIGN_IN_URL) { + event.preventDefault(); + startSignIn(win); + return; + } + + if (raw === RETRY_URL) { + event.preventDefault(); + loadApp(win); + return; + } + + const url = urlOf(raw); + if (!url) { + event.preventDefault(); + return; + } + + if (isLoginPage(url)) { + event.preventDefault(); + showSignIn(win); + return; + } + + if (!isAllowed(url)) { + event.preventDefault(); + openExternal(raw); + } + }); + + // The app sends a signed-out person to its login page from within, which is no navigation to + // intercept, so where the window ends up is watched too. + const onArrived = (_event: Electron.Event, raw: string) => { + const url = urlOf(raw); + if (url && isLoginPage(url)) { + showSignIn(win); + } + }; + win.webContents.on('did-navigate', onArrived); + win.webContents.on('did-navigate-in-page', onArrived); + + win.webContents.on('did-fail-load', (_event, errorCode, _description, _url, isMainFrame) => { + if (!isMainFrame) { + return; + } + + // ERR_ABORTED: a navigation the window cancelled itself. + if (errorCode === -3) { + return; + } + + load(win, splashUrl(offlineHtml)); + }); + + return win; +} diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 00000000..9179890d --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["bun"] + }, + "include": ["src/**/*.ts", "build.ts", "dev.ts"] +} From 63082a5e0c4098f94fd4f62772ae2cfe2fd81bf8 Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:28:18 +0000 Subject: [PATCH 3/8] connect services from the desktop app through the browser A service's sign-in page turns the app's window away, as Google's does, and the sign-in's handshake lives in a cookie, so the whole of it has to happen in one browser. From the desktop app it now runs in the person's browser: the web app, which knows the window by its user agent, asks the api for a handoff (POST /auth/desktop/handoff), an address with a one-time code that signs the browser in as the same person, unless it is already, and goes on to the service's sign-in, marked ?desktop=1. That sign-in ends on /desktop-return, which hands back through personalagentproxy://open?path=, and the window opens the page the sign-in would have ended on, with its error where it did not finish. A custom MCP server that asks for a sign-in moves its form to the browser, filled in as it was, to be sent again there. --- packages/api/src/auth/desktop.test.ts | 125 ++++++++++++++++++ packages/api/src/auth/desktop.ts | 89 ++++++++++++- packages/api/src/connections/connect-flow.ts | 96 ++++++++++---- .../api/src/connections/google/route.test.ts | 14 ++ packages/api/src/server/create-api-router.ts | 6 +- packages/contracts/src/auth.ts | 12 ++ .../v040_add_desktop_handoffs/migration.sql | 20 +++ packages/db/prisma/schema.prisma | 19 +++ packages/db/src/desktop-auth.ts | 53 +++++++- packages/desktop/README.md | 7 + packages/desktop/src/deep-link.test.ts | 16 +++ packages/desktop/src/deep-link.ts | 33 +++-- packages/desktop/src/main.ts | 10 +- packages/web/src/client/auth-client.ts | 10 ++ .../web/src/components/connect-mcp-form.tsx | 33 ++++- .../web/src/components/connection-access.tsx | 3 +- packages/web/src/components/icon-button.tsx | 9 +- .../src/components/integration-catalog.tsx | 9 +- .../src/components/widgets/connect-widget.tsx | 3 +- packages/web/src/lib/desktop.ts | 55 +++++++- packages/web/src/lib/integrations.ts | 2 + .../src/pages/desktop-return-page.test.tsx | 36 +++++ .../web/src/pages/desktop-return-page.tsx | 33 +++++ packages/web/src/router.tsx | 2 + 24 files changed, 632 insertions(+), 63 deletions(-) create mode 100644 packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql create mode 100644 packages/web/src/pages/desktop-return-page.test.tsx create mode 100644 packages/web/src/pages/desktop-return-page.tsx diff --git a/packages/api/src/auth/desktop.test.ts b/packages/api/src/auth/desktop.test.ts index 8de5be4f..33b3d3fa 100644 --- a/packages/api/src/auth/desktop.test.ts +++ b/packages/api/src/auth/desktop.test.ts @@ -189,3 +189,128 @@ describe('handleDesktopSessionRoute', () => { expect(res.cookies).toHaveLength(0); }); }); + +const createDesktopHandoff = mock(); +const takeDesktopHandoff = mock(); + +mock.module('@proxy/db/desktop-auth', () => ({ + createDesktopAuthCode, + getDesktopAuthCode, + takeDesktopAuthCode, + createDesktopHandoff, + takeDesktopHandoff, +})); + +const authenticateApiRequest = mock(); + +mock.module('../utils/auth', () => ({authenticateApiRequest})); + +function makeNavigationRes() { + const cookies: RecordedCookie[] = []; + const res = { + cookies, + cookie: mock((name: string, value: string, options: RecordedCookie['options']) => { + cookies.push({name, value, options}); + }), + redirect: mock(), + status: mock(() => res), + json: mock(() => res), + }; + return res; +} + +const START = '/api/orgs/acme/connections/google/start?desktop=1'; + +function liveHandoff() { + return {userId: 'user-1', next: START, expires: new Date(Date.now() + 60_000)}; +} + +beforeEach(() => { + createDesktopHandoff.mockResolvedValue(Ok(undefined)); + takeDesktopHandoff.mockResolvedValue(Ok(liveHandoff())); + authenticateApiRequest.mockResolvedValue(Err(ApiErr.unauthenticated())); +}); + +describe('handleDesktopHandoffRoute', () => { + test('keeps where the browser goes on to, for the person, and answers the address to open', async () => { + const {handleDesktopHandoffRoute} = await import('./desktop'); + const before = Date.now(); + + const result = await handleDesktopHandoffRoute({body: {next: START}, user: {userId: 'user-1'}} as never); + + const {url} = result.unwrap(); + const code = new URL(url).searchParams.get('code') ?? ''; + expect(url).toBe(`https://app.example.com/auth/desktop/handoff?code=${code}`); + expect(code).toMatch(/^[0-9a-f]{64}$/); + const stored = createDesktopHandoff.mock.calls[0]?.[0] as {codeHash: string; userId: string; next: string; expires: Date}; + expect(stored).toMatchObject({codeHash: sha256(code), userId: 'user-1', next: START}); + expect(stored.expires.valueOf() - before).toBeLessThanOrEqual(2 * 60 * 1000 + 1000); + }); + + test('goes on only to a path in the app', async () => { + const {handleDesktopHandoffRoute} = await import('./desktop'); + + for (const next of ['https://evil.example/', '//evil.example/', 'connections']) { + const result = await handleDesktopHandoffRoute({body: {next}, user: {userId: 'user-1'}} as never); + expect(result.isErr()).toBe(true); + } + expect(createDesktopHandoff).not.toHaveBeenCalled(); + }); +}); + +describe('handleDesktopHandoffOpenRoute', () => { + test('signs the browser in as the app’s person and sends it on', async () => { + const {handleDesktopHandoffOpenRoute} = await import('./desktop'); + const res = makeNavigationRes(); + + await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {}} as never, res as never); + + expect(takeDesktopHandoff).toHaveBeenCalledWith(sha256('code-1')); + expect(createSession.mock.calls[0]?.[0]).toMatchObject({userId: 'user-1'}); + expect(res.cookies).toHaveLength(1); + expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); + }); + + test('a browser already signed in as the same person keeps its session', async () => { + authenticateApiRequest.mockResolvedValue(Ok({userId: 'user-1', email: 'a@example.com'})); + const {handleDesktopHandoffOpenRoute} = await import('./desktop'); + const res = makeNavigationRes(); + + await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {cookie: 'x'}} as never, res as never); + + expect(createSession).not.toHaveBeenCalled(); + expect(res.cookies).toHaveLength(0); + expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); + }); + + test('a browser signed in as someone else is signed in as the app’s person', async () => { + authenticateApiRequest.mockResolvedValue(Ok({userId: 'user-2', email: 'b@example.com'})); + const {handleDesktopHandoffOpenRoute} = await import('./desktop'); + const res = makeNavigationRes(); + + await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {cookie: 'x'}} as never, res as never); + + expect(createSession.mock.calls[0]?.[0]).toMatchObject({userId: 'user-1'}); + expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); + }); + + test('an unknown, taken or expired code lands on the login page, as a spent magic link does', async () => { + const {handleDesktopHandoffOpenRoute} = await import('./desktop'); + + takeDesktopHandoff.mockResolvedValue(Ok(null)); + const unknown = makeNavigationRes(); + await handleDesktopHandoffOpenRoute({query: {code: 'nope'}, headers: {}} as never, unknown as never); + expect(unknown.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); + + takeDesktopHandoff.mockResolvedValue(Ok({...liveHandoff(), expires: new Date(Date.now() - 1000)})); + const expired = makeNavigationRes(); + await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {}} as never, expired as never); + expect(expired.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); + + const missing = makeNavigationRes(); + await handleDesktopHandoffOpenRoute({query: {}, headers: {}} as never, missing as never); + expect(missing.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); + + expect(createSession).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/api/src/auth/desktop.ts b/packages/api/src/auth/desktop.ts index d45672a3..b25ead91 100644 --- a/packages/api/src/auth/desktop.ts +++ b/packages/api/src/auth/desktop.ts @@ -3,13 +3,15 @@ import {createHash, randomBytes} from 'node:crypto'; import type {Request, Response} from 'express'; import type {Result} from 'ts-results-es'; -import {createDesktopAuthCode, getDesktopAuthCode, takeDesktopAuthCode} from '@proxy/db/desktop-auth'; -import {desktopCodeBodySchema, desktopSessionBodySchema, type DesktopCode} from '@proxy/contracts/auth'; -import {type ApiError, Do, parseSchema} from '@proxy/utils'; +import {createDesktopAuthCode, createDesktopHandoff, getDesktopAuthCode, takeDesktopAuthCode, takeDesktopHandoff} from '@proxy/db/desktop-auth'; +import {desktopCodeBodySchema, desktopHandoffBodySchema, desktopSessionBodySchema, type DesktopCode, type DesktopHandoff} from '@proxy/contracts/auth'; +import {ApiErr, type ApiError, Do, parseSchema, requirePresent} from '@proxy/utils'; import {log, serializeError} from '../observability/log'; import type {AuthenticatedRequest} from '../server/middleware/require-auth'; -import {establishSession, hashToken, timingSafeEqualString} from './session'; +import {authenticateApiRequest} from '../utils/auth'; +import {env} from '../utils/env'; +import {establishSession, hashToken, queryParam, redirectToLoginError, sanitizeCallbackUrl, timingSafeEqualString} from './session'; /** * Signing the desktop app in. The app can't sign in itself: Google turns embedded browsers away, @@ -19,6 +21,12 @@ import {establishSession, hashToken, timingSafeEqualString} from './session'; * to the app through its `personalagentproxy://` link; the app trades the code and the verifier * for a session cookie of its own (`handleDesktopSessionRoute`), and is signed in as the browser * is, with the same kind of session, for as long. + * + * The other way round, for what has to happen in the browser once the app is signed in, such as + * connecting a service whose sign-in turns the app's window away as Google's does: the app asks + * for a handoff (`handleDesktopHandoffRoute`), an address with a one-time code that signs the + * browser in as the same person and goes on to where the app was headed + * (`handleDesktopHandoffOpenRoute`), and opens it in the browser. */ // Long enough to click through the browser's "open the app?" prompt, no longer. @@ -106,3 +114,76 @@ export async function handleDesktopSessionRoute(req: Request, res: Response): Pr log.info('Desktop app signed in', {userId: authCode.userId}); res.json({ok: true}); } + +// Long enough for the browser to open, no longer. +const handoffMaxAgeMs = 2 * 60 * 1000; + +/** + * `POST /auth/desktop/handoff` `{next}` — the app's window, signed in: the address to open in the + * browser, for `next`, a path in the app, as the sign-in's callback paths are. + */ +export function handleDesktopHandoffRoute(request: AuthenticatedRequest): Promise> { + return Do(async ($) => { + const appUrl = $(requirePresent(env.APP_URL, ApiErr.internalError(new Error('APP_URL is not configured')))); + const body = $(parseSchema(desktopHandoffBodySchema, request.body)); + const next = $(requirePresent(sanitizeCallbackUrl(body.next), ApiErr.validationError('next must be a path in the app'))); + const code = randomBytes(32).toString('hex'); + $( + await createDesktopHandoff({ + codeHash: hashToken(code), + userId: request.user.userId, + next, + expires: new Date(Date.now() + handoffMaxAgeMs), + }), + ); + return {url: `${appUrl}/auth/desktop/handoff?code=${code}`}; + }); +} + +/** + * `GET /auth/desktop/handoff?code=` — the browser, opened by the app: signed in as the app's + * person, unless it is already, and sent on to `next`. A code that is unknown, expired or taken + * lands on the login page saying the link is no good, as a spent magic link does. + */ +export async function handleDesktopHandoffOpenRoute(req: Request, res: Response): Promise { + const appUrl = env.APP_URL; + if (!appUrl) { + res.status(500).json({error: 'app_url_not_configured'}); + return; + } + + const code = queryParam(req, 'code'); + if (!code) { + redirectToLoginError(res, appUrl, 'Verification'); + return; + } + + const handoffResult = await takeDesktopHandoff(hashToken(code)); + if (handoffResult.isErr()) { + log.error('Desktop handoff failed to take the code', serializeError(handoffResult.error)); + redirectToLoginError(res, appUrl, 'Callback'); + return; + } + + const handoff = handoffResult.value; + if (!handoff || handoff.expires.valueOf() < Date.now()) { + redirectToLoginError(res, appUrl, 'Verification'); + return; + } + + // The browser is usually signed in already, since the app was signed in through it: that + // session stays. Signed in as someone else, or not at all, it is signed in as the app's person. + const existing = await authenticateApiRequest(req.headers); + const signedIn = existing.isOk() && existing.value.userId === handoff.userId; + if (!signedIn) { + const sessionResult = await establishSession(res, handoff.userId); + if (sessionResult.isErr()) { + log.error('Desktop handoff failed to create the session', serializeError(sessionResult.error)); + redirectToLoginError(res, appUrl, 'Callback'); + return; + } + } + + log.info('Desktop app handed over to the browser', {userId: handoff.userId, signedIn}); + res.redirect(`${appUrl}${handoff.next}`); +} diff --git a/packages/api/src/connections/connect-flow.ts b/packages/api/src/connections/connect-flow.ts index 0ed4a019..8ded2dd8 100644 --- a/packages/api/src/connections/connect-flow.ts +++ b/packages/api/src/connections/connect-flow.ts @@ -45,8 +45,11 @@ import {ownership} from './ownership'; * `?error=` (with `?error=-unconfigured` when the server lacks what the * sign-in needs, such as Google's client); a sign-in begun from the welcome flow * (`returnTo: 'welcome'`) ends on its page for the connection, or its catalog, instead, and one - * begun from a home page widget's card (`returnTo: 'home'`) back on the home page. What the - * provider's sign-in page is and how a code becomes tokens and a connection is the integration's. + * begun from a home page widget's card (`returnTo: 'home'`) back on the home page. One begun from + * the desktop app runs in the browser, which the app hands over to (`auth/desktop.ts`) with + * `?desktop=1` on the start, and ends on `/desktop-return`, which opens that page in the app. What + * the provider's sign-in page is and how a code becomes tokens and a connection is the + * integration's. */ const stateCookieMaxAgeMs = 15 * 60 * 1000; @@ -205,17 +208,33 @@ export function connectWithForm(viewer: ManagingViewer, integrationId: Integr }); } -// Where the sign-in ends: the organization's catalog, or the welcome flow's connections step it -// started from; the app's root, which opens the person's first organization, when the sign-in -// can't be read at all. -function catalogUrl(appUrl: string, slug: string | null, returnTo: ReturnTo | undefined): string { +// Where the sign-in ends when not on the connection's page: the organization's catalog, or the +// welcome flow's connections step it started from; the app's root, which opens the person's first +// organization, when the sign-in can't be read at all. +function catalogPath(slug: string | null, returnTo: ReturnTo | undefined): string { if (slug === null) { - return `${appUrl}/`; + return '/'; } if (returnTo === 'welcome') { - return `${appUrl}/${slug}/welcome/connections`; + return `/${slug}/welcome/connections`; } - return `${appUrl}/${slug}/connections/new`; + return `/${slug}/connections/new`; +} + +/** + * Where a sign-in comes back to: the organization it began in and, for one begun from the desktop + * app, that it did, which the handoff to the browser marks with `?desktop=1` on the start. + */ +type Back = {slug: string | null; returnTo: ReturnTo | undefined; desktop: boolean}; + +// The browser goes on to `path` in the app, or, for a sign-in begun from the desktop app, to the +// page that hands the browser's work back to the app, which opens that path. +function redirectBack(res: Response, appUrl: string, back: Back, path: string): void { + if (back.desktop) { + res.redirect(`${appUrl}/desktop-return?path=${encodeURIComponent(path)}`); + return; + } + res.redirect(`${appUrl}${path}`); } type SignInArgs = { @@ -248,12 +267,13 @@ export function signInFlow(args: SignInArgs) { orgId: z.string(), slug: z.string(), returnTo: returnToSchema.optional(), + desktop: z.boolean().optional(), shared: z.boolean().optional(), connectionId: z.string().optional(), kept: args.keptSchema, }); - type StatePayload = {state: string; verifier: string; userId: string; orgId: string; slug: string; returnTo?: ReturnTo; shared?: boolean; connectionId?: string; kept: E}; + type StatePayload = {state: string; verifier: string; userId: string; orgId: string; slug: string; returnTo?: ReturnTo; desktop?: boolean; shared?: boolean; connectionId?: string; kept: E}; function redirectUri(appUrl: string): string { return `${appUrl}${basePath}/callback`; @@ -271,19 +291,20 @@ export function signInFlow(args: SignInArgs) { }); } - function redirectToConnectError(res: Response, appUrl: string, slug: string | null, returnTo: ReturnTo | undefined, error: ApiError): void { + function redirectToConnectError(res: Response, appUrl: string, back: Back, error: ApiError): void { log.warn(`Connecting ${name} failed`, {kind: error.kind, ...('message' in error ? {message: error.message} : {}), ...('cause' in error ? serializeError(error.cause) : {})}); - res.redirect(`${catalogUrl(appUrl, slug, returnTo)}?error=${integrationId}`); + redirectBack(res, appUrl, back, `${catalogPath(back.slug, back.returnTo)}?error=${integrationId}`); } /** * Keeps the handshake, and what `start` keeps beside it, in a cookie on `res`, and answers the * provider's sign-in page, for the browser to go to: for a connection of the viewer's own, or - * `shared` with every member, or renewing the one it was begun from (`connectionId`). + * `shared` with every member, or renewing the one it was begun from (`connectionId`); begun + * from the desktop app (`desktop`), it ends on the page that hands over to the app. */ function beginSignIn( res: Response, - begin: {viewer: OrgViewer; returnTo: ReturnTo | undefined; account: string | null; shared?: boolean; connectionId?: string | null; start: SignInStart}, + begin: {viewer: OrgViewer; returnTo: ReturnTo | undefined; desktop?: boolean; account: string | null; shared?: boolean; connectionId?: string | null; start: SignInStart}, ): Promise> { return Do(async ($) => { const appUrl = $(requirePresent(env.APP_URL, ApiErr.internalError(new Error('APP_URL is not configured')))); @@ -297,6 +318,7 @@ export function signInFlow(args: SignInArgs) { orgId: begin.viewer.orgId, slug: begin.viewer.slug, ...(begin.returnTo ? {returnTo: begin.returnTo} : {}), + ...(begin.desktop ? {desktop: true} : {}), ...(begin.shared ? {shared: true} : {}), ...(begin.connectionId ? {connectionId: begin.connectionId} : {}), kept, @@ -345,23 +367,26 @@ export function signInFlow(args: SignInArgs) { // The handshake is single-use: cleared however the callback ends. res.clearCookie(stateCookieName, stateCookieOptions()); const payload = readStateCookie(request); - const returnTo = payload.isOk() ? payload.value.returnTo : undefined; - const slug = payload.isOk() ? payload.value.slug : null; + const back: Back = payload.isOk() ? {slug: payload.value.slug, returnTo: payload.value.returnTo, desktop: payload.value.desktop === true} : {slug: null, returnTo: undefined, desktop: false}; const finished = payload.isErr() ? Err(payload.error) : await finishConnect(request, appUrl, payload.value); if (finished.isErr()) { - redirectToConnectError(res, appUrl, slug, returnTo, finished.error); + redirectToConnectError(res, appUrl, back, finished.error); return; } const {id, added} = finished.value; - if (returnTo === 'welcome') { - res.redirect(`${appUrl}/${slug}/welcome/connections/${id}`); - return; + redirectBack(res, appUrl, back, landingPath(back, id, added)); + } + + // Where a finished sign-in lands: the welcome flow's page for the connection, the home page, or + // the connection's page, saying it was added unless it was only signed in to again. + function landingPath(back: Back, id: string, added: boolean): string { + if (back.returnTo === 'welcome') { + return `/${back.slug}/welcome/connections/${id}`; } - if (returnTo === 'home') { - res.redirect(`${appUrl}/${slug}`); - return; + if (back.returnTo === 'home') { + return `/${back.slug}`; } - res.redirect(`${appUrl}/${slug}/connections/${id}${added ? '?added' : ''}`); + return `/${back.slug}/connections/${id}${added ? '?added' : ''}`; } return {beginSignIn, handleCallback, redirectToConnectError}; @@ -386,10 +411,13 @@ export function signInRoutes( return; } - const returnTo = returnToSchema.safeParse(queryParam(request, 'returnTo')).data; - const slug = request.params.org ?? null; + const back: Back = { + slug: request.params.org ?? null, + returnTo: returnToSchema.safeParse(queryParam(request, 'returnTo')).data, + desktop: queryParam(request, 'desktop') === '1', + }; if (args.configured && !args.configured()) { - res.redirect(`${catalogUrl(appUrl, slug, returnTo)}?error=${args.integrationId}-unconfigured`); + redirectBack(res, appUrl, back, `${catalogPath(back.slug, back.returnTo)}?error=${args.integrationId}-unconfigured`); return; } @@ -401,10 +429,20 @@ export function signInRoutes( if (connectionId !== null) { $(await requireManaged(viewer, connectionId)); } - return $(await flow.beginSignIn(res, {viewer, returnTo, account: queryParam(request, 'account'), shared: queryParam(request, 'share') === 'everyone', connectionId, start: args.start})); + return $( + await flow.beginSignIn(res, { + viewer, + returnTo: back.returnTo, + desktop: back.desktop, + account: queryParam(request, 'account'), + shared: queryParam(request, 'share') === 'everyone', + connectionId, + start: args.start, + }), + ); }); if (begun.isErr()) { - flow.redirectToConnectError(res, appUrl, slug, returnTo, begun.error); + flow.redirectToConnectError(res, appUrl, back, begun.error); return; } res.redirect(begun.value); diff --git a/packages/api/src/connections/google/route.test.ts b/packages/api/src/connections/google/route.test.ts index 02f7ac2a..6351e5eb 100644 --- a/packages/api/src/connections/google/route.test.ts +++ b/packages/api/src/connections/google/route.test.ts @@ -260,6 +260,20 @@ describe('handleGoogleCallbackRoute', () => { expect(res.redirect).toHaveBeenCalledWith('https://app.example.com/org-1/welcome/connections?error=google'); }); + test('begun from the desktop app, ends on the page that hands over to it, wherever it ends', async () => { + const {cookie, state} = await start({desktop: '1'}); + const res = await callback({cookie: cookie?.value ?? '', query: {state, code: 'code-1'}}); + expect(res.redirect).toHaveBeenCalledWith('https://app.example.com/desktop-return?path=%2Forg-1%2Fconnections%2Fconn-1%3Fadded'); + + const {cookie: welcome, state: welcomeState} = await start({desktop: '1', returnTo: 'welcome'}); + const welcomeRes = await callback({cookie: welcome?.value ?? '', query: {state: welcomeState, code: 'code-1'}}); + expect(welcomeRes.redirect).toHaveBeenCalledWith('https://app.example.com/desktop-return?path=%2Forg-1%2Fwelcome%2Fconnections%2Fconn-1'); + + const {cookie: refused, state: refusedState} = await start({desktop: '1'}); + const refusedRes = await callback({cookie: refused?.value ?? '', query: {state: refusedState, error: 'access_denied'}}); + expect(refusedRes.redirect).toHaveBeenCalledWith('https://app.example.com/desktop-return?path=%2Forg-1%2Fconnections%2Fnew%3Ferror%3Dgoogle'); + }); + test('a sign-in turned down at Google goes back to the catalog', async () => { const {cookie, state} = await start(); const res = await callback({cookie: cookie?.value ?? '', query: {state, error: 'access_denied'}}); diff --git a/packages/api/src/server/create-api-router.ts b/packages/api/src/server/create-api-router.ts index 1d4620a7..6aebd06e 100644 --- a/packages/api/src/server/create-api-router.ts +++ b/packages/api/src/server/create-api-router.ts @@ -25,7 +25,7 @@ import { handleSetAgentRevokedRoute, handleShareAgentRoute, } from '../agents/route'; -import {handleDesktopCodeRoute, handleDesktopSessionRoute} from '../auth/desktop'; +import {handleDesktopCodeRoute, handleDesktopHandoffOpenRoute, handleDesktopHandoffRoute, handleDesktopSessionRoute} from '../auth/desktop'; import {handleDevLoginRoute} from '../auth/dev-login'; import {handleEmailSignInRoute, handleEmailVerifyRoute} from '../auth/email'; import {handleGoogleCallbackRoute, handleGoogleStartRoute, handleSignInMethodsRoute} from '../auth/google'; @@ -104,6 +104,10 @@ export function createApiRouter(): express.Router { // session and sends no Origin, so the code and its verifier are the guard there. router.post('/auth/desktop/code', requireBrowserOrigin, withAuthResult('Desktop code route', handleDesktopCodeRoute)); router.post('/auth/desktop/session', wrapAsyncRoute('Desktop session route', handleDesktopSessionRoute)); + // And the browser signed in as the app's person: asked for by the app's window, a write from + // the web app; opened as a top-level navigation. + router.post('/auth/desktop/handoff', requireBrowserOrigin, withAuthResult('Desktop handoff route', handleDesktopHandoffRoute)); + router.get('/auth/desktop/handoff', wrapAsyncRoute('Desktop handoff open route', handleDesktopHandoffOpenRoute)); router.use('/api/me', requireBrowserOrigin); router.get('/api/me', withAuthResult('Me route', handleMeRoute)); diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 894aa47e..a96d9087 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -47,3 +47,15 @@ export const desktopSessionBodySchema = z.object({ code: z.string().min(1).max(256), verifier: z.string().min(1).max(256), }); + +/** + * The other way round: `POST /auth/desktop/handoff` `{next}`, from the app's window, signed in, + * for what has to happen in the browser, such as connecting a service whose sign-in turns the + * app's window away. The answer is the address the app opens in the browser, which signs the + * browser in as the same person, once, and goes on to `next`, a path in the app. + */ +export const desktopHandoffBodySchema = z.object({next: z.string().max(2000)}); + +export const desktopHandoffSchema = z.object({url: z.string()}); + +export type DesktopHandoff = z.infer; diff --git a/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql b/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql new file mode 100644 index 00000000..b5bc82c6 --- /dev/null +++ b/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql @@ -0,0 +1,20 @@ +-- One-time codes that sign the browser in as the desktop app's person, for what has to happen +-- there, such as connecting a service. Kept only as their hash. +-- CreateTable +CREATE TABLE "DesktopHandoff" ( + "id" TEXT NOT NULL, + "codeHash" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "next" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DesktopHandoff_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DesktopHandoff_codeHash_key" ON "DesktopHandoff"("codeHash"); + +-- CreateIndex +CREATE INDEX "DesktopHandoff_userId_idx" ON "DesktopHandoff"("userId"); + diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 63ec14f3..ee4c5edc 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -43,6 +43,7 @@ model User { threads AgentThread[] writeTargets WriteTarget[] desktopCodes DesktopAuthCode[] + handoffs DesktopHandoff[] } // A sign-in provider identity linked to a user, such as their Google account. Only who it is: @@ -97,6 +98,24 @@ model DesktopAuthCode { @@index([userId]) } +// The other way round: a one-time code that signs the browser in as the desktop app's person, for +// what has to happen there, such as connecting a service whose sign-in turns the app's window +// away (`GET /auth/desktop/handoff`). `next` is where the browser goes on to, a path in the app. +// Minutes long, single-use, and kept only as its hash. +model DesktopHandoff { + id String @id @default(cuid()) + codeHash String @unique + userId String + next String + expires DateTime + + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + // A magic link waiting to be clicked. `token` is a hash, never the token from the email. model VerificationToken { identifier String diff --git a/packages/db/src/desktop-auth.ts b/packages/db/src/desktop-auth.ts index 391831c6..fe370cce 100644 --- a/packages/db/src/desktop-auth.ts +++ b/packages/db/src/desktop-auth.ts @@ -4,9 +4,9 @@ import {type ApiError, wrapDb} from '@proxy/utils'; import {db} from '.'; -// The one-time codes that sign the desktop app in. Minted in the browser, signed in, for the PKCE -// challenge the app made; traded once by the app, with the verifier. Only the hash of a code is -// kept, so the table signs no one in. +// The one-time codes between the desktop app and the browser. Signing the app in: minted in the +// browser, signed in, for the PKCE challenge the app made; traded once by the app, with the +// verifier. Only the hash of a code is kept, so the table signs no one in. export type DesktopAuthCodeRow = { userId: string; @@ -53,3 +53,50 @@ export async function takeDesktopAuthCode(codeHash: string): Promise 0); } + +// The other way round: a code that signs the browser in as the app's person, for what has to +// happen there. Kept once, taken once. + +export type DesktopHandoffRow = { + userId: string; + // Where the browser goes on to: a path in the app. + next: string; + expires: Date; +}; + +/** Keeps a handoff; the expired ones go with it. */ +export async function createDesktopHandoff( + data: DesktopHandoffRow & {codeHash: string}, +): Promise> { + return ( + await wrapDb(() => + db.$transaction([ + db.desktopHandoff.deleteMany({where: {expires: {lt: new Date()}}}), + db.desktopHandoff.create({data}), + ]), + ) + ).map(() => undefined); +} + +/** + * Takes a handoff, once: `Ok(null)` when it is unknown or taken already, by this request or + * another at the same moment. An expired one is returned for the caller to refuse. + */ +export async function takeDesktopHandoff( + codeHash: string, +): Promise> { + return wrapDb(async () => { + const handoff = await db.desktopHandoff.findUnique({ + where: {codeHash}, + select: {id: true, userId: true, next: true, expires: true}, + }); + if (!handoff) { + return null; + } + const {count} = await db.desktopHandoff.deleteMany({where: {id: handoff.id}}); + if (count === 0) { + return null; + } + return {userId: handoff.userId, next: handoff.next, expires: handoff.expires}; + }); +} diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 5bfc1516..87c1b31c 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -17,6 +17,13 @@ everything but the app's own origin out of the window: links out of the app open `personalagentproxy://auth-callback?code=…`. The app trades code and verifier at `POST /auth/desktop/session` for a session cookie, which its window keeps, and is signed in as a browser is. +- Connecting a service whose sign-in turns the app's window away, as Google's does, happens in + the browser too: the web app, which knows the window by its user agent, asks the api for a + handoff (`POST /auth/desktop/handoff`), an address with a one-time code that signs the browser + in as the same person and goes on to the service's sign-in, and opens it in the browser. That + sign-in ends on `/desktop-return`, which hands back through `personalagentproxy://open?path=…`, + and the window opens the page the sign-in would have ended on. A custom MCP server that asks for + a sign-in moves its form to the browser, filled in as it was. - The main process is bundled by `build.ts` into `dist/main.cjs`, the splash pages as text in it. ## Development diff --git a/packages/desktop/src/deep-link.test.ts b/packages/desktop/src/deep-link.test.ts index dc3a8b45..0a53e9e0 100644 --- a/packages/desktop/src/deep-link.test.ts +++ b/packages/desktop/src/deep-link.test.ts @@ -22,6 +22,22 @@ describe('parseDeepLink', () => { expect(parseDeepLink('personalagentproxy-evil://auth-callback?code=abc123')).toBeNull(); }); + test('reads the page to open, a path in the app', () => { + expect( + parseDeepLink('personalagentproxy://open?path=%2Facme%2Fconnections%2Fc-1%3Fadded'), + ).toEqual({ + kind: 'open', + path: '/acme/connections/c-1?added', + }); + }); + + test('opens no other site', () => { + expect(parseDeepLink('personalagentproxy://open?path=https%3A%2F%2Fevil.example')).toBeNull(); + expect(parseDeepLink('personalagentproxy://open?path=%2F%2Fevil.example')).toBeNull(); + expect(parseDeepLink('personalagentproxy://open?path=%2F%5Cevil.example')).toBeNull(); + expect(parseDeepLink('personalagentproxy://open')).toBeNull(); + }); + test('knows no other target', () => { expect(parseDeepLink('personalagentproxy://open-project?id=123')).toBeNull(); }); diff --git a/packages/desktop/src/deep-link.ts b/packages/desktop/src/deep-link.ts index 3349d3df..b3c1ac3c 100644 --- a/packages/desktop/src/deep-link.ts +++ b/packages/desktop/src/deep-link.ts @@ -1,10 +1,18 @@ // The app's own links, `personalagentproxy://…`, which the browser hands things back through: -// the one-time code of a sign-in (`auth-callback?code=`). macOS delivers them as the host form, -// `personalagentproxy://auth-callback?code=…`; the path form, with a third slash, is accepted too. +// the one-time code of a sign-in (`auth-callback?code=`), and the page of the app to open once +// the browser has done what the app handed it (`open?path=`). macOS delivers them as the host +// form, `personalagentproxy://auth-callback?code=…`; the path form, with a third slash, is +// accepted too. export const DEEP_LINK_SCHEME = 'personalagentproxy'; -export type DeepLink = {kind: 'auth-callback'; code: string}; +export type DeepLink = {kind: 'auth-callback'; code: string} | {kind: 'open'; path: string}; + +// A page of the app: a path, never another site (`//evil.example`, or the backslash browsers +// take for a slash). +function isAppPath(path: string): boolean { + return path.startsWith('/') && !path.startsWith('//') && !path.startsWith('/\\'); +} /** The link as what it asks for, or null for anything that isn't one of the app's. */ export function parseDeepLink(raw: string): DeepLink | null { @@ -18,16 +26,23 @@ export function parseDeepLink(raw: string): DeepLink | null { } const target = url.hostname || url.pathname.replace(/^\/+/, ''); - if (target !== 'auth-callback') { - return null; + if (target === 'auth-callback') { + const code = url.searchParams.get('code'); + if (!code) { + return null; + } + return {kind: 'auth-callback', code}; } - const code = url.searchParams.get('code'); - if (!code) { - return null; + if (target === 'open') { + const path = url.searchParams.get('path'); + if (!path || !isAppPath(path)) { + return null; + } + return {kind: 'open', path}; } - return {kind: 'auth-callback', code}; + return null; } /** The app's link among a command line's arguments, as Windows and Linux hand it to a second instance. */ diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 03a6ca0e..4b4ba972 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -8,7 +8,8 @@ import {DESKTOP_VERSION} from './env'; import {createMainWindow, loadApp, showSignIn} from './window'; // The desktop app: a window around the web app, hosted, with nothing of it bundled. The main -// process keeps the window, signs in through the browser, and takes the app's links. +// process keeps the window, signs in through the browser, and takes the app's links: the browser +// coming back from the sign-in, and from what the app hands it, as a service's sign-in is. // So the web app can tell it is in the desktop app, and which version. app.userAgentFallback = `${app.userAgentFallback} PersonalAgentProxyDesktop/${DESKTOP_VERSION}`; @@ -37,6 +38,13 @@ async function onDeepLink(link: DeepLink): Promise { await app.whenReady(); const win = focusWindow(); + // The browser has done what the app handed it, such as connecting a service: on to where that + // ended. + if (link.kind === 'open') { + loadApp(win, link.path); + return; + } + const signedIn = await finishSignIn(link.code); if (signedIn.isErr()) { showSignIn(win); diff --git a/packages/web/src/client/auth-client.ts b/packages/web/src/client/auth-client.ts index c6c606e8..b2c1c1d4 100644 --- a/packages/web/src/client/auth-client.ts +++ b/packages/web/src/client/auth-client.ts @@ -1,5 +1,6 @@ import { desktopCodeSchema, + desktopHandoffSchema, magicLinkResponseSchema, signInMethodsSchema, } from '@proxy/contracts/auth'; @@ -64,3 +65,12 @@ export function devLogin() { export function createDesktopCode(challenge: string) { return apiRequest('POST', '/auth/desktop/code', desktopCodeSchema, {challenge}); } + +/** + * The other way round (`POST /auth/desktop/handoff`): the address the desktop app opens in the + * browser, which signs the browser in as the person signed in here, once, and goes on to `next`, + * a path in the app. + */ +export function createDesktopHandoff(next: string) { + return apiRequest('POST', '/auth/desktop/handoff', desktopHandoffSchema, {next}); +} diff --git a/packages/web/src/components/connect-mcp-form.tsx b/packages/web/src/components/connect-mcp-form.tsx index 4c69e1ab..30bd24d1 100644 --- a/packages/web/src/components/connect-mcp-form.tsx +++ b/packages/web/src/components/connect-mcp-form.tsx @@ -1,11 +1,13 @@ import type {Connection} from '@proxy/contracts/connections'; import type {FetchError} from '@proxy/utils'; import {useState, type ReactNode} from 'react'; +import {useSearchParams} from 'react-router'; import {connectMcp} from '@/client/connections-client'; import {Button} from '@proxy/ui/components/button'; import {Input} from '@proxy/ui/components/input'; import {Label} from '@proxy/ui/components/label'; import {apiMessage} from '@/lib/agent-errors'; +import {handOffToBrowser, isDesktopApp} from '@/lib/desktop'; import {NO_PASSWORD_MANAGER} from '@/lib/password-managers'; import {useCommandEnterSubmit} from '@/hooks/use-command-enter'; import {useOrgSlug} from '@/hooks/use-org'; @@ -28,7 +30,9 @@ function connectErrorMessage(error: FetchError): string { // sign-in. Connecting asks the server for its tools: a server that answers is // connected at once, and for one that asks for a sign-in the api begins it and answers the // server's sign-in page, which the browser goes to; it comes back to the new connection, or to the -// welcome flow's page for it. `cancel` sits beside the button. +// welcome flow's page for it. In the desktop app, whose window a sign-in page would turn away, the +// form moves to the browser instead, filled in as it was (`?name=&url=`), to be sent again there. +// `cancel` sits beside the button. export function ConnectMcpForm({ cancel, returnTo, @@ -42,11 +46,12 @@ export function ConnectMcpForm({ onConnected: (connection: Connection) => void; }) { const org = useOrgSlug(); - const [name, setName] = useState(''); - const [url, setUrl] = useState(''); + const [params] = useSearchParams(); + const [name, setName] = useState(params.get('name') ?? ''); + const [url, setUrl] = useState(params.get('url') ?? ''); const [token, setToken] = useState(''); const [error, setError] = useState(null); - const [pending, setPending] = useState<'connecting' | 'signingIn' | null>(null); + const [pending, setPending] = useState<'connecting' | 'signingIn' | 'browser' | null>(null); const ready = name.trim() !== '' && url.trim() !== ''; const form = useCommandEnterSubmit(ready && pending === null); @@ -71,6 +76,18 @@ export function ConnectMcpForm({ return; } if (result.value.kind === 'signIn') { + if (isDesktopApp()) { + setPending('browser'); + const here = new URL(window.location.href); + here.searchParams.set('name', name.trim()); + here.searchParams.set('url', url.trim()); + const handed = await handOffToBrowser(`${here.pathname}${here.search}`); + if (handed.isErr()) { + setPending(null); + setError('Could not open your browser. Try again.'); + } + return; + } // Only ever a web address: the api has checked it, and the browser goes nowhere else. if (!/^https?:\/\//.test(result.value.url)) { setPending(null); @@ -134,9 +151,11 @@ export function ConnectMcpForm({ diff --git a/packages/web/src/components/connection-access.tsx b/packages/web/src/components/connection-access.tsx index a8b86588..3dafb50c 100644 --- a/packages/web/src/components/connection-access.tsx +++ b/packages/web/src/components/connection-access.tsx @@ -6,6 +6,7 @@ import {Badge} from '@proxy/ui/components/badge'; import {Button} from '@proxy/ui/components/button'; import {Checkbox} from '@proxy/ui/components/checkbox'; import {cn} from '@proxy/ui/lib/utils'; +import {onSignInClick} from '@/lib/desktop'; type Props = { actions: Action[]; @@ -364,7 +365,7 @@ export function SignInAgain({ return (

This connection isn't signed in yet, so agents can't use it.{' '} - + Sign in {useKey && <>, or {useKey},} to start. diff --git a/packages/web/src/components/icon-button.tsx b/packages/web/src/components/icon-button.tsx index 642655e0..0812592c 100644 --- a/packages/web/src/components/icon-button.tsx +++ b/packages/web/src/components/icon-button.tsx @@ -1,4 +1,4 @@ -import type {ReactNode} from 'react'; +import type {MouseEvent, ReactNode} from 'react'; import {Link} from 'react-router'; import {Button} from '@proxy/ui/components/button'; import {Tooltip, TooltipContent, TooltipTrigger} from '@proxy/ui/components/tooltip'; @@ -11,8 +11,9 @@ type Props = { } & ( | {onClick: () => void; to?: never; href?: never} | {to: string; onClick?: never; href?: never} - // A full navigation, such as to an api route that sends the browser on to a sign-in. - | {href: string; onClick?: never; to?: never} + // A full navigation, such as to an api route that sends the browser on to a sign-in, with what + // the desktop app does with it instead (`onSignInClick`). + | {href: string; onClick?: (event: MouseEvent) => void; to?: never} ); // A header's icon action: a ghost icon button with its label in a tooltip. @@ -41,7 +42,7 @@ export function IconButton({ size={buttonSize} aria-label={label} nativeButton={false} - render={} + render={} /> ) : ( + + + + ); +} diff --git a/packages/web/src/router.tsx b/packages/web/src/router.tsx index 016c19bb..14baecfb 100644 --- a/packages/web/src/router.tsx +++ b/packages/web/src/router.tsx @@ -63,6 +63,7 @@ import {ConnectAgentPage} from '@/pages/connect-agent-page'; import {ConnectionPage} from '@/pages/connection-page'; import {ConnectionsPage} from '@/pages/connections-page'; import {DesktopAuthPage} from '@/pages/desktop-auth-page'; +import {DesktopReturnPage} from '@/pages/desktop-return-page'; import {HomePage} from '@/pages/home-page'; import {InvitePage} from '@/pages/invite-page'; import {LoginPage} from '@/pages/login-page'; @@ -183,6 +184,7 @@ export const router = createBrowserRouter([ {path: '/', loader: rootLoader, element: null, errorElement: }, {path: '/login', loader: loginLoader, element: }, {path: '/desktop-auth', loader: desktopAuthLoader, element: }, + {path: '/desktop-return', element: }, {path: '/agent/login', element: }, // An MCP client, such as Claude, signing in as one of the person's agents. { From ac815a2368b6c987c8bc9d683ed86f19e16f6fd3 Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:33:59 +0000 Subject: [PATCH 4/8] package and release the desktop app electron-builder.yml builds the macOS app for Apple silicon and Intel, a disk image and a zip each, with latest-mac.yml, the feed electron-updater checks the repository's latest GitHub release for (src/updates.ts). The desktop workflow builds on a desktop-v tag, checks it names the version in package.json, and publishes the files as the release; a pull request touching the app only builds it. Signing and notarization start once Apple's certificate and credentials are secrets; until then the build is unsigned, which macOS warns about and the updater can't update. A first icon from the brand mark. --- .github/workflows/desktop.yml | 66 +++ bun.lock | 538 +++++++++++++++++- packages/desktop/README.md | 18 + packages/desktop/build/entitlements.mac.plist | 9 + packages/desktop/build/icon.png | Bin 0 -> 61733 bytes packages/desktop/electron-builder.yml | 40 ++ packages/desktop/package.json | 11 +- packages/desktop/src/main.ts | 2 + packages/desktop/src/updates.ts | 39 ++ 9 files changed, 713 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/desktop.yml create mode 100644 packages/desktop/build/entitlements.mac.plist create mode 100644 packages/desktop/build/icon.png create mode 100644 packages/desktop/electron-builder.yml create mode 100644 packages/desktop/src/updates.ts diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml new file mode 100644 index 00000000..ac70c7a7 --- /dev/null +++ b/.github/workflows/desktop.yml @@ -0,0 +1,66 @@ +name: desktop + +# The macOS desktop app (packages/desktop), on a `desktop-v1.2.3` tag: built for Apple silicon and +# Intel and published as the release for the tag, with latest-mac.yml, the feed the app checks the +# repository's latest release for. Pull requests touching the app only build it. +# +# Signed and notarized once the secrets are there: CSC_LINK (the Developer ID certificate as a +# base64 .p12) and CSC_KEY_PASSWORD, and APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and APPLE_TEAM_ID for +# notarization. Without them the build is unsigned, which macOS warns about on first open, and the +# app can't update itself. + +on: + push: + tags: ['desktop-v*'] + pull_request: + paths: + - packages/desktop/** + - .github/workflows/desktop.yml + +permissions: + contents: write + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - name: Check the tag names the version in package.json + if: startsWith(github.ref, 'refs/tags/desktop-v') + run: | + version=$(bun -e "console.log(require('./packages/desktop/package.json').version)") + if [[ "${GITHUB_REF_NAME}" != "desktop-v${version}" ]]; then + echo "::error::The tag is ${GITHUB_REF_NAME} but packages/desktop/package.json says ${version}." + exit 1 + fi + - run: bun run --cwd packages/desktop package + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + - if: startsWith(github.ref, 'refs/tags/desktop-v') + name: Publish the release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + --title "Personal Agent Proxy for macOS ${GITHUB_REF_NAME#desktop-v}" \ + --generate-notes \ + packages/desktop/release/*.dmg \ + packages/desktop/release/*.zip \ + packages/desktop/release/*.blockmap \ + packages/desktop/release/latest-mac.yml + - if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: desktop-mac + path: | + packages/desktop/release/*.dmg + packages/desktop/release/latest-mac.yml + if-no-files-found: error diff --git a/bun.lock b/bun.lock index c9e7a4ea..51affd0a 100644 --- a/bun.lock +++ b/bun.lock @@ -77,6 +77,8 @@ "@types/bun": "latest", "dotenv": "^16.6.1", "electron": "^41.0.0", + "electron-builder": "^26.18.0", + "electron-updater": "^6.8.11", "esbuild": "^0.27.0", "oxlint": "^1.79.0", "typescript": "~6.0.2", @@ -342,8 +344,22 @@ "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.8", "", {}, "sha512-8iajeLFbzNwM0m3AsQZSZgYREWglAImyGvvFSWU34FgMW8sHlzdwj9QKvv9dgN7jMP591EFUF/sLOC3Jl9YIjQ=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + "@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="], + "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], + + "@electron/rebuild": ["@electron/rebuild@4.2.1", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-IS4pG3BysRrCLJ8xTAfvg+IQDbiUoAzdJy2SfjcYKnLDy5DpCdjnPdhS5Zf5UMfrkB24xu0KC+TqcOxg+eG+6w=="], + + "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + + "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -464,6 +480,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -474,6 +492,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="], @@ -500,6 +522,8 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@nodable/entities": ["@nodable/entities@3.1.0", "", {}, "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -548,6 +572,14 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.11.1", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-jaU9OaCcbZUdyXx9hMETZB9xoNmxTGjOGFqd3cOwKFkpKnyKd9WxF3uqj4PvX4cyOMP9bSdHg5YyML2qtx/d/g=="], + + "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], + + "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], + + "@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ktTSACRJ5WdpfjHjFT2EWJQQPacGv1cPCDfEbdmpAHOxdFiYqPe4lfnXGA3D8Dg9/0kHPYSvyRlgujIOVlTdvQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@prisma/client": ["@prisma/client@6.19.3", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg=="], @@ -646,6 +678,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], @@ -654,6 +688,8 @@ "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], @@ -700,6 +736,8 @@ "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], @@ -726,10 +764,16 @@ "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="], + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + "@types/mailparser": ["@types/mailparser@3.9.0", "", { "dependencies": { "@types/node": "*", "iconv-lite": "^0.6.3" } }, "sha512-YnhrRiRVDiEAphFvd72hKa1n8y2b9Q3rgSQiONZQyhQz67h5g4MZHNApET3XWXVEFdInWtHqDHKEwnyjxU3QVw=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -756,6 +800,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -792,12 +838,16 @@ "@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.20", "", { "dependencies": { "libbase64": "1.3.2", "libmime": "5.4.7", "libqp": "2.1.2" } }, "sha512-6B2w0zRBWOwbO5h7SL5N/2z1AuNchTP3OT33MObUr4/cz9bWPrZ6KadLKxnSrOZG0AGu7Q03p4X7eQhZHc1u/A=="], + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -806,24 +856,38 @@ "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], - "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], + "app-builder-lib": ["app-builder-lib@26.18.0", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.2.1", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.18.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.18.0", "electron-builder-squirrel-windows": "26.18.0" } }, "sha512-lx+KkuCSZHcbECf4R3IAcC9VOFk0+cMUwljO63s5y8yqS1Lw0sRGmjPaWsPwP3zv3ohZYbMhUvjXeiQXXZhJXA=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asn1js": ["asn1js@3.1.0", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.2.0", "tslib": "^2.8.1" } }, "sha512-HWYi1THkOqguCiO+vwj09ZkQFD2kKEsejYHAYCtX0D80WVdoDnvRc9IukXX/jeb0T9NRWJnRtqwdbHltnFjwXA=="], + "ast-types": ["ast-types@0.16.3", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="], + "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], @@ -836,8 +900,12 @@ "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + "body-parser": ["body-parser@1.20.8", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -846,16 +914,28 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="], + + "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], + "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], @@ -866,7 +946,7 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -878,8 +958,14 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + "chrono-node": ["chrono-node@2.10.3", "", {}, "sha512-MOfrzXgSMZTH4SZ5gyRKQCVwiCrCKY+uYkSJiWoRLXS88DA6WGJm8UjgS+VVEuUQeew/UUXaxrO+SeDn/WCisg=="], + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -892,6 +978,10 @@ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="], @@ -900,10 +990,20 @@ "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="], "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], @@ -926,6 +1026,8 @@ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -958,6 +1060,8 @@ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -968,10 +1072,18 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -986,12 +1098,18 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dingbat-to-unicode": ["dingbat-to-unicode@1.0.2", "", {}, "sha512-R0bRerYzy/EZP6QzD3Hkl6JjUDA1Mnn+pd928w0nWjPzihnbAR6dcjGJGiRyznj5E4S1duEKzWrTqw02DPcdzg=="], + "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + + "dmg-builder": ["dmg-builder@26.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-ozCGC54wi2C1RkS2v69y9lQxvoQ1Coh+iT8CmWiyY8y1ZuOKVvuNson2Hd+HEOs9+EZNNsOBbzKgQZ4SNqWd0w=="], + "docs": ["docs@workspace:packages/docs"], "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -1008,20 +1126,36 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electron": ["electron@41.10.7", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js" } }, "sha512-AqIiefddlf5i+HYCGatr8VlBuEbWJpad4/yloBFYcMB2r57j7xhW1ogiJ+BfA5kxQyfmSs5yjwy54Me+wJn1jw=="], + "electron-builder": ["electron-builder@26.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.18.0", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-ASgIuQnOq4RuD8jMhS9hI0LS3EMRT1I/M5R6utqifB/BHN80IcCjBdt+DED+NmNPvmBgEtrlExXZitNdA7kXOg=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.18.0", "", { "dependencies": { "app-builder-lib": "26.18.0", "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, "sha512-a2elgTipboXNIsxqS1HOmHMGp4Pd3LJgIZOdIFTOskBSawvxKyZWYWQ1d7KKA4CtINSchYCYEKzhxRqiKZev9Q=="], + + "electron-publish": ["electron-publish@26.18.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-MagnqaZSALSWjcdKu5HEVhYUZD/2BDL3q7XzrqWwub6gf/UfD3ItMgmthVDv9w5y9E0gH1C3JWqKaXwf6f4ROg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.446", "", {}, "sha512-kWlGMPHzuitCKx0O+MbY1ZGvQmP14JtckrnGjD4JsusTxh2cX9wu4bb4rOYZqLSvENjFFrHzlqY8kXhseucR/w=="], + "electron-updater": ["electron-updater@6.8.11", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-12U/mbzaIKQS3Q543aQ7DjyAZMXeVJy1Wpu0qdq5ne4RXpIQDpWYm25zRjgXngAoo6swMrm4C4owGyiIipelsg=="], + + "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -1030,6 +1164,8 @@ "encoding-japanese": ["encoding-japanese@2.4.0", "", {}, "sha512-PE0KjlsDy9JSsoU4C2Q8NXb1kC4c+bZRac9ZvHXUE4q1Ht63gScKMdEy9jst9+Fi3M4eDkqzkEhBZvix4blP2A=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.26.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -1038,12 +1174,18 @@ "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -1080,6 +1222,8 @@ "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "express": ["express@4.22.3", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.13", "proxy-addr": "~2.0.7", "qs": "~6.16.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw=="], "express-rate-limit": ["express-rate-limit@8.7.1", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA=="], @@ -1114,6 +1258,8 @@ "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], @@ -1124,12 +1270,16 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], "fs-extra": ["fs-extra@11.4.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1138,6 +1288,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1154,12 +1306,20 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "glsl-noise": ["glsl-noise@0.0.0", "", {}, "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "grammex": ["grammex@3.1.13", "", {}, "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg=="], @@ -1168,8 +1328,14 @@ "happy-dom": ["happy-dom@20.14.5", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], @@ -1190,6 +1356,8 @@ "hono": ["hono@4.13.13", "", {}, "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "html-to-text": ["html-to-text@10.0.1", "", { "dependencies": { "@selderee/plugin-htmlparser2": "~0.12.0", "deepmerge-ts": "^8.0.1", "dom-serializer": "^2.0.0", "htmlparser2": "^10.1.0", "selderee": "~0.12.0" } }, "sha512-GiVhRI1BatGARSCmlXWNCjDT0cWrwBWoeduLoV0WSKAgaV/wa+hUWy5LiQLUs4UwiUrE52ZCMfBGiKD87TDPrg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1198,8 +1366,16 @@ "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-cache-semantics": ["http-cache-semantics@4.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1212,6 +1388,8 @@ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], @@ -1230,6 +1408,8 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], @@ -1260,30 +1440,44 @@ "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], "its-fine": ["its-fine@2.1.1", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-C5ZoVkeqSt0Aje0tzESO+iT/EQpNFNL39GkP7mCACtoZUFD6GcyerFCr6kp0ZtiII4W9HoYkY48Soy5xE/uHKw=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "jszip": ["jszip@3.10.2", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + "leac": ["leac@0.7.0", "", {}, "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw=="], "libbase64": ["libbase64@1.3.2", "", {}, "sha512-fFCoI0ceXhTu/0QiJ6Uf6BV9k7X/8QW2/D4BR1vw3BPrHcIjLbGLH+U7inQ4SeWrfNCOmpGE09Qt4XjSFfus0g=="], @@ -1322,6 +1516,12 @@ "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -1330,6 +1530,8 @@ "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@1.52.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-TUgPtl5ZI9WgxOcbu4ckaC5B3l1qxPQrCMgb6OfUB9owx/OvSJfcf93SMgUWAxRw5/1XDfh9uI5F5iNGXCcGDQ=="], @@ -1350,6 +1552,8 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw=="], @@ -1482,10 +1686,18 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], @@ -1494,12 +1706,24 @@ "next": ["next@16.4.2", "", { "dependencies": { "@next/env": "16.4.2", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.2", "@next/swc-darwin-x64": "16.4.2", "@next/swc-linux-arm64-gnu": "16.4.2", "@next/swc-linux-arm64-musl": "16.4.2", "@next/swc-linux-x64-gnu": "16.4.2", "@next/swc-linux-x64-musl": "16.4.2", "@next/swc-win32-arm64-msvc": "16.4.2", "@next/swc-win32-x64-msvc": "16.4.2", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w=="], + "node-abi": ["node-abi@4.40.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-GuvqvtrOk+AC27RidBxBsMzVNkwNl7PjHgc4NXbmdvSVLgIUjpaqcG1FjNRYnt2Yn9Ytq+Ln37XsBaglUcte/w=="], + + "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + "node-releases": ["node-releases@2.0.57", "", {}, "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw=="], "nodemailer": ["nodemailer@10.0.15", "", {}, "sha512-EUqp5PhtcsYXs9Fq/lS7s/8zlTrnBqmzZWzFFROrNikMiz+om/YRKMwqN907t+0A1KKyT8jd39CBUbFZmaZmcA=="], + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], + + "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "nypm": ["nypm@0.6.10", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.3.1" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw=="], @@ -1508,6 +1732,8 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], @@ -1532,7 +1758,9 @@ "oxlint": ["oxlint@1.87.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.87.0", "@oxlint/binding-android-arm64": "1.87.0", "@oxlint/binding-darwin-arm64": "1.87.0", "@oxlint/binding-darwin-x64": "1.87.0", "@oxlint/binding-freebsd-x64": "1.87.0", "@oxlint/binding-linux-arm-gnueabihf": "1.87.0", "@oxlint/binding-linux-arm-musleabihf": "1.87.0", "@oxlint/binding-linux-arm64-gnu": "1.87.0", "@oxlint/binding-linux-arm64-musl": "1.87.0", "@oxlint/binding-linux-ppc64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-gnu": "1.87.0", "@oxlint/binding-linux-riscv64-musl": "1.87.0", "@oxlint/binding-linux-s390x-gnu": "1.87.0", "@oxlint/binding-linux-x64-gnu": "1.87.0", "@oxlint/binding-linux-x64-musl": "1.87.0", "@oxlint/binding-openharmony-arm64": "1.87.0", "@oxlint/binding-win32-arm64-msvc": "1.87.0", "@oxlint/binding-win32-ia32-msvc": "1.87.0", "@oxlint/binding-win32-x64-msvc": "1.87.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg=="], - "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], @@ -1554,12 +1782,16 @@ "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + "peberminta": ["peberminta@0.10.0", "", {}, "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -1580,14 +1812,20 @@ "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "pkijs": ["pkijs@3.4.3", "", { "dependencies": { "@noble/hashes": "1.8.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-V9kOzaItwDgqlryhXysQsAXUEKi5O7kqWUaP3dXI7MLvkPqWWMb4wVb1QAtmEDCKI+XIAINsZoNKMFF7sZb1eg=="], + "playwright-core": ["playwright-core@1.64.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + "postal-mime": ["postal-mime@2.7.6", "", {}, "sha512-UUlyE2KlxmvwMGq060onF/VWU3zD6dVW31FwokQ5v26jWd35fbs2MoTFzh+jXnPwAyV2MULMKXcBInGOl5TO6Q=="], "postcss": ["postcss@8.5.29", "", { "dependencies": { "nanoid": "^3.3.19", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="], + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + "potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="], "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], @@ -1600,12 +1838,16 @@ "prisma": ["prisma@6.19.3", "", { "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg=="], + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -1618,16 +1860,24 @@ "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="], + "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], @@ -1648,6 +1898,8 @@ "react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="], + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -1686,12 +1938,20 @@ "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + "reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], "resend": ["resend@6.32.1", "", { "dependencies": { "postal-mime": "2.7.6", "standardwebhooks": "1.1.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-Z9DxkIZt5uvVNkjxjc+r4rCPI0MdDIJ7H+GvU50o9GkhrD0tVTwP6caE8KYEco90sos312l6R72jAEMCBFBiFQ=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], @@ -1700,6 +1960,10 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rolldown": ["rolldown@1.2.12", "", { "dependencies": { "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.12", "@rolldown/binding-android-arm64": "1.2.12", "@rolldown/binding-darwin-arm64": "1.2.12", "@rolldown/binding-darwin-x64": "1.2.12", "@rolldown/binding-freebsd-x64": "1.2.12", "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", "@rolldown/binding-linux-arm64-gnu": "1.2.12", "@rolldown/binding-linux-arm64-musl": "1.2.12", "@rolldown/binding-linux-ppc64-gnu": "1.2.12", "@rolldown/binding-linux-s390x-gnu": "1.2.12", "@rolldown/binding-linux-x64-gnu": "1.2.12", "@rolldown/binding-linux-x64-musl": "1.2.12", "@rolldown/binding-openharmony-arm64": "1.2.12", "@rolldown/binding-win32-arm64-msvc": "1.2.12", "@rolldown/binding-win32-x64-msvc": "1.2.12" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -1716,14 +1980,22 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], "selderee": ["selderee@0.12.0", "", { "dependencies": { "parseley": "~0.13.1" } }, "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], @@ -1750,6 +2022,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -1762,6 +2036,8 @@ "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -1770,6 +2046,8 @@ "standardwebhooks": ["standardwebhooks@1.1.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ=="], + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="], @@ -1804,6 +2082,8 @@ "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="], "systeminformation": ["systeminformation@5.33.15", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-s5RloL0F7xhN0ZjfHtLGnbf7EW74EI0N8NFL+1xR7Oxz9QA33obNZsl7lyfnJOhN04E/kGFPoJPlvY7TZ5fzmw=="], @@ -1812,6 +2092,12 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], + + "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], "three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="], @@ -1820,8 +2106,12 @@ "three-stdlib": ["three-stdlib@2.36.1", "", { "dependencies": { "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", "@types/webxr": "^0.5.2", "draco3d": "^1.4.1", "fflate": "^0.6.9", "potpack": "^1.0.1" }, "peerDependencies": { "three": ">=0.128.0" } }, "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], @@ -1832,6 +2122,10 @@ "tldts-core": ["tldts-core@7.4.19", "", {}, "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], + + "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -1846,6 +2140,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], "ts-results-es": ["ts-results-es@7.1.0", "", {}, "sha512-s6BU3PE8ZimmclHbN0uDAUFJQEkDPX/rlSEKhe99F9WjpAtRZQoVVQEnntNGHRUrh3kQUv85gPpxVAJe0ung3Q=="], @@ -1894,10 +2190,14 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="], + "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], @@ -1920,6 +2220,8 @@ "web": ["web@workspace:packages/web"], + "webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="], + "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], @@ -1930,6 +2232,8 @@ "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], @@ -1942,10 +2246,18 @@ "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="], "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], @@ -1962,6 +2274,12 @@ "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -1974,10 +2292,38 @@ "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@electron/get/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@electron/get/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@electron/notarize/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@electron/osx-sign/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/rebuild/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@electron/universal/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@electron/windows-sign/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@malept/flatpak-bundler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -2014,8 +2360,32 @@ "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + + "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "app-builder-lib/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "builder-util/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "builder-util-runtime/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "conf/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -2026,44 +2396,102 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-publish/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-winstaller/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "express-rate-limit/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "global-agent/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "http-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "imapflow/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "libmime/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "mailparser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "mailparser/nodemailer": ["nodemailer@10.0.14", "", {}, "sha512-eJoLFKg55fesSlzUekmfOv/SBXh4KUYT8oiQc2RLP0tfG0rbNMJBK7wgHJwXTJwqyKlSJMz9UzDZ4v+aHEvo8g=="], + "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + "node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "node-api-version/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "node-gyp/undici": ["undici@6.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="], + + "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "pkg-types/confbox": ["confbox@0.3.1", "", {}, "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="], + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -2072,12 +2500,16 @@ "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "read-binary-file-arch/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -2086,26 +2518,44 @@ "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "simple-update-notifier/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "stats-gl/@types/three": ["@types/three@0.186.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.3", "meshoptimizer": "~1.1.1" } }, "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ=="], "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "sumchecker/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "temp-file/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], "three-stdlib/fflate": ["fflate@0.6.11", "", {}, "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg=="], + "tiny-async-pool/semver": ["semver@5.7.3", "", { "bin": { "semver": "bin/semver" } }, "sha512-kM18BPOqTi0+TB68703/GJRpAML5Me+IDQsR1J26YB3EFKKNEyHIVZuWDX7+WKQB3tHwDwgk24BfhdpWUbytvQ=="], + + "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], + "website/@types/three": ["@types/three@0.186.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.3", "meshoptimizer": "~1.1.1" } }, "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ=="], "website/three": ["three@0.186.1", "", {}, "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA=="], + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -2124,8 +2574,24 @@ "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + "@electron/get/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@electron/notarize/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@electron/osx-sign/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@electron/rebuild/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@electron/universal/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "@electron/windows-sign/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@malept/flatpak-bundler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], @@ -2176,14 +2642,52 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "app-builder-lib/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "builder-util-runtime/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "builder-util/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "electron-winstaller/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "express-rate-limit/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "http-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "micromark/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "read-binary-file-arch/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "router/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "stats-gl/@types/three/meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], @@ -2192,6 +2696,18 @@ "website/@types/three/meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], @@ -2208,6 +2724,18 @@ "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], } } diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 87c1b31c..6fb2574e 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -36,3 +36,21 @@ A `bun run dev` already running is used as it is; without one, the stack is star again when the window closes. The app loads the root `.env`'s `APP_URL`, the one origin the api takes sign-ins from. `DESKTOP_APP_URL` names another and skips the stack. A packaged app always loads hosted Personal Agent Proxy. + +## Packaging and releases + +```bash +bun run --cwd packages/desktop package +``` + +Builds the macOS app for Apple silicon and Intel into `release/`: a disk image each to install +from, a zip each for updates, and `latest-mac.yml`, the update feed (`electron-builder.yml`). +Without Apple's certificate the app is unsigned, which macOS warns about on first open, and +can't update itself. + +A release is a `desktop-v` tag, with the version in `package.json`: +`.github/workflows/desktop.yml` builds on it and publishes the files as a GitHub release. The app +checks the repository's latest release for `latest-mac.yml` (`src/updates.ts`), so the desktop +app's releases are the repository's releases; the Docker image has tags, not releases. Signing +and notarization start once the workflow has `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_ID`, +`APPLE_APP_SPECIFIC_PASSWORD` and `APPLE_TEAM_ID` as secrets. diff --git a/packages/desktop/build/entitlements.mac.plist b/packages/desktop/build/entitlements.mac.plist new file mode 100644 index 00000000..3da4e455 --- /dev/null +++ b/packages/desktop/build/entitlements.mac.plist @@ -0,0 +1,9 @@ + + + + + + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory + + diff --git a/packages/desktop/build/icon.png b/packages/desktop/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b364ecebd568172ac94bd25c71706f4c5a23748f GIT binary patch literal 61733 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 literal 0 HcmV?d00001 diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml new file mode 100644 index 00000000..3ef67d69 --- /dev/null +++ b/packages/desktop/electron-builder.yml @@ -0,0 +1,40 @@ +# The macOS app, for Apple silicon and Intel: a disk image to install from, and the zip +# electron-updater installs updates from. `bun run --cwd packages/desktop package` builds them +# into release/; .github/workflows/desktop.yml builds and publishes them on a `desktop-v*` tag. +appId: com.personalagentproxy.desktop +productName: Personal Agent Proxy +# No spaces: GitHub renames an asset with spaces, and the update feed names the files as built. +artifactName: personal-agent-proxy-${version}-${arch}.${ext} +directories: + output: release + buildResources: build +# The main process, bundled with everything it needs by build.ts; nothing from node_modules. +files: + - dist/**/* + - '!dist/**/*.map' + - package.json + - '!node_modules/**' +npmRebuild: false +# Where electron-updater looks: the assets of the repository's latest GitHub release, whatever +# its tag, which the workflow uploads with the feed, latest-mac.yml. +publish: + - provider: generic + url: https://github.com/personalagentproxy/proxy/releases/latest/download +mac: + category: public.app-category.productivity + icon: build/icon.png + # Signed and notarized once the workflow has Apple's certificate (CSC_LINK) and credentials; + # until then the build is unsigned, which macOS warns about on first open and electron-updater + # cannot update. + hardenedRuntime: true + entitlements: build/entitlements.mac.plist + entitlementsInherit: build/entitlements.mac.plist + target: + - target: dmg + arch: [arm64, x64] + - target: zip + arch: [arm64, x64] +# The app's own links, personalagentproxy://…, which the browser hands things back through. +protocols: + - name: Personal Agent Proxy + schemes: [personalagentproxy] diff --git a/packages/desktop/package.json b/packages/desktop/package.json index fcba1bad..46f743bf 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -10,20 +10,21 @@ "scripts": { "desktop": "bun run dev.ts", "build": "bun run build.ts", + "package": "NODE_ENV=production bun run build.ts && electron-builder --mac --publish never", "lint": "oxlint", "test": "bun test", "typecheck": "tsc --noEmit" }, - "dependencies": { - "ts-results-es": "^7.1.0", - "zod": "^4.5.4" - }, "devDependencies": { "@types/bun": "latest", "dotenv": "^16.6.1", "electron": "^41.0.0", + "electron-builder": "^26.18.0", + "electron-updater": "^6.8.11", "esbuild": "^0.27.0", "oxlint": "^1.79.0", - "typescript": "~6.0.2" + "ts-results-es": "^7.1.0", + "typescript": "~6.0.2", + "zod": "^4.5.4" } } diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 4b4ba972..17f8627d 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -5,6 +5,7 @@ import {app, dialog, type BrowserWindow} from 'electron'; import {finishSignIn} from './auth'; import {DEEP_LINK_SCHEME, findDeepLink, parseDeepLink, type DeepLink} from './deep-link'; import {DESKTOP_VERSION} from './env'; +import {initUpdates} from './updates'; import {createMainWindow, loadApp, showSignIn} from './window'; // The desktop app: a window around the web app, hosted, with nothing of it bundled. The main @@ -103,6 +104,7 @@ if (!app.requestSingleInstanceLock()) { app.dock?.setIcon(path.join(__dirname, '../build/icon.png')); } loadApp(window()); + initUpdates(); const link = findDeepLink(process.argv); if (link) { void onDeepLink(link); diff --git a/packages/desktop/src/updates.ts b/packages/desktop/src/updates.ts new file mode 100644 index 00000000..85077c55 --- /dev/null +++ b/packages/desktop/src/updates.ts @@ -0,0 +1,39 @@ +import {app, dialog} from 'electron'; +import {autoUpdater} from 'electron-updater'; + +// Updates, from the feed electron-builder.yml names: checked on start and every few hours, and +// installed when the person says so. + +const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; + +export function initUpdates(): void { + // A development run has no feed, and an unsigned build can't be updated on macOS either way. + if (!app.isPackaged) { + return; + } + + autoUpdater.on('error', (error) => { + console.warn('[updates] Checking for updates failed:', error.message); + }); + + autoUpdater.on('update-downloaded', (info) => { + void dialog + .showMessageBox({ + type: 'info', + message: `Personal Agent Proxy ${info.version} is ready to install`, + buttons: ['Restart now', 'Later'], + defaultId: 0, + cancelId: 1, + }) + .then(({response}) => { + if (response === 0) { + autoUpdater.quitAndInstall(); + } + }); + }); + + void autoUpdater.checkForUpdates().catch(() => undefined); + setInterval(() => { + void autoUpdater.checkForUpdates().catch(() => undefined); + }, CHECK_INTERVAL_MS); +} From 7227116290ea0f0f15530785d1439fdf8b3140f0 Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 22:20:02 +0000 Subject: [PATCH 5/8] let Plaid Link open an OAuth bank's sign-in in a window of the app's Plaid Link runs in its frame over the page, and signs in to an OAuth bank in a popup that reports back to it and closes, which only a window of the app's can do; the desktop app sent every popup to the browser, where Link never heard from it. With Link's frame on the page, the page under it inert, a popup is Link's and becomes a window of the app's, whose own links still go to the browser. --- packages/desktop/README.md | 3 +++ packages/desktop/src/window.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 6fb2574e..5b275224 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -24,6 +24,9 @@ everything but the app's own origin out of the window: links out of the app open sign-in ends on `/desktop-return`, which hands back through `personalagentproxy://open?path=…`, and the window opens the page the sign-in would have ended on. A custom MCP server that asks for a sign-in moves its form to the browser, filled in as it was. +- A bank is connected in the window as in the browser: Plaid Link runs in its frame over the page, + and an OAuth bank's sign-in, which Link opens in a popup that reports back to it, gets a window of + the app's for that, the one popup that doesn't go to the browser. - The main process is bundled by `build.ts` into `dist/main.cjs`, the splash pages as text in it. ## Development diff --git a/packages/desktop/src/window.ts b/packages/desktop/src/window.ts index f82dd72d..6cc06468 100644 --- a/packages/desktop/src/window.ts +++ b/packages/desktop/src/window.ts @@ -19,6 +19,10 @@ const RETRY_URL = 'personalagentproxy-desktop://retry'; const EXTERNAL_SCHEMES = new Set(['http:', 'https:', 'mailto:']); +// Plaid Link's frame, from which an OAuth bank's sign-in is opened. +const PLAID_LINK_ORIGIN = 'https://cdn.plaid.com'; +const POPUP_BOUNDS = {width: 520, height: 760}; + function urlOf(raw: string): URL | null { if (!URL.canParse(raw)) { return null; @@ -53,6 +57,13 @@ function isLoginPage(url: URL): boolean { return url.origin === APP_ORIGIN && url.pathname === '/login'; } +// Whether Plaid Link is open on the page: one of the page's frames is Plaid's. +function hasPlaidLinkFrame(win: BrowserWindow): boolean { + return win.webContents.mainFrame.framesInSubtree.some( + (frame) => urlOf(frame.url)?.origin === PLAID_LINK_ORIGIN, + ); +} + export function openExternal(raw: string): void { const url = urlOf(raw); if (!url || !EXTERNAL_SCHEMES.has(url.protocol)) { @@ -98,12 +109,27 @@ export function createMainWindow(): BrowserWindow { keepWindowBounds(win); win.once('ready-to-show', () => win.show()); - // Links that open a new window, such as the docs', open in the browser. + // Links that open a new window, such as the docs', open in the browser. The one window opened + // here is an OAuth bank's sign-in: Plaid Link, in its frame over the page, opens the bank's page + // in a popup that reports back to Link and closes, which it can only do from a window of this + // app's. With Link open the page under it is inert, so the popup is Link's. win.webContents.setWindowOpenHandler(({url}) => { + if (hasPlaidLinkFrame(win)) { + return {action: 'allow', overrideBrowserWindowOptions: {...POPUP_BOUNDS, parent: win}}; + } openExternal(url); return {action: 'deny'}; }); + // The popup is the bank's: it goes where the bank's sign-in goes, and opens no windows of its + // own. + win.webContents.on('did-create-window', (popup) => { + popup.webContents.setWindowOpenHandler(({url}) => { + openExternal(url); + return {action: 'deny'}; + }); + }); + win.webContents.on('will-navigate', (event, raw) => { if (raw === SIGN_IN_URL) { event.preventDefault(); From 1f00a371f59a03cd0a43afa6e2c789661d563371 Mon Sep 17 00:00:00 2001 From: hkonsti <45428767+hkonsti@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:06:54 +0000 Subject: [PATCH 6/8] open the desktop app's browser work through the login page, not a sign-in code The handoff let the app mint a link that signed whichever browser opened it in as the app's person, which any signed-in user could send to anyone else. The browser now goes through /login?callbackUrl=, which sends a signed-in browser straight on and lets any other sign in as itself first. --- packages/api/src/auth/desktop.test.ts | 125 ------------------ packages/api/src/auth/desktop.ts | 93 ++----------- packages/api/src/connections/connect-flow.ts | 2 +- packages/api/src/server/create-api-router.ts | 6 +- packages/contracts/src/auth.ts | 12 -- .../v040_add_desktop_handoffs/migration.sql | 20 --- packages/db/prisma/schema.prisma | 19 --- packages/db/src/desktop-auth.ts | 47 ------- packages/desktop/README.md | 9 +- packages/web/src/client/auth-client.ts | 10 -- packages/web/src/lib/desktop.ts | 25 +--- packages/web/src/router.tsx | 7 +- 12 files changed, 29 insertions(+), 346 deletions(-) delete mode 100644 packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql diff --git a/packages/api/src/auth/desktop.test.ts b/packages/api/src/auth/desktop.test.ts index 33b3d3fa..8de5be4f 100644 --- a/packages/api/src/auth/desktop.test.ts +++ b/packages/api/src/auth/desktop.test.ts @@ -189,128 +189,3 @@ describe('handleDesktopSessionRoute', () => { expect(res.cookies).toHaveLength(0); }); }); - -const createDesktopHandoff = mock(); -const takeDesktopHandoff = mock(); - -mock.module('@proxy/db/desktop-auth', () => ({ - createDesktopAuthCode, - getDesktopAuthCode, - takeDesktopAuthCode, - createDesktopHandoff, - takeDesktopHandoff, -})); - -const authenticateApiRequest = mock(); - -mock.module('../utils/auth', () => ({authenticateApiRequest})); - -function makeNavigationRes() { - const cookies: RecordedCookie[] = []; - const res = { - cookies, - cookie: mock((name: string, value: string, options: RecordedCookie['options']) => { - cookies.push({name, value, options}); - }), - redirect: mock(), - status: mock(() => res), - json: mock(() => res), - }; - return res; -} - -const START = '/api/orgs/acme/connections/google/start?desktop=1'; - -function liveHandoff() { - return {userId: 'user-1', next: START, expires: new Date(Date.now() + 60_000)}; -} - -beforeEach(() => { - createDesktopHandoff.mockResolvedValue(Ok(undefined)); - takeDesktopHandoff.mockResolvedValue(Ok(liveHandoff())); - authenticateApiRequest.mockResolvedValue(Err(ApiErr.unauthenticated())); -}); - -describe('handleDesktopHandoffRoute', () => { - test('keeps where the browser goes on to, for the person, and answers the address to open', async () => { - const {handleDesktopHandoffRoute} = await import('./desktop'); - const before = Date.now(); - - const result = await handleDesktopHandoffRoute({body: {next: START}, user: {userId: 'user-1'}} as never); - - const {url} = result.unwrap(); - const code = new URL(url).searchParams.get('code') ?? ''; - expect(url).toBe(`https://app.example.com/auth/desktop/handoff?code=${code}`); - expect(code).toMatch(/^[0-9a-f]{64}$/); - const stored = createDesktopHandoff.mock.calls[0]?.[0] as {codeHash: string; userId: string; next: string; expires: Date}; - expect(stored).toMatchObject({codeHash: sha256(code), userId: 'user-1', next: START}); - expect(stored.expires.valueOf() - before).toBeLessThanOrEqual(2 * 60 * 1000 + 1000); - }); - - test('goes on only to a path in the app', async () => { - const {handleDesktopHandoffRoute} = await import('./desktop'); - - for (const next of ['https://evil.example/', '//evil.example/', 'connections']) { - const result = await handleDesktopHandoffRoute({body: {next}, user: {userId: 'user-1'}} as never); - expect(result.isErr()).toBe(true); - } - expect(createDesktopHandoff).not.toHaveBeenCalled(); - }); -}); - -describe('handleDesktopHandoffOpenRoute', () => { - test('signs the browser in as the app’s person and sends it on', async () => { - const {handleDesktopHandoffOpenRoute} = await import('./desktop'); - const res = makeNavigationRes(); - - await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {}} as never, res as never); - - expect(takeDesktopHandoff).toHaveBeenCalledWith(sha256('code-1')); - expect(createSession.mock.calls[0]?.[0]).toMatchObject({userId: 'user-1'}); - expect(res.cookies).toHaveLength(1); - expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); - }); - - test('a browser already signed in as the same person keeps its session', async () => { - authenticateApiRequest.mockResolvedValue(Ok({userId: 'user-1', email: 'a@example.com'})); - const {handleDesktopHandoffOpenRoute} = await import('./desktop'); - const res = makeNavigationRes(); - - await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {cookie: 'x'}} as never, res as never); - - expect(createSession).not.toHaveBeenCalled(); - expect(res.cookies).toHaveLength(0); - expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); - }); - - test('a browser signed in as someone else is signed in as the app’s person', async () => { - authenticateApiRequest.mockResolvedValue(Ok({userId: 'user-2', email: 'b@example.com'})); - const {handleDesktopHandoffOpenRoute} = await import('./desktop'); - const res = makeNavigationRes(); - - await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {cookie: 'x'}} as never, res as never); - - expect(createSession.mock.calls[0]?.[0]).toMatchObject({userId: 'user-1'}); - expect(res.redirect).toHaveBeenCalledWith(`https://app.example.com${START}`); - }); - - test('an unknown, taken or expired code lands on the login page, as a spent magic link does', async () => { - const {handleDesktopHandoffOpenRoute} = await import('./desktop'); - - takeDesktopHandoff.mockResolvedValue(Ok(null)); - const unknown = makeNavigationRes(); - await handleDesktopHandoffOpenRoute({query: {code: 'nope'}, headers: {}} as never, unknown as never); - expect(unknown.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); - - takeDesktopHandoff.mockResolvedValue(Ok({...liveHandoff(), expires: new Date(Date.now() - 1000)})); - const expired = makeNavigationRes(); - await handleDesktopHandoffOpenRoute({query: {code: 'code-1'}, headers: {}} as never, expired as never); - expect(expired.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); - - const missing = makeNavigationRes(); - await handleDesktopHandoffOpenRoute({query: {}, headers: {}} as never, missing as never); - expect(missing.redirect).toHaveBeenCalledWith('https://app.example.com/login?error=Verification'); - - expect(createSession).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/api/src/auth/desktop.ts b/packages/api/src/auth/desktop.ts index b25ead91..ef494c24 100644 --- a/packages/api/src/auth/desktop.ts +++ b/packages/api/src/auth/desktop.ts @@ -3,15 +3,13 @@ import {createHash, randomBytes} from 'node:crypto'; import type {Request, Response} from 'express'; import type {Result} from 'ts-results-es'; -import {createDesktopAuthCode, createDesktopHandoff, getDesktopAuthCode, takeDesktopAuthCode, takeDesktopHandoff} from '@proxy/db/desktop-auth'; -import {desktopCodeBodySchema, desktopHandoffBodySchema, desktopSessionBodySchema, type DesktopCode, type DesktopHandoff} from '@proxy/contracts/auth'; -import {ApiErr, type ApiError, Do, parseSchema, requirePresent} from '@proxy/utils'; +import {createDesktopAuthCode, getDesktopAuthCode, takeDesktopAuthCode} from '@proxy/db/desktop-auth'; +import {desktopCodeBodySchema, desktopSessionBodySchema, type DesktopCode} from '@proxy/contracts/auth'; +import {type ApiError, Do, parseSchema} from '@proxy/utils'; import {log, serializeError} from '../observability/log'; import type {AuthenticatedRequest} from '../server/middleware/require-auth'; -import {authenticateApiRequest} from '../utils/auth'; -import {env} from '../utils/env'; -import {establishSession, hashToken, queryParam, redirectToLoginError, sanitizeCallbackUrl, timingSafeEqualString} from './session'; +import {establishSession, hashToken, timingSafeEqualString} from './session'; /** * Signing the desktop app in. The app can't sign in itself: Google turns embedded browsers away, @@ -22,11 +20,11 @@ import {establishSession, hashToken, queryParam, redirectToLoginError, sanitizeC * for a session cookie of its own (`handleDesktopSessionRoute`), and is signed in as the browser * is, with the same kind of session, for as long. * - * The other way round, for what has to happen in the browser once the app is signed in, such as - * connecting a service whose sign-in turns the app's window away as Google's does: the app asks - * for a handoff (`handleDesktopHandoffRoute`), an address with a one-time code that signs the - * browser in as the same person and goes on to where the app was headed - * (`handleDesktopHandoffOpenRoute`), and opens it in the browser. + * Nothing signs the browser in the other way round. What has to happen in the browser once the + * app is signed in, such as connecting a service whose sign-in turns the app's window away as + * Google's does, the web app opens there through the login page, which sends a signed-in browser + * straight on and lets any other sign in as itself first. A code the app could mint for the browser + * would be a link that signs whoever opens it in as the app's person. */ // Long enough to click through the browser's "open the app?" prompt, no longer. @@ -114,76 +112,3 @@ export async function handleDesktopSessionRoute(req: Request, res: Response): Pr log.info('Desktop app signed in', {userId: authCode.userId}); res.json({ok: true}); } - -// Long enough for the browser to open, no longer. -const handoffMaxAgeMs = 2 * 60 * 1000; - -/** - * `POST /auth/desktop/handoff` `{next}` — the app's window, signed in: the address to open in the - * browser, for `next`, a path in the app, as the sign-in's callback paths are. - */ -export function handleDesktopHandoffRoute(request: AuthenticatedRequest): Promise> { - return Do(async ($) => { - const appUrl = $(requirePresent(env.APP_URL, ApiErr.internalError(new Error('APP_URL is not configured')))); - const body = $(parseSchema(desktopHandoffBodySchema, request.body)); - const next = $(requirePresent(sanitizeCallbackUrl(body.next), ApiErr.validationError('next must be a path in the app'))); - const code = randomBytes(32).toString('hex'); - $( - await createDesktopHandoff({ - codeHash: hashToken(code), - userId: request.user.userId, - next, - expires: new Date(Date.now() + handoffMaxAgeMs), - }), - ); - return {url: `${appUrl}/auth/desktop/handoff?code=${code}`}; - }); -} - -/** - * `GET /auth/desktop/handoff?code=` — the browser, opened by the app: signed in as the app's - * person, unless it is already, and sent on to `next`. A code that is unknown, expired or taken - * lands on the login page saying the link is no good, as a spent magic link does. - */ -export async function handleDesktopHandoffOpenRoute(req: Request, res: Response): Promise { - const appUrl = env.APP_URL; - if (!appUrl) { - res.status(500).json({error: 'app_url_not_configured'}); - return; - } - - const code = queryParam(req, 'code'); - if (!code) { - redirectToLoginError(res, appUrl, 'Verification'); - return; - } - - const handoffResult = await takeDesktopHandoff(hashToken(code)); - if (handoffResult.isErr()) { - log.error('Desktop handoff failed to take the code', serializeError(handoffResult.error)); - redirectToLoginError(res, appUrl, 'Callback'); - return; - } - - const handoff = handoffResult.value; - if (!handoff || handoff.expires.valueOf() < Date.now()) { - redirectToLoginError(res, appUrl, 'Verification'); - return; - } - - // The browser is usually signed in already, since the app was signed in through it: that - // session stays. Signed in as someone else, or not at all, it is signed in as the app's person. - const existing = await authenticateApiRequest(req.headers); - const signedIn = existing.isOk() && existing.value.userId === handoff.userId; - if (!signedIn) { - const sessionResult = await establishSession(res, handoff.userId); - if (sessionResult.isErr()) { - log.error('Desktop handoff failed to create the session', serializeError(sessionResult.error)); - redirectToLoginError(res, appUrl, 'Callback'); - return; - } - } - - log.info('Desktop app handed over to the browser', {userId: handoff.userId, signedIn}); - res.redirect(`${appUrl}${handoff.next}`); -} diff --git a/packages/api/src/connections/connect-flow.ts b/packages/api/src/connections/connect-flow.ts index 8ded2dd8..a92ba359 100644 --- a/packages/api/src/connections/connect-flow.ts +++ b/packages/api/src/connections/connect-flow.ts @@ -223,7 +223,7 @@ function catalogPath(slug: string | null, returnTo: ReturnTo | undefined): strin /** * Where a sign-in comes back to: the organization it began in and, for one begun from the desktop - * app, that it did, which the handoff to the browser marks with `?desktop=1` on the start. + * app, that it did, which the web app marks with `?desktop=1` on the start it opens in the browser. */ type Back = {slug: string | null; returnTo: ReturnTo | undefined; desktop: boolean}; diff --git a/packages/api/src/server/create-api-router.ts b/packages/api/src/server/create-api-router.ts index 6aebd06e..1d4620a7 100644 --- a/packages/api/src/server/create-api-router.ts +++ b/packages/api/src/server/create-api-router.ts @@ -25,7 +25,7 @@ import { handleSetAgentRevokedRoute, handleShareAgentRoute, } from '../agents/route'; -import {handleDesktopCodeRoute, handleDesktopHandoffOpenRoute, handleDesktopHandoffRoute, handleDesktopSessionRoute} from '../auth/desktop'; +import {handleDesktopCodeRoute, handleDesktopSessionRoute} from '../auth/desktop'; import {handleDevLoginRoute} from '../auth/dev-login'; import {handleEmailSignInRoute, handleEmailVerifyRoute} from '../auth/email'; import {handleGoogleCallbackRoute, handleGoogleStartRoute, handleSignInMethodsRoute} from '../auth/google'; @@ -104,10 +104,6 @@ export function createApiRouter(): express.Router { // session and sends no Origin, so the code and its verifier are the guard there. router.post('/auth/desktop/code', requireBrowserOrigin, withAuthResult('Desktop code route', handleDesktopCodeRoute)); router.post('/auth/desktop/session', wrapAsyncRoute('Desktop session route', handleDesktopSessionRoute)); - // And the browser signed in as the app's person: asked for by the app's window, a write from - // the web app; opened as a top-level navigation. - router.post('/auth/desktop/handoff', requireBrowserOrigin, withAuthResult('Desktop handoff route', handleDesktopHandoffRoute)); - router.get('/auth/desktop/handoff', wrapAsyncRoute('Desktop handoff open route', handleDesktopHandoffOpenRoute)); router.use('/api/me', requireBrowserOrigin); router.get('/api/me', withAuthResult('Me route', handleMeRoute)); diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index a96d9087..894aa47e 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -47,15 +47,3 @@ export const desktopSessionBodySchema = z.object({ code: z.string().min(1).max(256), verifier: z.string().min(1).max(256), }); - -/** - * The other way round: `POST /auth/desktop/handoff` `{next}`, from the app's window, signed in, - * for what has to happen in the browser, such as connecting a service whose sign-in turns the - * app's window away. The answer is the address the app opens in the browser, which signs the - * browser in as the same person, once, and goes on to `next`, a path in the app. - */ -export const desktopHandoffBodySchema = z.object({next: z.string().max(2000)}); - -export const desktopHandoffSchema = z.object({url: z.string()}); - -export type DesktopHandoff = z.infer; diff --git a/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql b/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql deleted file mode 100644 index b5bc82c6..00000000 --- a/packages/db/prisma/migrations/v040_add_desktop_handoffs/migration.sql +++ /dev/null @@ -1,20 +0,0 @@ --- One-time codes that sign the browser in as the desktop app's person, for what has to happen --- there, such as connecting a service. Kept only as their hash. --- CreateTable -CREATE TABLE "DesktopHandoff" ( - "id" TEXT NOT NULL, - "codeHash" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "next" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "DesktopHandoff_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "DesktopHandoff_codeHash_key" ON "DesktopHandoff"("codeHash"); - --- CreateIndex -CREATE INDEX "DesktopHandoff_userId_idx" ON "DesktopHandoff"("userId"); - diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index ee4c5edc..63ec14f3 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -43,7 +43,6 @@ model User { threads AgentThread[] writeTargets WriteTarget[] desktopCodes DesktopAuthCode[] - handoffs DesktopHandoff[] } // A sign-in provider identity linked to a user, such as their Google account. Only who it is: @@ -98,24 +97,6 @@ model DesktopAuthCode { @@index([userId]) } -// The other way round: a one-time code that signs the browser in as the desktop app's person, for -// what has to happen there, such as connecting a service whose sign-in turns the app's window -// away (`GET /auth/desktop/handoff`). `next` is where the browser goes on to, a path in the app. -// Minutes long, single-use, and kept only as its hash. -model DesktopHandoff { - id String @id @default(cuid()) - codeHash String @unique - userId String - next String - expires DateTime - - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) -} - // A magic link waiting to be clicked. `token` is a hash, never the token from the email. model VerificationToken { identifier String diff --git a/packages/db/src/desktop-auth.ts b/packages/db/src/desktop-auth.ts index fe370cce..f65248a9 100644 --- a/packages/db/src/desktop-auth.ts +++ b/packages/db/src/desktop-auth.ts @@ -53,50 +53,3 @@ export async function takeDesktopAuthCode(codeHash: string): Promise 0); } - -// The other way round: a code that signs the browser in as the app's person, for what has to -// happen there. Kept once, taken once. - -export type DesktopHandoffRow = { - userId: string; - // Where the browser goes on to: a path in the app. - next: string; - expires: Date; -}; - -/** Keeps a handoff; the expired ones go with it. */ -export async function createDesktopHandoff( - data: DesktopHandoffRow & {codeHash: string}, -): Promise> { - return ( - await wrapDb(() => - db.$transaction([ - db.desktopHandoff.deleteMany({where: {expires: {lt: new Date()}}}), - db.desktopHandoff.create({data}), - ]), - ) - ).map(() => undefined); -} - -/** - * Takes a handoff, once: `Ok(null)` when it is unknown or taken already, by this request or - * another at the same moment. An expired one is returned for the caller to refuse. - */ -export async function takeDesktopHandoff( - codeHash: string, -): Promise> { - return wrapDb(async () => { - const handoff = await db.desktopHandoff.findUnique({ - where: {codeHash}, - select: {id: true, userId: true, next: true, expires: true}, - }); - if (!handoff) { - return null; - } - const {count} = await db.desktopHandoff.deleteMany({where: {id: handoff.id}}); - if (count === 0) { - return null; - } - return {userId: handoff.userId, next: handoff.next, expires: handoff.expires}; - }); -} diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 5b275224..8c5852bb 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -18,10 +18,11 @@ everything but the app's own origin out of the window: links out of the app open `POST /auth/desktop/session` for a session cookie, which its window keeps, and is signed in as a browser is. - Connecting a service whose sign-in turns the app's window away, as Google's does, happens in - the browser too: the web app, which knows the window by its user agent, asks the api for a - handoff (`POST /auth/desktop/handoff`), an address with a one-time code that signs the browser - in as the same person and goes on to the service's sign-in, and opens it in the browser. That - sign-in ends on `/desktop-return`, which hands back through `personalagentproxy://open?path=…`, + the browser too: the web app, which knows the window by its user agent, opens the service's + sign-in in the browser through `/login?callbackUrl=…`, which sends a signed-in browser straight + on and lets any other sign in as itself first. (The app mints nothing that signs the browser in: + such a link would sign whoever opened it in as the app's person.) That sign-in ends on + `/desktop-return`, which hands back through `personalagentproxy://open?path=…`, and the window opens the page the sign-in would have ended on. A custom MCP server that asks for a sign-in moves its form to the browser, filled in as it was. - A bank is connected in the window as in the browser: Plaid Link runs in its frame over the page, diff --git a/packages/web/src/client/auth-client.ts b/packages/web/src/client/auth-client.ts index b2c1c1d4..c6c606e8 100644 --- a/packages/web/src/client/auth-client.ts +++ b/packages/web/src/client/auth-client.ts @@ -1,6 +1,5 @@ import { desktopCodeSchema, - desktopHandoffSchema, magicLinkResponseSchema, signInMethodsSchema, } from '@proxy/contracts/auth'; @@ -65,12 +64,3 @@ export function devLogin() { export function createDesktopCode(challenge: string) { return apiRequest('POST', '/auth/desktop/code', desktopCodeSchema, {challenge}); } - -/** - * The other way round (`POST /auth/desktop/handoff`): the address the desktop app opens in the - * browser, which signs the browser in as the person signed in here, once, and goes on to `next`, - * a path in the app. - */ -export function createDesktopHandoff(next: string) { - return apiRequest('POST', '/auth/desktop/handoff', desktopHandoffSchema, {next}); -} diff --git a/packages/web/src/lib/desktop.ts b/packages/web/src/lib/desktop.ts index a5e560cf..570cb342 100644 --- a/packages/web/src/lib/desktop.ts +++ b/packages/web/src/lib/desktop.ts @@ -1,7 +1,4 @@ -import type {FetchError} from '@proxy/utils'; import type {MouseEvent} from 'react'; -import {Ok, type Result} from 'ts-results-es'; -import {createDesktopHandoff} from '@/client/auth-client'; // The desktop app, a window around this web app (packages/desktop). It signs in through the // browser, which hands what it needs back through the app's own links, `personalagentproxy://…`, @@ -29,17 +26,13 @@ export function desktopOpenLink(path: string): string { } /** - * Hands `next`, a path in the app, to the browser: the api gives an address that signs the - * browser in as the same person, once, and sends it on, and the app opens every link out of its - * window in the browser. + * Hands `next`, a path in the app, to the browser, where the app opens every link out of its + * window. The browser goes through the login page, which sends a signed-in browser straight on + * and lets any other sign in first: the browser is signed in as whoever holds it, never as + * whoever holds the app, so nothing the app mints could sign someone else in. */ -export async function handOffToBrowser(next: string): Promise> { - const handoff = await createDesktopHandoff(next); - if (handoff.isErr()) { - return handoff; - } - window.open(handoff.value.url, '_blank'); - return Ok(undefined); +export function handOffToBrowser(next: string): void { + window.open(`${window.location.origin}/login?callbackUrl=${encodeURIComponent(next)}`, '_blank'); } /** @@ -54,9 +47,5 @@ export function onSignInClick(event: MouseEvent): void { event.preventDefault(); const url = new URL(event.currentTarget.href); url.searchParams.set('desktop', '1'); - void handOffToBrowser(`${url.pathname}${url.search}`).then((result) => { - if (result.isErr()) { - console.warn('Could not hand the sign-in to the browser', result.error.kind); - } - }); + handOffToBrowser(`${url.pathname}${url.search}`); } diff --git a/packages/web/src/router.tsx b/packages/web/src/router.tsx index 14baecfb..1b05d4af 100644 --- a/packages/web/src/router.tsx +++ b/packages/web/src/router.tsx @@ -1,6 +1,6 @@ import type {SignInMethods} from '@proxy/contracts/auth'; import type {Org} from '@proxy/contracts/orgs'; -import {Outlet, createBrowserRouter, redirect} from 'react-router'; +import {Outlet, createBrowserRouter, redirect, redirectDocument} from 'react-router'; import { agentCollectionLoader, agentRecordLoader, @@ -177,6 +177,11 @@ async function loginLoader({request}: {request: Request}): Promise Date: Tue, 3 Nov 2026 23:06:55 +0000 Subject: [PATCH 7/8] keep every sign-in the desktop app's window would follow in the browser Every "Sign in again" link hands off to the browser, and the window turns any redirect out of the app's origin into the browser running that page. A custom MCP server's sign-in begun from the app ends back in it, and its form no longer waits on the browser. A second press of the sign-in button shares the first's verifier. Startup takes an early return instead of an else block, and AGENTS.md lists bun run desktop. --- AGENTS.md | 3 +- packages/api/src/connections/custom/route.ts | 7 ++-- .../api/src/connections/mcp/mcp-connect.ts | 7 +++- packages/contracts/src/connections.ts | 2 + packages/desktop/src/auth.ts | 8 +++- packages/desktop/src/main.ts | 13 +++++-- packages/desktop/src/window.ts | 38 ++++++++++++++++++- .../web/src/components/connect-mcp-form.tsx | 34 ++++++++++------- .../web/src/components/connection-access.tsx | 4 +- 9 files changed, 88 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b9020733..d19c7ba2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,11 +7,12 @@ - `bun run docs` - Start only the docs (http://localhost:5174), without the database, the api or the other apps - `bun run website` - Start only the website (http://localhost:5175), without the database, the api or the other apps - `bun run map` - Start the screen map (http://localhost:5176), the development-only canvas of every screen of the web app and the ways between them. It shows what `bun run crawl` in `packages/map` last wrote: with `bun run dev` up, the crawl seeds the demo workspace, signs in as a person, as a new organization and as an agent, visits every route in the web app's router, screenshots each and notes its links. Everything it writes is in `packages/map/generated`, which is gitignored +- `bun run desktop` - Start the desktop app (`packages/desktop`, an Electron window around the web app) on this checkout. A `bun run dev` already running is used as it is; without one the stack is started first and stopped when the window closes. The app loads `.env`'s `APP_URL`; `DESKTOP_APP_URL=` points it at another origin and skips the stack - `bun run seed` - With `bun run dev` up, fill the local database with a demo workspace (connections, agents with settings of their own, activity) and print a sign-in link for `demo@proxy.local`. Re-running replaces it - `bun run --cwd packages/db db:migrate:diff` - With the database running, print the SQL from the database to `schema.prisma`; save it as `packages/db/prisma/migrations//migration.sql`, the name `v` and the next three-digit number, such as `v011_add_x`: Prisma applies migrations in the order their names sort as text, so `10_x` would run before `9_mcp_oauth` (a test refuses any other name). `bun run dev` applies pending migrations on start - `bun run typecheck` - Typecheck all packages - `bun run test` - Run every package's tests (`bun test`; the api's with `--isolate`, since its tests mock modules per file) -- `bun run lint` - Lint the web app, the docs and the website with oxlint +- `bun run lint` - Lint the web app, the docs, the website, the screen map and the desktop app with oxlint - `bun run build` - Production builds of the web app, the docs and the website - `docker build -t proxy .` - The self-hosting image (`Dockerfile`): the api serving the web app's build on one origin, applying the migrations on start (`docker-entrypoint.sh`). `docker-compose.yml` runs it with Postgres and Caddy; `.github/workflows/image.yml` publishes it to `ghcr.io/personalagentproxy/proxy`. Hosted Personal Agent Proxy runs on Render from `render.yaml`: the image as one service (api and web app on one origin, two instances), Render Postgres, and the docs and the website as static sites diff --git a/packages/api/src/connections/custom/route.ts b/packages/api/src/connections/custom/route.ts index a5f77dfa..15c931df 100644 --- a/packages/api/src/connections/custom/route.ts +++ b/packages/api/src/connections/custom/route.ts @@ -62,6 +62,7 @@ function connectCustomMcp(request: AuthenticatedRequest, res: Response): Promise const token = body.token || null; const viewer = $(await requireViewer(request)); const shared = body.shared ?? false; + const desktop = body.desktop ?? false; // A server of an integration built in is connected as that integration: with the token, or // the key in the address, as its API key, where it takes one, else signed in to as its // catalog's entry is. A token for one that only signs in is refused rather than dropped. @@ -75,13 +76,13 @@ function connectCustomMcp(request: AuthenticatedRequest, res: Response): Promise return $(Err(ApiErr.validationError(`${builtIn.server.name} is connected by signing in to it, so leave the token out`))); } if (builtIn) { - return {kind: 'signIn' as const, url: $(await beginBuiltInSignIn(builtIn, res, {viewer, returnTo: body.returnTo, name: body.name, shared}))}; + return {kind: 'signIn' as const, url: $(await beginBuiltInSignIn(builtIn, res, {viewer, returnTo: body.returnTo, desktop, name: body.name, shared}))}; } const probe = $(await probeMcpServer(url, token)); if (probe.kind === 'signIn') { const server = $(await discoverMcpServer(url)); const start = (signIn: SignInRequest) => startMcpSignIn(server, signIn, {server: mcpEndpoints(server), name: body.name}); - return {kind: 'signIn' as const, url: $(await beginSignIn(res, {viewer, returnTo: body.returnTo, account: null, shared, start}))}; + return {kind: 'signIn' as const, url: $(await beginSignIn(res, {viewer, returnTo: body.returnTo, desktop, account: null, shared, start}))}; } const credential: CustomCredential = token === null ? {auth: 'none', url: url.toString()} : {auth: 'token', url: url.toString(), token}; const {row} = $(await saveConnection(viewer, 'mcp', {account: mcpAccount(url), credential, defaults: readOnlyToolNames(probe.tools), tools: probe.tools, name: body.name}, {shared})); @@ -89,7 +90,7 @@ function connectCustomMcp(request: AuthenticatedRequest, res: Response): Promise }); } -/** `POST /api/connections/mcp` with `{name, url, token?, returnTo?, shared?}`: connects the server, or begins the sign-in it asks for. */ +/** `POST /api/connections/mcp` with `{name, url, token?, returnTo?, shared?, desktop?}`: connects the server, or begins the sign-in it asks for. */ export async function handleConnectCustomMcpRoute(request: AuthenticatedRequest, res: Response): Promise { sendResult(res, await connectCustomMcp(request, res)); } diff --git a/packages/api/src/connections/mcp/mcp-connect.ts b/packages/api/src/connections/mcp/mcp-connect.ts index 0405a1de..5dd138ce 100644 --- a/packages/api/src/connections/mcp/mcp-connect.ts +++ b/packages/api/src/connections/mcp/mcp-connect.ts @@ -84,10 +84,15 @@ export function mcpConnectRoutes(builtIn: BuiltInMcp) { * integration's own server, the connection called `name`, the viewer's own or `shared`; its sign-in * page, for the browser to go to, which comes back to the integration's callback. */ -export function beginBuiltInSignIn(builtIn: BuiltInMcp, res: Response, begin: {viewer: OrgViewer; returnTo: ReturnTo | undefined; name: string; shared: boolean}): Promise> { +export function beginBuiltInSignIn( + builtIn: BuiltInMcp, + res: Response, + begin: {viewer: OrgViewer; returnTo: ReturnTo | undefined; desktop?: boolean; name: string; shared: boolean}, +): Promise> { return signInFlow(builtInSignIn(builtIn)).beginSignIn(res, { viewer: begin.viewer, returnTo: begin.returnTo, + desktop: begin.desktop, account: null, shared: begin.shared, start: (request) => startMcpSignIn(builtIn.server, request, {name: begin.name}), diff --git a/packages/contracts/src/connections.ts b/packages/contracts/src/connections.ts index a4036e73..18a9b90b 100644 --- a/packages/contracts/src/connections.ts +++ b/packages/contracts/src/connections.ts @@ -160,6 +160,8 @@ export const connectMcpBodySchema = z.object({ returnTo: returnToSchema.optional(), /** For every member of the organization rather than the person's own. */ shared: z.boolean().optional(), + /** Sent from the browser for the desktop app, whose window the server's sign-in page would turn away: the sign-in ends on the page that hands back to the app. */ + desktop: z.boolean().optional(), }); /** Connected, with the new connection, or the server asks for a sign-in, begun, at the page the browser goes to. */ diff --git a/packages/desktop/src/auth.ts b/packages/desktop/src/auth.ts index e3c35297..972c6a0a 100644 --- a/packages/desktop/src/auth.ts +++ b/packages/desktop/src/auth.ts @@ -12,9 +12,13 @@ import {APP_URL} from './env'; let verifier: string | null = null; -/** Where the browser signs in: the login page, coming back to /desktop-auth with the challenge. */ +/** + * Where the browser signs in: the login page, coming back to /desktop-auth with the challenge. A + * sign-in begun while one is under way, as a second press of the button is, shares its verifier: + * whichever browser tab comes back first signs the app in, and the other's code is nothing to it. + */ export function beginSignIn(): string { - verifier = randomBytes(32).toString('base64url'); + verifier ??= randomBytes(32).toString('base64url'); const challenge = createHash('sha256').update(verifier).digest('base64url'); const callbackUrl = `/desktop-auth?challenge=${challenge}`; diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 17f8627d..baba8780 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -60,10 +60,13 @@ async function onDeepLink(link: DeepLink): Promise { loadApp(win); } -// One instance: a link opened while the app runs reaches it, not a second copy. -if (!app.requestSingleInstanceLock()) { - app.quit(); -} else { +function start(): void { + // One instance: a link opened while the app runs reaches it, not a second copy. + if (!app.requestSingleInstanceLock()) { + app.quit(); + return; + } + app.setAsDefaultProtocolClient(DEEP_LINK_SCHEME); // macOS hands the app its links as they are opened, running or not. @@ -111,3 +114,5 @@ if (!app.requestSingleInstanceLock()) { } }); } + +start(); diff --git a/packages/desktop/src/window.ts b/packages/desktop/src/window.ts index 6cc06468..936891b6 100644 --- a/packages/desktop/src/window.ts +++ b/packages/desktop/src/window.ts @@ -11,7 +11,7 @@ import {initialWindowBounds, keepWindowBounds} from './window-state'; // The window: the web app, and in its place, when there is no one signed in or no connection, // one of two pages of the app's own. Links out of the app, and anything but the app's own -// origin, open in the browser. +// origin, open in the browser, a page of the app's that sends the window elsewhere included. // What the splash pages' buttons ask: intercepted as navigations, never loaded. const SIGN_IN_URL = 'personalagentproxy-desktop://sign-in'; @@ -57,6 +57,15 @@ function isLoginPage(url: URL): boolean { return url.origin === APP_ORIGIN && url.pathname === '/login'; } +// A page of the app's for the browser to run instead, as the web app has it run a service's +// sign-in (`onSignInClick` there): through the login page, which sends a signed-in browser +// straight on, and marked `?desktop=1` so it ends back in the app. +function inBrowser(url: URL): string { + const page = new URL(url); + page.searchParams.set('desktop', '1'); + return `${APP_URL}/login?callbackUrl=${encodeURIComponent(`${page.pathname}${page.search}`)}`; +} + // Whether Plaid Link is open on the page: one of the page's frames is Plaid's. function hasPlaidLinkFrame(win: BrowserWindow): boolean { return win.webContents.mainFrame.framesInSubtree.some( @@ -130,6 +139,9 @@ export function createMainWindow(): BrowserWindow { }); }); + // The page the window is on its way to, should it send the window on elsewhere. + let navigatingTo: URL | null = null; + win.webContents.on('will-navigate', (event, raw) => { if (raw === SIGN_IN_URL) { event.preventDefault(); @@ -158,12 +170,35 @@ export function createMainWindow(): BrowserWindow { if (!isAllowed(url)) { event.preventDefault(); openExternal(raw); + return; } + + navigatingTo = url; + }); + + // A page of the app's that sends the window on to another origin, as the api's sign-in starts + // send it to a provider's page, which would turn the window away as Google's does: the browser + // gets the page the window was headed to, and runs it as the web app's own sign-in links are + // run. The web app hands those to the browser itself; this catches a link it did not. + win.webContents.on('will-redirect', (details) => { + const target = urlOf(details.url); + if (!details.isMainFrame || !target || isAllowed(target)) { + return; + } + + details.preventDefault(); + if (navigatingTo?.origin === APP_ORIGIN) { + openExternal(inBrowser(navigatingTo)); + return; + } + + openExternal(details.url); }); // The app sends a signed-out person to its login page from within, which is no navigation to // intercept, so where the window ends up is watched too. const onArrived = (_event: Electron.Event, raw: string) => { + navigatingTo = null; const url = urlOf(raw); if (url && isLoginPage(url)) { showSignIn(win); @@ -177,6 +212,7 @@ export function createMainWindow(): BrowserWindow { return; } + navigatingTo = null; // ERR_ABORTED: a navigation the window cancelled itself. if (errorCode === -3) { return; diff --git a/packages/web/src/components/connect-mcp-form.tsx b/packages/web/src/components/connect-mcp-form.tsx index 30bd24d1..0d3661c4 100644 --- a/packages/web/src/components/connect-mcp-form.tsx +++ b/packages/web/src/components/connect-mcp-form.tsx @@ -31,8 +31,9 @@ function connectErrorMessage(error: FetchError): string { // connected at once, and for one that asks for a sign-in the api begins it and answers the // server's sign-in page, which the browser goes to; it comes back to the new connection, or to the // welcome flow's page for it. In the desktop app, whose window a sign-in page would turn away, the -// form moves to the browser instead, filled in as it was (`?name=&url=`), to be sent again there. -// `cancel` sits beside the button. +// form moves to the browser instead, filled in as it was (`?name=&url=`), to be sent again there, +// marked `?desktop=1` so the sign-in ends back in the app (`/desktop-return`), which then shows the +// new connection in place of this form. `cancel` sits beside the button. export function ConnectMcpForm({ cancel, returnTo, @@ -50,8 +51,11 @@ export function ConnectMcpForm({ const [name, setName] = useState(params.get('name') ?? ''); const [url, setUrl] = useState(params.get('url') ?? ''); const [token, setToken] = useState(''); + // Sent again from the browser for the desktop app. + const fromDesktop = params.get('desktop') === '1'; const [error, setError] = useState(null); - const [pending, setPending] = useState<'connecting' | 'signingIn' | 'browser' | null>(null); + const [notice, setNotice] = useState(null); + const [pending, setPending] = useState<'connecting' | 'signingIn' | null>(null); const ready = name.trim() !== '' && url.trim() !== ''; const form = useCommandEnterSubmit(ready && pending === null); @@ -63,12 +67,14 @@ export function ConnectMcpForm({ event.preventDefault(); setPending('connecting'); setError(null); + setNotice(null); const result = await connectMcp(org, { name: name.trim(), url: url.trim(), ...(token.trim() ? {token: token.trim()} : {}), ...(returnTo ? {returnTo} : {}), ...(shared ? {shared} : {}), + ...(fromDesktop ? {desktop: true} : {}), }); if (result.isErr()) { setPending(null); @@ -77,15 +83,16 @@ export function ConnectMcpForm({ } if (result.value.kind === 'signIn') { if (isDesktopApp()) { - setPending('browser'); const here = new URL(window.location.href); here.searchParams.set('name', name.trim()); here.searchParams.set('url', url.trim()); - const handed = await handOffToBrowser(`${here.pathname}${here.search}`); - if (handed.isErr()) { - setPending(null); - setError('Could not open your browser. Try again.'); - } + here.searchParams.set('desktop', '1'); + handOffToBrowser(`${here.pathname}${here.search}`); + // The form stays as it is, to be sent again should the browser not come back. + setPending(null); + setNotice( + 'Sign in to the server in your browser. The connection shows here once you have.', + ); return; } // Only ever a web address: the api has checked it, and the browser goes nowhere else. @@ -146,16 +153,15 @@ export function ConnectMcpForm({

{error &&

{error}

} + {notice &&

{notice}

}
{cancel}
diff --git a/packages/web/src/components/connection-access.tsx b/packages/web/src/components/connection-access.tsx index 3dafb50c..0ca1f7fb 100644 --- a/packages/web/src/components/connection-access.tsx +++ b/packages/web/src/components/connection-access.tsx @@ -375,7 +375,7 @@ export function SignInAgain({ if (reason === 'renew') { return (

- + Sign in again {useKey && <>, or {useKey}}, keeping every agent's settings. @@ -385,7 +385,7 @@ export function SignInAgain({ return (

Some actions weren't allowed when this account was signed in.{' '} - + Sign in again {' '} to allow them. From 19d8541ab659182172586ed7c5a9b22eb7facd69 Mon Sep 17 00:00:00 2001 From: arnestrickmann <115920878+arnestrickmann@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:10:16 -0800 Subject: [PATCH 8/8] sign and notarize desktop releases --- .github/workflows/desktop.yml | 60 +++++++++++++++++++++++---- packages/desktop/README.md | 45 +++++++++++++++++--- packages/desktop/electron-builder.yml | 7 ++-- packages/desktop/package.json | 1 + 4 files changed, 97 insertions(+), 16 deletions(-) diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index ac70c7a7..f7581d76 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -4,10 +4,9 @@ name: desktop # Intel and published as the release for the tag, with latest-mac.yml, the feed the app checks the # repository's latest release for. Pull requests touching the app only build it. # -# Signed and notarized once the secrets are there: CSC_LINK (the Developer ID certificate as a -# base64 .p12) and CSC_KEY_PASSWORD, and APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and APPLE_TEAM_ID for -# notarization. Without them the build is unsigned, which macOS warns about on first open, and the -# app can't update itself. +# Pull requests build an unsigned artifact. Tagged releases require a Developer ID Application +# certificate and App Store Connect API key, then verify the signature and stapled notarization +# ticket before anything is published. See packages/desktop/README.md for the one-time setup. on: push: @@ -37,13 +36,58 @@ jobs: echo "::error::The tag is ${GITHUB_REF_NAME} but packages/desktop/package.json says ${version}." exit 1 fi - - run: bun run --cwd packages/desktop package + - if: github.event_name == 'pull_request' + name: Build the unsigned pull-request artifact + run: bun run --cwd packages/desktop package + - if: startsWith(github.ref, 'refs/tags/desktop-v') + name: Require Apple release credentials + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: | + missing=() + for name in CSC_LINK CSC_KEY_PASSWORD APPLE_API_KEY_BASE64 APPLE_API_KEY_ID APPLE_API_ISSUER; do + if [[ -z "${!name}" ]]; then + missing+=("${name}") + fi + done + if (( ${#missing[@]} > 0 )); then + echo "::error::Missing required release secrets: ${missing[*]}" + exit 1 + fi + - if: startsWith(github.ref, 'refs/tags/desktop-v') + name: Prepare the notarization key + env: + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + run: | + umask 077 + printf '%s' "${APPLE_API_KEY_BASE64}" | base64 --decode > "${RUNNER_TEMP}/AuthKey.p8" + - if: startsWith(github.ref, 'refs/tags/desktop-v') + name: Build, sign and notarize the release env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_API_KEY: ${{ runner.temp }}/AuthKey.p8 + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: bun run --cwd packages/desktop package:release + - if: startsWith(github.ref, 'refs/tags/desktop-v') + name: Verify the signatures and notarization tickets + run: | + app_count=0 + while IFS= read -r -d '' app; do + codesign --verify --deep --strict --verbose=2 "${app}" + xcrun stapler validate "${app}" + spctl --assess --type exec --verbose=2 "${app}" + app_count=$((app_count + 1)) + done < <(find packages/desktop/release -type d -name '*.app' -prune -print0) + if [[ "${app_count}" -ne 2 ]]; then + echo "::error::Expected two signed apps, found ${app_count}." + exit 1 + fi - if: startsWith(github.ref, 'refs/tags/desktop-v') name: Publish the release env: diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 8c5852bb..48017697 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -48,13 +48,48 @@ bun run --cwd packages/desktop package ``` Builds the macOS app for Apple silicon and Intel into `release/`: a disk image each to install -from, a zip each for updates, and `latest-mac.yml`, the update feed (`electron-builder.yml`). -Without Apple's certificate the app is unsigned, which macOS warns about on first open, and -can't update itself. +from, a zip each for updates, and `latest-mac.yml`, the update feed (`electron-builder.yml`). A +local build uses a Developer ID identity from the Mac's Keychain when one is available, but it is +not notarized without separate Apple credentials and is not a distributable release. A release is a `desktop-v` tag, with the version in `package.json`: `.github/workflows/desktop.yml` builds on it and publishes the files as a GitHub release. The app checks the repository's latest release for `latest-mac.yml` (`src/updates.ts`), so the desktop app's releases are the repository's releases; the Docker image has tags, not releases. Signing -and notarization start once the workflow has `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_ID`, -`APPLE_APP_SPECIFIC_PASSWORD` and `APPLE_TEAM_ID` as secrets. +and notarization are mandatory for a tagged release. The workflow refuses to publish unless it +can verify both architectures' signatures and stapled notarization tickets. + +### One-time Apple and GitHub setup + +1. Join or renew the Apple Developer Program for the team that will publish Personal Agent Proxy. +2. Use a valid **Developer ID Application** certificate for the team (not an Apple Distribution or + Developer ID Installer certificate), or have the Apple team's Account Holder create one. On the + Mac that holds its private key, export the certificate and private key from **My Certificates** + in Keychain Access as a password-protected `.p12` file. +3. Base64-encode the `.p12` without line breaks: + + ```bash + base64 -i developer-id-application.p12 | tr -d '\n' | pbcopy + ``` + +4. In App Store Connect, open **Users and Access → Integrations → App Store Connect API**, create a + team key with the **Developer** role, download its `.p8` file, and record its Key ID and Issuer + ID. Apple only offers the private key for download once. +5. Base64-encode the `.p8` without line breaks: + + ```bash + base64 -i AuthKey_KEYID.p8 | tr -d '\n' | pbcopy + ``` + +6. Add these repository Actions secrets in GitHub under **Settings → Secrets and variables → + Actions**: + + - `CSC_LINK`: the base64-encoded `.p12` + - `CSC_KEY_PASSWORD`: the `.p12` export password + - `APPLE_API_KEY_BASE64`: the base64-encoded `.p8` + - `APPLE_API_KEY_ID`: the App Store Connect key ID + - `APPLE_API_ISSUER`: the App Store Connect issuer ID + +The certificate and private key files must never be committed. Keep encrypted backups: the `.p8` +cannot be downloaded again, and losing the `.p12` private key means a new Developer ID certificate +is needed for future releases. diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 3ef67d69..1175ba24 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -23,9 +23,10 @@ publish: mac: category: public.app-category.productivity icon: build/icon.png - # Signed and notarized once the workflow has Apple's certificate (CSC_LINK) and credentials; - # until then the build is unsigned, which macOS warns about on first open and electron-updater - # cannot update. + # Pull-request builds have no Apple credentials and remain unsigned. A local build can use an + # identity from the Mac's Keychain, but only tagged releases require both signing and + # notarization and verify the signature and stapled ticket before publishing. + notarize: true hardenedRuntime: true entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 46f743bf..810af23b 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -11,6 +11,7 @@ "desktop": "bun run dev.ts", "build": "bun run build.ts", "package": "NODE_ENV=production bun run build.ts && electron-builder --mac --publish never", + "package:release": "NODE_ENV=production bun run build.ts && electron-builder --mac --publish never --config.mac.forceCodeSigning=true", "lint": "oxlint", "test": "bun test", "typecheck": "tsc --noEmit"