From f707e3f3f398eae1938a826ed0578b5aaef6b84e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Apr 2026 14:34:19 +0000 Subject: [PATCH 1/3] Remove secret approval request tokens Co-authored-by: Kent C. Dodds --- e2e/account-secrets.spec.ts | 85 +----- .../client/routes/account-approval-shared.ts | 8 +- .../worker/client/routes/account-secrets.tsx | 6 +- packages/worker/client/routes/account.tsx | 2 +- .../app/handlers/account-secrets.node.test.ts | 266 ++++++++---------- .../src/app/handlers/account-secrets.ts | 140 ++++----- packages/worker/src/mcp/executor.node.test.ts | 10 +- .../worker/src/mcp/fetch-gateway.node.test.ts | 2 +- packages/worker/src/mcp/fetch-gateway.ts | 15 +- .../src/mcp/secrets/errors.node.test.ts | 4 +- .../mcp/secrets/host-approval.node.test.ts | 101 ------- .../worker/src/mcp/secrets/host-approval.ts | 88 ------ .../src/mcp/secrets/package-approval-url.ts | 4 - .../src/mcp/secrets/package-approval.ts | 117 -------- 14 files changed, 196 insertions(+), 652 deletions(-) delete mode 100644 packages/worker/src/mcp/secrets/host-approval.node.test.ts delete mode 100644 packages/worker/src/mcp/secrets/package-approval.ts diff --git a/e2e/account-secrets.spec.ts b/e2e/account-secrets.spec.ts index 45f521b582..d4dff3abd8 100644 --- a/e2e/account-secrets.spec.ts +++ b/e2e/account-secrets.spec.ts @@ -1,8 +1,5 @@ import { expect, test } from './playwright-utils.ts' -const e2eCookieSecret = 'LOCAL_AND_PREVIEW_COOKIE_SECRET_32_CHARS_MINIMUM' -const secretHostApprovalPurpose = 'secret-host-approval' - async function saveSecret( page: Parameters[1]>[0]['page'], input: { @@ -29,75 +26,6 @@ async function saveSecret( expect(response.ok()).toBeTruthy() } -function bytesToBase64Url(bytes: Uint8Array) { - let binary = '' - for (const value of bytes) { - binary += String.fromCharCode(value) - } - return btoa(binary) - .replaceAll('+', '-') - .replaceAll('/', '_') - .replaceAll('=', '') -} - -async function deriveEncryptionKey(cookieSecret: string, purpose: string) { - const digest = await crypto.subtle.digest( - 'SHA-256', - new TextEncoder().encode(`${purpose}:${cookieSecret}`), - ) - return crypto.subtle.importKey('raw', digest, 'AES-GCM', false, [ - 'encrypt', - 'decrypt', - ]) -} - -async function encryptStringWithPurpose(purpose: string, value: string) { - const key = await deriveEncryptionKey(e2eCookieSecret, purpose) - const iv = crypto.getRandomValues(new Uint8Array(12)) - const ciphertext = await crypto.subtle.encrypt( - { - name: 'AES-GCM', - iv, - }, - key, - new TextEncoder().encode(value), - ) - return `${bytesToBase64Url(iv)}.${bytesToBase64Url( - new Uint8Array(ciphertext), - )}` -} - -async function createHostApprovalToken(input: { - userId: string - name: string - requestedHost: string -}) { - const now = Date.now() - const token = await encryptStringWithPurpose( - secretHostApprovalPurpose, - JSON.stringify({ - kind: 'host', - userId: input.userId, - name: input.name, - scope: 'user', - requestedHost: input.requestedHost, - storageContext: null, - iat: now, - exp: now + 60_000, - }), - ) - return `host:${token}` -} - -async function createStableUserIdFromEmail(email: string) { - const normalized = email.trim().toLowerCase() - const data = new TextEncoder().encode(normalized) - const hash = await crypto.subtle.digest('SHA-256', data) - return Array.from(new Uint8Array(hash), (byte) => - byte.toString(16).padStart(2, '0'), - ).join('') -} - test('switching secrets updates detail view without a full reload', async ({ page, login, @@ -172,7 +100,7 @@ test('landing on an approval link shows already added when the host is present', await saveSecret(page, secret) await page.goto( - `/account/secrets/user/${secret.name}?allowed-host=api.cloudflare.com&request=stale-token`, + `/account/secrets/user/${secret.name}?allowed-host=api.cloudflare.com`, ) await expect( @@ -188,7 +116,7 @@ test('generated host approval link shows one-click approve and persists host', a page, login, }) => { - const user = await login() + await login() const nonce = Date.now().toString(36) const secret = { name: `fly-token-${nonce}`, @@ -198,15 +126,8 @@ test('generated host approval link shows one-click approve and persists host', a await saveSecret(page, secret) const requestedHost = 'api.fly.io' - const token = await createHostApprovalToken({ - userId: await createStableUserIdFromEmail(user.email), - name: secret.name, - requestedHost, - }) await page.goto( - `/account/secrets/user/${secret.name}?allowed-host=${requestedHost}&request=${encodeURIComponent( - token, - )}`, + `/account/secrets/user/${secret.name}?allowed-host=${requestedHost}`, ) const approvalCard = page.getByRole('heading', { diff --git a/packages/worker/client/routes/account-approval-shared.ts b/packages/worker/client/routes/account-approval-shared.ts index 94c8a78a98..cda210ec32 100644 --- a/packages/worker/client/routes/account-approval-shared.ts +++ b/packages/worker/client/routes/account-approval-shared.ts @@ -3,7 +3,6 @@ export type ApprovalAction = 'approve' | 'reject' export type ApprovalScope = 'session' | 'app' | 'user' export type ApprovalView = { - token: string name: string scope: ApprovalScope requestedHost: string @@ -27,11 +26,7 @@ export async function readJson(response: Response) { export async function submitApprovalRequest< T extends { ok?: boolean; error?: string }, ->( - action: ApprovalAction, - requestToken: string, - requestUrl = accountSecretsApiPath, -) { +>(action: ApprovalAction, requestUrl = accountSecretsApiPath) { const response = await fetch(requestUrl, { method: 'POST', headers: { @@ -41,7 +36,6 @@ export async function submitApprovalRequest< credentials: 'include', body: JSON.stringify({ action, - requestToken, }), }) if (response.status === 401) { diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index b36b87a490..a567eaca12 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -381,11 +381,10 @@ function replaceSecretsLocation(to: string) { function getDataRefreshKey(href: string) { const url = new URL(href, 'http://localhost') - const request = url.searchParams.get('request') ?? '' const requestedHost = url.searchParams.get('allowed-host') ?? '' const requestedCapability = url.searchParams.get('capability') ?? '' const requestedPackageId = url.searchParams.get('package_id') ?? '' - return `${url.pathname}?request=${request}&allowed-host=${requestedHost}&capability=${requestedCapability}&package_id=${requestedPackageId}` + return `${url.pathname}?allowed-host=${requestedHost}&capability=${requestedCapability}&package_id=${requestedPackageId}` } function readFilterState( @@ -635,7 +634,7 @@ export function AccountSecretsRoute(handle: Handle) { } const payload = await submitApprovalRequest< AccountSecretsPayload & { error?: string; ok?: boolean } - >(action, approval.token, requestUrl.toString()) + >(action, requestUrl.toString()) if (!payload) return applyPayload( @@ -665,7 +664,6 @@ export function AccountSecretsRoute(handle: Handle) { ) : buildHrefWithUpdatedFilters({}, { pathname: secretsBasePath }) const nextUrl = new URL(nextHref, window.location.href) - nextUrl.searchParams.delete('request') nextUrl.searchParams.delete('allowed-host') nextUrl.searchParams.delete('capability') nextUrl.searchParams.delete('package_id') diff --git a/packages/worker/client/routes/account.tsx b/packages/worker/client/routes/account.tsx index 15f39f6eb8..a6c0973a28 100644 --- a/packages/worker/client/routes/account.tsx +++ b/packages/worker/client/routes/account.tsx @@ -76,7 +76,7 @@ export function AccountRoute(handle: Handle) { try { const payload = await submitApprovalRequest< AccountSecretsPayload & { error?: string; ok?: boolean } - >(action, approval.token) + >(action) if (!payload) return email = payload.email approval = payload.approval diff --git a/packages/worker/src/app/handlers/account-secrets.node.test.ts b/packages/worker/src/app/handlers/account-secrets.node.test.ts index caaddee056..3d31e7693b 100644 --- a/packages/worker/src/app/handlers/account-secrets.node.test.ts +++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts @@ -28,14 +28,9 @@ const mockModule = vi.hoisted(() => ({ })), setSecretAllowedHosts: vi.fn(async () => undefined), saveValue: vi.fn(async () => undefined), - createSecretHostApprovalToken: vi.fn( - async (_env: unknown, input: { name: string; requestedHost: string }) => { - return `token:${input.name}:${input.requestedHost}` - }, - ), buildSecretHostApprovalUrl: vi.fn( - (input: { name: string; requestedHost: string; token: string }) => - `https://example.com/account/secrets/user/${input.name}?allowed-host=${input.requestedHost}&request=${input.token}`, + (input: { name: string; requestedHost: string }) => + `https://example.com/account/secrets/user/${input.name}?allowed-host=${input.requestedHost}`, ), listSavedPackagesByUserId: vi.fn(async () => []), listSecrets: vi.fn(async () => []), @@ -45,12 +40,6 @@ const mockModule = vi.hoisted(() => ({ setSecretAllowedCapabilities: vi.fn(async () => undefined), setSecretAllowedPackages: vi.fn(async () => undefined), getValue: vi.fn(async () => null), - verifySecretHostApprovalToken: vi.fn(async () => { - throw new Error('not used') - }), - verifySecretPackageApprovalToken: vi.fn(async () => { - throw new Error('not used') - }), })) vi.mock('#app/authenticated-user.ts', () => ({ @@ -91,19 +80,8 @@ vi.mock('#mcp/secrets/allowed-capabilities.ts', () => ({ })) vi.mock('#mcp/secrets/host-approval.ts', () => ({ - createSecretHostApprovalToken: (...args: Array) => - mockModule.createSecretHostApprovalToken(...args), buildSecretHostApprovalUrl: (...args: Array) => mockModule.buildSecretHostApprovalUrl(...args), - verifySecretHostApprovalToken: (...args: Array) => - mockModule.verifySecretHostApprovalToken(...args), - secretHostApprovalTokenPrefix: 'host:', -})) - -vi.mock('#mcp/secrets/package-approval.ts', () => ({ - verifySecretPackageApprovalToken: (...args: Array) => - mockModule.verifySecretPackageApprovalToken(...args), - secretPackageApprovalTokenPrefix: 'pkg:', })) vi.mock('#mcp/secrets/service.ts', () => ({ @@ -176,52 +154,30 @@ test('connect oauth returns direct host approval links for saved token secrets', secretName: 'githubAccessToken', host: 'api.github.com', approvalUrl: - 'https://example.com/account/secrets/user/githubAccessToken?allowed-host=api.github.com&request=token:githubAccessToken:api.github.com', + 'https://example.com/account/secrets/user/githubAccessToken?allowed-host=api.github.com', }, { secretName: 'githubAccessToken', host: 'github.com', approvalUrl: - 'https://example.com/account/secrets/user/githubAccessToken?allowed-host=github.com&request=token:githubAccessToken:github.com', + 'https://example.com/account/secrets/user/githubAccessToken?allowed-host=github.com', }, { secretName: 'githubRefreshToken', host: 'api.github.com', approvalUrl: - 'https://example.com/account/secrets/user/githubRefreshToken?allowed-host=api.github.com&request=token:githubRefreshToken:api.github.com', + 'https://example.com/account/secrets/user/githubRefreshToken?allowed-host=api.github.com', }, { secretName: 'githubRefreshToken', host: 'github.com', approvalUrl: - 'https://example.com/account/secrets/user/githubRefreshToken?allowed-host=github.com&request=token:githubRefreshToken:github.com', + 'https://example.com/account/secrets/user/githubRefreshToken?allowed-host=github.com', }, ], connectorName: 'GitHub', }) - expect(mockModule.createSecretHostApprovalToken).toHaveBeenCalledTimes(4) - expect(mockModule.createSecretHostApprovalToken).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ COOKIE_SECRET: 'secret' }), - expect.objectContaining({ - userId: 'stable-user-1', - name: 'githubAccessToken', - scope: 'user', - requestedHost: 'api.github.com', - storageContext: null, - }), - ) - expect(mockModule.createSecretHostApprovalToken).toHaveBeenNthCalledWith( - 4, - expect.objectContaining({ COOKIE_SECRET: 'secret' }), - expect.objectContaining({ - userId: 'stable-user-1', - name: 'githubRefreshToken', - scope: 'user', - requestedHost: 'github.com', - storageContext: null, - }), - ) + expect(mockModule.buildSecretHostApprovalUrl).toHaveBeenCalledTimes(4) expect(mockModule.setSecretAllowedHosts).not.toHaveBeenCalled() }) @@ -302,7 +258,107 @@ test('connect oauth omits direct host approval links when hosts are already appr 'fleet-auth.prd.vn.cloud.tesla.com', ]), ) - expect(mockModule.createSecretHostApprovalToken).not.toHaveBeenCalled() + expect(mockModule.buildSecretHostApprovalUrl).not.toHaveBeenCalled() +}) + +test('host approval view is derived from allowed-host and selected secret', async () => { + mockModule.listSecrets.mockResolvedValueOnce([ + { + name: 'cloudflareToken', + scope: 'user', + description: 'Cloudflare token', + appId: null, + allowedHosts: [], + allowedCapabilities: [], + allowedPackages: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + ttlMs: null, + }, + ]) + mockModule.resolveSecret.mockResolvedValueOnce({ + found: true, + value: 'secret-value', + }) + mockModule.listSecrets.mockResolvedValueOnce([ + { + name: 'cloudflareToken', + scope: 'user', + description: 'Cloudflare token', + appId: null, + allowedHosts: [], + allowedCapabilities: [], + allowedPackages: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + ttlMs: null, + }, + ]) + + const handler = createAccountSecretsApiHandler(createEnv()) + const response = await handler.action({ + request: new Request( + 'https://example.com/account/secrets.json?selected=user::::cloudflareToken&allowed-host=API.Cloudflare.com', + { method: 'GET' }, + ), + params: {}, + } as never) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toMatchObject({ + ok: true, + approval: { + name: 'cloudflareToken', + scope: 'user', + requestedHost: 'api.cloudflare.com', + requestedPackageId: null, + currentAllowedHosts: [], + }, + }) +}) + +test('host approval approve persists host from allowed-host and selected secret', async () => { + mockModule.listSecrets.mockResolvedValueOnce([ + { + name: 'cloudflareToken', + scope: 'user', + description: 'Cloudflare token', + appId: null, + allowedHosts: ['api.github.com'], + allowedCapabilities: [], + allowedPackages: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + ttlMs: null, + }, + ]) + mockModule.listSecrets.mockResolvedValueOnce([]) + mockModule.listSavedPackagesByUserId.mockResolvedValueOnce([]) + mockModule.listAppSecretsByAppIds.mockResolvedValueOnce(new Map()) + + const handler = createAccountSecretsApiHandler(createEnv()) + const response = await handler.action({ + request: new Request( + 'https://example.com/account/secrets.json?selected=user::::cloudflareToken&allowed-host=API.Cloudflare.com', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'approve' }), + }, + ), + params: {}, + } as never) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toMatchObject({ ok: true }) + expect(mockModule.setSecretAllowedHosts).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'cloudflareToken', + scope: 'user', + allowedHosts: ['api.github.com', 'api.cloudflare.com'], + storageContext: { appId: null, sessionId: null }, + }), + ) }) test('account secrets payload preserves app titles and allowed packages', async () => { @@ -396,84 +452,21 @@ test('account secrets payload preserves app titles and allowed packages', async }) }) -test('package approval expiry surfaces package-token error without host fallback', async () => { - mockModule.verifySecretPackageApprovalToken.mockRejectedValueOnce( - new Error('Secret package approval request has expired.'), - ) - - const handler = createAccountSecretsApiHandler(createEnv()) - const response = await handler.action({ - request: new Request('https://example.com/account/secrets.json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'approve', - requestToken: 'expired-package-token', - }), - }), - params: {}, - } as never) - - expect(response.status).toBe(400) - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error: 'Secret package approval request has expired.', - }) - expect(mockModule.verifySecretHostApprovalToken).not.toHaveBeenCalled() -}) - -test('malformed package approval token surfaces package-token error without host fallback', async () => { - mockModule.verifySecretPackageApprovalToken.mockRejectedValueOnce( - new Error('Invalid secret package approval request.'), - ) - - const handler = createAccountSecretsApiHandler(createEnv()) - const response = await handler.action({ - request: new Request('https://example.com/account/secrets.json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'approve', - requestToken: 'pkg:invalid-package-token', - }), - }), - params: {}, - } as never) - - expect(response.status).toBe(400) - await expect(response.json()).resolves.toMatchObject({ - ok: false, - error: 'Invalid secret package approval request.', - }) - expect(mockModule.verifySecretHostApprovalToken).not.toHaveBeenCalled() -}) - test('package approval reject succeeds even when the secret no longer exists', async () => { - mockModule.verifySecretPackageApprovalToken.mockResolvedValueOnce({ - kind: 'package', - userId: 'stable-user-1', - name: 'discordBotToken', - scope: 'user', - packageId: 'pkg-allowed', - packageKodyId: 'discord-gateway', - storageContext: null, - iat: Date.now(), - exp: Date.now() + 60_000, - }) mockModule.listSavedPackagesByUserId.mockResolvedValueOnce([]) mockModule.listSecrets.mockResolvedValueOnce([]) mockModule.listAppSecretsByAppIds.mockResolvedValueOnce(new Map()) const handler = createAccountSecretsApiHandler(createEnv()) const response = await handler.action({ - request: new Request('https://example.com/account/secrets.json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'reject', - requestToken: 'package-token', - }), - }), + request: new Request( + 'https://example.com/account/secrets.json?selected=user::::discordBotToken&package_id=pkg-allowed', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'reject' }), + }, + ), params: {}, } as never) @@ -487,17 +480,6 @@ test('package approval reject succeeds even when the secret no longer exists', a }) test('package approval approve deduplicates allowed package ids', async () => { - mockModule.verifySecretPackageApprovalToken.mockResolvedValueOnce({ - kind: 'package', - userId: 'stable-user-1', - name: 'discordBotToken', - scope: 'user', - packageId: 'pkg-new', - packageKodyId: 'discord-gateway', - storageContext: null, - iat: Date.now(), - exp: Date.now() + 60_000, - }) mockModule.listSecrets.mockResolvedValueOnce([ { name: 'discordBotToken', @@ -518,14 +500,14 @@ test('package approval approve deduplicates allowed package ids', async () => { const handler = createAccountSecretsApiHandler(createEnv()) const response = await handler.action({ - request: new Request('https://example.com/account/secrets.json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'approve', - requestToken: 'package-token', - }), - }), + request: new Request( + 'https://example.com/account/secrets.json?selected=user::::discordBotToken&package_id=pkg-new', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'approve' }), + }, + ), params: {}, } as never) diff --git a/packages/worker/src/app/handlers/account-secrets.ts b/packages/worker/src/app/handlers/account-secrets.ts index 7036b3c165..4d5be39d8f 100644 --- a/packages/worker/src/app/handlers/account-secrets.ts +++ b/packages/worker/src/app/handlers/account-secrets.ts @@ -10,16 +10,7 @@ import { redirectToLogin } from '#app/auth-redirect.ts' import { Layout } from '#app/layout.ts' import { render } from '#app/render.ts' import { type StorageContext } from '#mcp/storage.ts' -import { - buildSecretHostApprovalUrl, - createSecretHostApprovalToken, - secretHostApprovalTokenPrefix, - verifySecretHostApprovalToken, -} from '#mcp/secrets/host-approval.ts' -import { - secretPackageApprovalTokenPrefix, - verifySecretPackageApprovalToken, -} from '#mcp/secrets/package-approval.ts' +import { buildSecretHostApprovalUrl } from '#mcp/secrets/host-approval.ts' import { deleteSecret, listAppSecretsByAppIds, @@ -78,7 +69,6 @@ type AccountSecretDetail = AccountSecretListItem & { } type SecretApprovalView = { - token: string name: string scope: SecretScope requestedHost: string @@ -170,7 +160,6 @@ export function createAccountSecretsApiHandler(env: Env) { env, user, action, - body, }) } if (action === 'save') { @@ -457,19 +446,11 @@ async function buildConnectOauthHostApprovalLinks(input: { if (approvedHostsBySecretName.get(secretName)?.has(host)) { return null } - const token = await createSecretHostApprovalToken(input.env, { - userId: input.userId, - name: secretName, - scope: 'user', - requestedHost: host, - storageContext: null, - }) return { secretName, host, approvalUrl: buildSecretHostApprovalUrl({ baseUrl, - token, name: secretName, scope: 'user', requestedHost: host, @@ -638,7 +619,6 @@ async function buildAccountSecretsPayload(input: { savedPackages?: Array }): Promise { const url = new URL(input.request.url) - const approvalToken = url.searchParams.get('request') const requestedApprovalHost = readApprovalHost(url) const requestedCapability = readRequestedCapability(url) const requestedPackageId = readRequestedPackageId(url) @@ -665,12 +645,12 @@ async function buildAccountSecretsPayload(input: { let approval: SecretApprovalView | null = null let approvalError: string | null = null - if (approvalToken) { + if (input.selectedSecretId && (requestedApprovalHost || requestedPackageId)) { try { approval = await resolveSecretApprovalView({ env: input.env, userId: input.user.mcpUser.userId, - token: approvalToken, + secretId: input.selectedSecretId, requestedHost: requestedApprovalHost, requestedCapability, requestedPackageId, @@ -679,7 +659,7 @@ async function buildAccountSecretsPayload(input: { approvalError = error instanceof Error ? error.message - : 'Unable to verify approval request.' + : 'Unable to read approval request.' } } @@ -733,62 +713,65 @@ async function listAccountSecrets(input: { } type ResolvedSecretApproval = - | Awaited> - | Awaited> - -function isExpiredPackageApprovalError(error: unknown) { - return ( - error instanceof Error && - error.message === 'Secret package approval request has expired.' - ) -} - -async function resolveApprovalRequest( - env: Env, - token: string, -): Promise { - if (token.startsWith(secretHostApprovalTokenPrefix)) { - return await verifySecretHostApprovalToken(env, token) + | { + kind: 'host' + name: string + scope: SecretScope + requestedHost: string + storageContext: StorageContext | null + } + | { + kind: 'package' + name: string + scope: SecretScope + packageId: string + storageContext: StorageContext | null + } + +function resolveApprovalRequest(input: { + secretId: string | null + requestedHost: string | null + requestedPackageId: string | null +}): ResolvedSecretApproval { + const parsed = input.secretId ? parseAccountSecretId(input.secretId) : null + if (!parsed) { + throw new Error('Invalid approval request.') } - if (token.startsWith(secretPackageApprovalTokenPrefix)) { - return await verifySecretPackageApprovalToken(env, token) + const storageContext = getSecretContextForAccountSecret(parsed) + if (input.requestedPackageId) { + return { + kind: 'package', + name: parsed.name, + scope: parsed.scope, + packageId: input.requestedPackageId, + storageContext, + } } - try { - return await verifySecretPackageApprovalToken(env, token) - } catch (error) { - if (isExpiredPackageApprovalError(error)) { - throw error + if (input.requestedHost) { + const [requestedHost] = normalizeAllowedHosts([input.requestedHost]) + if (!requestedHost) { + throw new Error('Invalid approval request host.') + } + return { + kind: 'host', + name: parsed.name, + scope: parsed.scope, + requestedHost, + storageContext, } } - return await verifySecretHostApprovalToken(env, token) + throw new Error('Approval request is missing a host or package.') } async function resolveSecretApprovalView(input: { env: Env userId: string - token: string + secretId: string requestedHost: string | null requestedCapability: string | null requestedPackageId: string | null }) { - const approval = await resolveApprovalRequest(input.env, input.token) - if (approval.userId !== input.userId) { - throw new Error('Approval request mismatch.') - } - if ( - approval.kind === 'host' && - input.requestedHost != null && - approval.requestedHost !== normalizeAllowedHosts([input.requestedHost])[0] - ) { - throw new Error('Approval request host mismatch.') - } - if ( - approval.kind === 'package' && - input.requestedPackageId != null && - approval.packageId !== input.requestedPackageId - ) { - throw new Error('Approval request package mismatch.') - } + const approval = resolveApprovalRequest(input) const secrets = await listSecrets({ env: input.env, userId: input.userId, @@ -802,7 +785,6 @@ async function resolveSecretApprovalView(input: { throw new Error('Secret not found.') } return { - token: input.token, name: approval.name, scope: approval.scope, requestedHost: approval.kind === 'host' ? approval.requestedHost : '', @@ -927,24 +909,14 @@ async function handleApprovalAction(input: { env: Env user: NonNullable>> action: SecretApprovalAction - body: object }) { - const token = readString(input.body, 'requestToken') - if (!token) { - return jsonResponse( - { ok: false, error: 'Approval request token is required.' }, - 400, - ) - } - try { - const approval = await resolveApprovalRequest(input.env, token) - if (approval.userId !== input.user.mcpUser.userId) { - return jsonResponse( - { ok: false, error: 'Approval request mismatch.' }, - 403, - ) - } + const url = new URL(input.request.url) + const approval = resolveApprovalRequest({ + secretId: readSelectedSecretId(input.request), + requestedHost: readApprovalHost(url), + requestedPackageId: readRequestedPackageId(url), + }) if (approval.kind === 'package') { if (input.action === 'approve') { diff --git a/packages/worker/src/mcp/executor.node.test.ts b/packages/worker/src/mcp/executor.node.test.ts index deed9bc62e..7677c85745 100644 --- a/packages/worker/src/mcp/executor.node.test.ts +++ b/packages/worker/src/mcp/executor.node.test.ts @@ -143,13 +143,13 @@ test('getExecutionErrorDetails returns batch host approvals', () => { secretName: 'cloudflareToken', host: 'api.cloudflare.com', approvalUrl: - 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com&request=token', + 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com', }, { secretName: 'slackToken', host: 'slack.com', approvalUrl: - 'https://example.com/account/secrets/user/slackToken?allowed-host=slack.com&request=token', + 'https://example.com/account/secrets/user/slackToken?allowed-host=slack.com', }, ]), ) @@ -157,7 +157,7 @@ test('getExecutionErrorDetails returns batch host approvals', () => { expect(getExecutionErrorDetails(error)).toEqual({ kind: 'host_approval_required_batch', message: - 'Secrets require host approval: [{"secretName":"cloudflareToken","host":"api.cloudflare.com","approvalUrl":"https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com&request=token"},{"secretName":"slackToken","host":"slack.com","approvalUrl":"https://example.com/account/secrets/user/slackToken?allowed-host=slack.com&request=token"}]', + 'Secrets require host approval: [{"secretName":"cloudflareToken","host":"api.cloudflare.com","approvalUrl":"https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com"},{"secretName":"slackToken","host":"slack.com","approvalUrl":"https://example.com/account/secrets/user/slackToken?allowed-host=slack.com"}]', nextStep: 'Ask the user whether they want to approve these hosts for the listed secrets in the account web UI, then retry after approval.', missingApprovals: [ @@ -165,13 +165,13 @@ test('getExecutionErrorDetails returns batch host approvals', () => { secretName: 'cloudflareToken', host: 'api.cloudflare.com', approvalUrl: - 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com&request=token', + 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com', }, { secretName: 'slackToken', host: 'slack.com', approvalUrl: - 'https://example.com/account/secrets/user/slackToken?allowed-host=slack.com&request=token', + 'https://example.com/account/secrets/user/slackToken?allowed-host=slack.com', }, ], suggestedAction: { diff --git a/packages/worker/src/mcp/fetch-gateway.node.test.ts b/packages/worker/src/mcp/fetch-gateway.node.test.ts index f93e422dec..bb11d86227 100644 --- a/packages/worker/src/mcp/fetch-gateway.node.test.ts +++ b/packages/worker/src/mcp/fetch-gateway.node.test.ts @@ -46,7 +46,7 @@ test('fetch gateway blocks placeholders when allowed hosts are empty', async () secretName: 'spotifyRefreshToken', host: 'example.com', approvalUrl: expect.stringMatching( - /\/account\/secrets\/user\/spotifyRefreshToken\?[^#]*allowed-host=example\.com[^#]*request=/, + /\/account\/secrets\/user\/spotifyRefreshToken\?allowed-host=example\.com$/, ), }), ]) diff --git a/packages/worker/src/mcp/fetch-gateway.ts b/packages/worker/src/mcp/fetch-gateway.ts index fa5052a36d..d08ba0a7ee 100644 --- a/packages/worker/src/mcp/fetch-gateway.ts +++ b/packages/worker/src/mcp/fetch-gateway.ts @@ -1,8 +1,5 @@ import { WorkerEntrypoint } from 'cloudflare:workers' -import { - buildSecretHostApprovalUrl, - createSecretHostApprovalToken, -} from '#mcp/secrets/host-approval.ts' +import { buildSecretHostApprovalUrl } from '#mcp/secrets/host-approval.ts' import { buildSecretPlaceholder, parseSecretPlaceholders, @@ -99,7 +96,6 @@ export async function expandSecretPlaceholders(input: { } const normalizedHost = normalizeHost(requestedHost) const missingApprovals = await collectHostApprovalEntries({ - env: input.env, props: input.props, requestedHost, normalizedHost, @@ -167,7 +163,6 @@ function resolveRequestUrlForFetchGateway(url: string, baseUrl: string) { } async function collectHostApprovalEntries(input: { - env: Pick props: FetchGatewayProps requestedHost: string normalizedHost: string @@ -182,16 +177,8 @@ async function collectHostApprovalEntries(input: { resolved.allowedHosts.length > 0 && resolved.allowedHosts.includes(input.normalizedHost) if (allowedForHost) return null - const approvalToken = await createSecretHostApprovalToken(input.env, { - userId: input.props.userId!, - name: referenced.name, - scope: resolved.scope ?? referenced.scope ?? 'user', - requestedHost: input.requestedHost, - storageContext: input.props.storageContext, - }) const approvalUrl = buildSecretHostApprovalUrl({ baseUrl: input.props.baseUrl, - token: approvalToken, name: referenced.name, scope: resolved.scope ?? referenced.scope ?? 'user', requestedHost: input.requestedHost, diff --git a/packages/worker/src/mcp/secrets/errors.node.test.ts b/packages/worker/src/mcp/secrets/errors.node.test.ts index faaba35bdb..112856a353 100644 --- a/packages/worker/src/mcp/secrets/errors.node.test.ts +++ b/packages/worker/src/mcp/secrets/errors.node.test.ts @@ -92,13 +92,13 @@ test('batch approval parsers round-trip entries and reject invalid payloads', () secretName: 'cloudflareToken', host: 'api.cloudflare.com', approvalUrl: - 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com&request=token', + 'https://example.com/account/secrets/user/cloudflareToken?allowed-host=api.cloudflare.com', }, { secretName: 'githubToken', host: 'api.github.com', approvalUrl: - 'https://example.com/account/secrets/user/githubToken?allowed-host=api.github.com&request=token', + 'https://example.com/account/secrets/user/githubToken?allowed-host=api.github.com', }, ] expect( diff --git a/packages/worker/src/mcp/secrets/host-approval.node.test.ts b/packages/worker/src/mcp/secrets/host-approval.node.test.ts deleted file mode 100644 index 052cd0f2df..0000000000 --- a/packages/worker/src/mcp/secrets/host-approval.node.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { expect, test } from 'vitest' -import { - createSecretHostApprovalToken, - verifySecretHostApprovalToken, -} from './host-approval.ts' -import { encryptStringWithPurpose } from './crypto.ts' - -const env = { - COOKIE_SECRET: 'test-cookie-secret', -} as Pick - -test('verifySecretHostApprovalToken accepts legacy tokens without kind', async () => { - const now = Date.now() - const token = await encryptStringWithPurpose( - env, - 'secret-host-approval', - JSON.stringify({ - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'API.Cloudflare.com', - storageContext: null, - iat: now, - exp: now + 60_000, - }), - ) - - await expect(verifySecretHostApprovalToken(env, token)).resolves.toEqual({ - kind: 'host', - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'api.cloudflare.com', - storageContext: null, - iat: now, - exp: now + 60_000, - }) -}) - -test('verifySecretHostApprovalToken still accepts new tokens with kind', async () => { - const token = await createSecretHostApprovalToken(env, { - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'api.cloudflare.com', - storageContext: null, - }) - - await expect(verifySecretHostApprovalToken(env, token)).resolves.toMatchObject({ - kind: 'host', - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'api.cloudflare.com', - storageContext: null, - }) -}) - -test('verifySecretHostApprovalToken rejects tokens with explicit non-host kind', async () => { - const now = Date.now() - const token = await encryptStringWithPurpose( - env, - 'secret-host-approval', - JSON.stringify({ - kind: 'package', - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'api.cloudflare.com', - storageContext: null, - iat: now, - exp: now + 60_000, - }), - ) - - await expect(verifySecretHostApprovalToken(env, token)).rejects.toThrow( - 'Invalid secret host approval request.', - ) -}) - -test('verifySecretHostApprovalToken rejects tokens with explicit non-host kind', async () => { - const now = Date.now() - const token = await encryptStringWithPurpose( - env, - 'secret-host-approval', - JSON.stringify({ - kind: 'package', - userId: 'user-1', - name: 'cloudflareToken', - scope: 'user', - requestedHost: 'api.cloudflare.com', - storageContext: null, - iat: now, - exp: now + 60_000, - }), - ) - - await expect(verifySecretHostApprovalToken(env, token)).rejects.toThrow( - 'Invalid secret host approval request.', - ) -}) diff --git a/packages/worker/src/mcp/secrets/host-approval.ts b/packages/worker/src/mcp/secrets/host-approval.ts index 7a0a1a5907..2302108c83 100644 --- a/packages/worker/src/mcp/secrets/host-approval.ts +++ b/packages/worker/src/mcp/secrets/host-approval.ts @@ -1,97 +1,10 @@ import { buildAccountSecretPath } from '@kody-internal/shared/account-secret-route.ts' import { type StorageContext } from '#mcp/storage.ts' -import { decryptStringWithPurpose, encryptStringWithPurpose } from './crypto.ts' import { normalizeHost } from './allowed-hosts.ts' import { type SecretScope } from './types.ts' -const secretHostApprovalPurpose = 'secret-host-approval' -const defaultSecretHostApprovalTtlMs = 1000 * 60 * 60 * 24 -export const secretHostApprovalTokenPrefix = 'host:' - -export type SecretHostApprovalRequest = { - kind: 'host' - userId: string - name: string - scope: SecretScope - requestedHost: string - storageContext: StorageContext | null - iat: number - exp: number -} - -export async function createSecretHostApprovalToken( - env: Pick, - input: { - userId: string - name: string - scope: SecretScope - requestedHost: string - storageContext: StorageContext | null - ttlMs?: number - }, -) { - const now = Date.now() - const ttlMs = input.ttlMs ?? defaultSecretHostApprovalTtlMs - const token = await encryptStringWithPurpose( - env, - secretHostApprovalPurpose, - JSON.stringify({ - kind: 'host', - userId: input.userId, - name: input.name.trim(), - scope: input.scope, - requestedHost: normalizeHost(input.requestedHost), - storageContext: input.storageContext ?? null, - iat: now, - exp: now + ttlMs, - } satisfies SecretHostApprovalRequest), - ) - return `${secretHostApprovalTokenPrefix}${token}` -} - -export async function verifySecretHostApprovalToken( - env: Pick, - token: string, -) { - if (token.startsWith('pkg:')) { - throw new Error('Invalid secret host approval request.') - } - const encodedToken = token.startsWith(secretHostApprovalTokenPrefix) - ? token.slice(secretHostApprovalTokenPrefix.length) - : token - const raw = await decryptStringWithPurpose( - env, - secretHostApprovalPurpose, - encodedToken, - ) - const parsed = JSON.parse(raw) as Partial - if ( - (parsed.kind != null && parsed.kind !== 'host') || - typeof parsed.userId !== 'string' || - typeof parsed.name !== 'string' || - typeof parsed.scope !== 'string' || - typeof parsed.requestedHost !== 'string' - ) { - throw new Error('Invalid secret host approval request.') - } - if (typeof parsed.exp === 'number' && Date.now() > parsed.exp) { - throw new Error('Secret host approval request has expired.') - } - return { - kind: 'host', - userId: parsed.userId, - name: parsed.name.trim(), - scope: parsed.scope as SecretScope, - requestedHost: normalizeHost(parsed.requestedHost), - storageContext: parsed.storageContext ?? null, - iat: typeof parsed.iat === 'number' ? parsed.iat : Date.now(), - exp: typeof parsed.exp === 'number' ? parsed.exp : Date.now(), - } satisfies SecretHostApprovalRequest -} - export function buildSecretHostApprovalUrl(input: { baseUrl: string - token: string name: string scope: SecretScope requestedHost: string @@ -105,6 +18,5 @@ export function buildSecretHostApprovalUrl(input: { }) const url = new URL(secretPath, input.baseUrl) url.searchParams.set('allowed-host', normalizeHost(input.requestedHost)) - url.searchParams.set('request', input.token) return url.toString() } diff --git a/packages/worker/src/mcp/secrets/package-approval-url.ts b/packages/worker/src/mcp/secrets/package-approval-url.ts index 76ff44eead..9d1529b1d4 100644 --- a/packages/worker/src/mcp/secrets/package-approval-url.ts +++ b/packages/worker/src/mcp/secrets/package-approval-url.ts @@ -8,7 +8,6 @@ export function buildSecretPackageApprovalUrl(input: { scope: SecretScope packageId: string kodyId: string | null - token?: string | null storageContext: StorageContext | null }) { if (input.scope === 'app' && !input.storageContext?.appId) { @@ -30,8 +29,5 @@ export function buildSecretPackageApprovalUrl(input: { if (input.kodyId) { url.searchParams.set('package', input.kodyId) } - if (input.token) { - url.searchParams.set('request', input.token) - } return url.toString() } diff --git a/packages/worker/src/mcp/secrets/package-approval.ts b/packages/worker/src/mcp/secrets/package-approval.ts deleted file mode 100644 index 1257005672..0000000000 --- a/packages/worker/src/mcp/secrets/package-approval.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { buildAccountSecretPath } from '@kody-internal/shared/account-secret-route.ts' -import { type StorageContext } from '#mcp/storage.ts' -import { decryptStringWithPurpose, encryptStringWithPurpose } from './crypto.ts' -import { type SecretScope } from './types.ts' - -const secretPackageApprovalPurpose = 'secret-package-approval' -const defaultSecretPackageApprovalTtlMs = 1000 * 60 * 60 * 24 -export const secretPackageApprovalTokenPrefix = 'pkg:' - -export type SecretPackageApprovalRequest = { - kind: 'package' - userId: string - name: string - scope: SecretScope - packageId: string - packageKodyId: string | null - storageContext: StorageContext | null - iat: number - exp: number -} - -export async function createSecretPackageApprovalToken( - env: Pick, - input: { - userId: string - name: string - scope: SecretScope - packageId: string - packageKodyId: string | null - storageContext: StorageContext | null - ttlMs?: number - }, -) { - const now = Date.now() - const ttlMs = input.ttlMs ?? defaultSecretPackageApprovalTtlMs - const token = await encryptStringWithPurpose( - env, - secretPackageApprovalPurpose, - JSON.stringify({ - kind: 'package', - userId: input.userId, - name: input.name.trim(), - scope: input.scope, - packageId: input.packageId.trim(), - packageKodyId: input.packageKodyId?.trim() || null, - storageContext: input.storageContext ?? null, - iat: now, - exp: now + ttlMs, - } satisfies SecretPackageApprovalRequest), - ) - return `${secretPackageApprovalTokenPrefix}${token}` -} - -export async function verifySecretPackageApprovalToken( - env: Pick, - token: string, -) { - const encryptedToken = token.startsWith(secretPackageApprovalTokenPrefix) - ? token.slice(secretPackageApprovalTokenPrefix.length) - : token - const raw = await decryptStringWithPurpose( - env, - secretPackageApprovalPurpose, - encryptedToken, - ) - const parsed = JSON.parse(raw) as Partial - if ( - parsed.kind !== 'package' || - typeof parsed.userId !== 'string' || - typeof parsed.name !== 'string' || - typeof parsed.scope !== 'string' || - typeof parsed.packageId !== 'string' - ) { - throw new Error('Invalid secret package approval request.') - } - if (typeof parsed.exp === 'number' && Date.now() > parsed.exp) { - throw new Error('Secret package approval request has expired.') - } - return { - kind: 'package', - userId: parsed.userId, - name: parsed.name.trim(), - scope: parsed.scope as SecretScope, - packageId: parsed.packageId.trim(), - packageKodyId: - typeof parsed.packageKodyId === 'string' - ? (parsed.packageKodyId.trim() || null) - : null, - storageContext: parsed.storageContext ?? null, - iat: typeof parsed.iat === 'number' ? parsed.iat : Date.now(), - exp: typeof parsed.exp === 'number' ? parsed.exp : Date.now(), - } satisfies SecretPackageApprovalRequest -} - -export function buildSecretPackageApprovalTokenUrl(input: { - baseUrl: string - token: string - name: string - scope: SecretScope - packageId: string - packageKodyId: string | null - storageContext: StorageContext | null -}) { - const secretPath = buildAccountSecretPath({ - name: input.name, - scope: input.scope, - appId: input.storageContext?.appId ?? null, - sessionId: input.storageContext?.sessionId ?? null, - }) - const url = new URL(secretPath, input.baseUrl) - url.searchParams.set('package_id', input.packageId) - if (input.packageKodyId) { - url.searchParams.set('package', input.packageKodyId) - } - url.searchParams.set('request', input.token) - return url.toString() -} From 7cda239bf30bc76d93a1bca4057ae8b21e18a5dc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Apr 2026 15:49:32 +0000 Subject: [PATCH 2/3] Preserve approval query params on submit Co-authored-by: Kent C. Dodds --- .../worker/client/routes/account-secrets.tsx | 6 +++-- packages/worker/client/routes/account.tsx | 7 +++++- .../app/handlers/account-secrets.node.test.ts | 23 +++++++++++++++++++ .../src/app/handlers/account-secrets.ts | 3 +++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index a567eaca12..f1527731b9 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -627,8 +627,10 @@ export function AccountSecretsRoute(handle: Handle) { handle.update() try { - const selection = getSelectionState(getCurrentHref()) - const requestUrl = new URL(accountSecretsApiPath, getCurrentHref()) + const currentUrl = new URL(getCurrentHref()) + const selection = getSelectionState(currentUrl.toString()) + const requestUrl = new URL(accountSecretsApiPath, currentUrl) + requestUrl.search = currentUrl.search if (selection.selectedSecretId) { requestUrl.searchParams.set('selected', selection.selectedSecretId) } diff --git a/packages/worker/client/routes/account.tsx b/packages/worker/client/routes/account.tsx index a6c0973a28..ce4be6348b 100644 --- a/packages/worker/client/routes/account.tsx +++ b/packages/worker/client/routes/account.tsx @@ -74,9 +74,14 @@ export function AccountRoute(handle: Handle) { message = null handle.update() try { + const href = + typeof window === 'undefined' ? '/account' : window.location.href + const currentUrl = new URL(href) + const requestUrl = new URL(accountSecretsApiPath, currentUrl) + requestUrl.search = currentUrl.search const payload = await submitApprovalRequest< AccountSecretsPayload & { error?: string; ok?: boolean } - >(action) + >(action, requestUrl.toString()) if (!payload) return email = payload.email approval = payload.approval diff --git a/packages/worker/src/app/handlers/account-secrets.node.test.ts b/packages/worker/src/app/handlers/account-secrets.node.test.ts index 3d31e7693b..c7120edec3 100644 --- a/packages/worker/src/app/handlers/account-secrets.node.test.ts +++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts @@ -361,6 +361,29 @@ test('host approval approve persists host from allowed-host and selected secret' ) }) +test('approval request rejects ambiguous host and package targets', async () => { + const handler = createAccountSecretsApiHandler(createEnv()) + const response = await handler.action({ + request: new Request( + 'https://example.com/account/secrets.json?selected=user::::cloudflareToken&allowed-host=api.cloudflare.com&package_id=pkg-123', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'approve' }), + }, + ), + params: {}, + } as never) + + expect(response.status).toBe(400) + await expect(response.json()).resolves.toMatchObject({ + ok: false, + error: 'Approval request contains both host and package.', + }) + expect(mockModule.setSecretAllowedHosts).not.toHaveBeenCalled() + expect(mockModule.setSecretAllowedPackages).not.toHaveBeenCalled() +}) + test('account secrets payload preserves app titles and allowed packages', async () => { mockModule.listSavedPackagesByUserId.mockResolvedValueOnce([ { diff --git a/packages/worker/src/app/handlers/account-secrets.ts b/packages/worker/src/app/handlers/account-secrets.ts index 4d5be39d8f..2b48498c72 100644 --- a/packages/worker/src/app/handlers/account-secrets.ts +++ b/packages/worker/src/app/handlers/account-secrets.ts @@ -737,6 +737,9 @@ function resolveApprovalRequest(input: { if (!parsed) { throw new Error('Invalid approval request.') } + if (input.requestedHost && input.requestedPackageId) { + throw new Error('Approval request contains both host and package.') + } const storageContext = getSecretContextForAccountSecret(parsed) if (input.requestedPackageId) { return { From e04380ebcbf54f075ffe11678503643a1bbc82df Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Apr 2026 16:09:44 +0000 Subject: [PATCH 3/3] Remove stale account approval card Co-authored-by: Kent C. Dodds --- packages/worker/client/routes/account.tsx | 112 +--------------------- 1 file changed, 1 insertion(+), 111 deletions(-) diff --git a/packages/worker/client/routes/account.tsx b/packages/worker/client/routes/account.tsx index ce4be6348b..0a3837e627 100644 --- a/packages/worker/client/routes/account.tsx +++ b/packages/worker/client/routes/account.tsx @@ -4,31 +4,23 @@ import { cardCss, cardTitleCss, descriptionCss, - getPrimaryButtonCss, - getSecondaryButtonCss, primaryLinkCss, } from '#client/styles/style-primitives.ts' import { type AccountStatus, - type ApprovalAction, - type ApprovalView, accountSecretsApiPath, readJson, - submitApprovalRequest, } from '#client/routes/account-approval-shared.ts' type AccountSecretsPayload = { ok: true email: string - approval: ApprovalView | null } export function AccountRoute(handle: Handle) { let status: AccountStatus = 'loading' let email = '' - let approval: ApprovalView | null = null let message: string | null = null - let submittingApprovalAction: ApprovalAction | null = null let lastLoadedHref = '' async function loadAccountSecrets(signal: AbortSignal) { @@ -54,10 +46,8 @@ export function AccountRoute(handle: Handle) { throw new Error('Unable to load your account secrets.') } email = payload.email - approval = payload.approval status = 'ready' message = null - submittingApprovalAction = null handle.update() } catch (error) { if (signal.aborted) return @@ -68,41 +58,6 @@ export function AccountRoute(handle: Handle) { } } - async function submitApproval(action: ApprovalAction) { - if (!approval || submittingApprovalAction != null) return - submittingApprovalAction = action - message = null - handle.update() - try { - const href = - typeof window === 'undefined' ? '/account' : window.location.href - const currentUrl = new URL(href) - const requestUrl = new URL(accountSecretsApiPath, currentUrl) - requestUrl.search = currentUrl.search - const payload = await submitApprovalRequest< - AccountSecretsPayload & { error?: string; ok?: boolean } - >(action, requestUrl.toString()) - if (!payload) return - email = payload.email - approval = payload.approval - submittingApprovalAction = null - message = - action === 'approve' - ? 'Approved requested host.' - : 'Rejected host approval request.' - handle.update() - if (typeof window !== 'undefined' && window.location.search) { - window.history.replaceState(null, '', '/account') - lastLoadedHref = window.location.href - } - } catch (error) { - submittingApprovalAction = null - message = - error instanceof Error ? error.message : 'Unable to process approval.' - handle.update() - } - } - return () => { const currentHref = typeof window === 'undefined' ? '/account' : window.location.href @@ -137,70 +92,8 @@ export function AccountRoute(handle: Handle) {

- {approval && !isRefreshingForLocationChange ? ( -
-
-

- Approve host access -

-

- Allow {approval.requestedHost} to receive secret{' '} - {approval.name} from the {approval.scope} scope. -

-

- Current allowed hosts:{' '} - {approval.currentAllowedHosts.length > 0 - ? approval.currentAllowedHosts.join(', ') - : 'none'} -

-
-
- - -
-
- ) : null} - {status === 'loading' ? ( -

- Loading secret approvals… -

+

Loading account…

) : null} {message ? (