From db99290f40cbbd320937793ab97cde4ed2a22441 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 14:14:11 +0000 Subject: [PATCH 1/4] Hide approved OAuth host links Co-authored-by: Kent C. Dodds --- .../app/handlers/account-secrets.node.test.ts | 79 +++++++++++++++++++ .../src/app/handlers/account-secrets.ts | 18 ++++- 2 files changed, 95 insertions(+), 2 deletions(-) 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 23d1e20d7a..c11485951c 100644 --- a/packages/worker/src/app/handlers/account-secrets.node.test.ts +++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts @@ -128,6 +128,7 @@ function createEnv() { } test('connect oauth returns direct host approval links for saved token secrets', async () => { + vi.clearAllMocks() const handler = createAccountSecretsApiHandler(createEnv()) const response = await handler.action({ request: new Request('https://example.com/account/secrets.json', { @@ -230,3 +231,81 @@ test('connect oauth returns direct host approval links for saved token secrets', }), ) }) + +test('connect oauth omits direct host approval links when hosts are already approved', async () => { + vi.clearAllMocks() + mockModule.listSecrets.mockResolvedValueOnce([ + { + name: 'teslaAccessToken', + scope: 'user', + description: '', + appId: null, + allowedHosts: [ + 'auth.tesla.com', + 'fleet-api.prd.na.vn.cloud.tesla.com', + 'fleet-auth.prd.vn.cloud.tesla.com', + ], + allowedCapabilities: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + ttlMs: null, + }, + { + name: 'teslaRefreshToken', + scope: 'user', + description: '', + appId: null, + allowedHosts: [ + 'auth.tesla.com', + 'fleet-api.prd.na.vn.cloud.tesla.com', + 'fleet-auth.prd.vn.cloud.tesla.com', + ], + allowedCapabilities: [], + 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', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'connect_oauth', + provider: 'Tesla', + tokenUrl: 'https://auth.tesla.com/oauth2/v3/token', + apiBaseUrl: 'https://fleet-api.prd.na.vn.cloud.tesla.com', + flow: 'pkce', + clientIdValueName: 'tesla-client-id', + accessTokenSecretName: 'teslaAccessToken', + refreshTokenSecretName: 'teslaRefreshToken', + allowedHosts: [ + 'fleet-api.prd.na.vn.cloud.tesla.com', + 'fleet-auth.prd.vn.cloud.tesla.com', + ], + tokenPayload: { + access_token: 'access-token', + refresh_token: 'refresh-token', + }, + }), + }), + params: {}, + } as never) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toMatchObject({ + ok: true, + accessTokenSaved: true, + refreshTokenSaved: true, + allowedHosts: [ + 'auth.tesla.com', + 'fleet-api.prd.na.vn.cloud.tesla.com', + 'fleet-auth.prd.vn.cloud.tesla.com', + ], + hostApprovalLinks: [], + connectorName: 'Tesla', + }) + expect(mockModule.createSecretHostApprovalToken).not.toHaveBeenCalled() +}) diff --git a/packages/worker/src/app/handlers/account-secrets.ts b/packages/worker/src/app/handlers/account-secrets.ts index b876bf86f5..43db1ecfc1 100644 --- a/packages/worker/src/app/handlers/account-secrets.ts +++ b/packages/worker/src/app/handlers/account-secrets.ts @@ -415,6 +415,15 @@ async function buildConnectOauthHostApprovalLinks(input: { 0, maxConnectOauthApprovalSecrets, ) + const secrets = await listSecrets({ + env: input.env, + userId: input.userId, + scope: 'user', + storageContext: null, + }) + const approvedHostsBySecretName = new Map( + secrets.map((secret) => [secret.name, new Set(secret.allowedHosts)]), + ) const baseUrl = getAppBaseUrl({ env: input.env, requestUrl: input.request.url, @@ -422,6 +431,9 @@ async function buildConnectOauthHostApprovalLinks(input: { const links = await Promise.all( uniqueSecretNames.flatMap((secretName) => uniqueHosts.map(async (host) => { + if (approvedHostsBySecretName.get(secretName)?.has(host)) { + return null + } const token = await createSecretHostApprovalToken(input.env, { userId: input.userId, name: secretName, @@ -444,12 +456,14 @@ async function buildConnectOauthHostApprovalLinks(input: { }), ), ) - return links.sort((left, right) => { + return links + .filter((link): link is ConnectOauthHostApprovalLink => link !== null) + .sort((left, right) => { return ( left.secretName.localeCompare(right.secretName) || left.host.localeCompare(right.host) ) - }) + }) } async function handleOAuthExchangeAction(input: { From c8e6aace1f1f398ff14cf94bc6a982fc5af7ee03 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 14:17:56 +0000 Subject: [PATCH 2/4] Stabilize OAuth host approval regression test Co-authored-by: Kent C. Dodds --- .../src/app/handlers/account-secrets.node.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 c11485951c..b9875ccb7e 100644 --- a/packages/worker/src/app/handlers/account-secrets.node.test.ts +++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts @@ -295,17 +295,20 @@ test('connect oauth omits direct host approval links when hosts are already appr } as never) expect(response.status).toBe(200) - await expect(response.json()).resolves.toMatchObject({ + const payload = await response.json() + expect(payload).toMatchObject({ ok: true, accessTokenSaved: true, refreshTokenSaved: true, - allowedHosts: [ - 'auth.tesla.com', - 'fleet-api.prd.na.vn.cloud.tesla.com', - 'fleet-auth.prd.vn.cloud.tesla.com', - ], hostApprovalLinks: [], connectorName: 'Tesla', }) + expect(payload.allowedHosts).toEqual( + expect.arrayContaining([ + 'auth.tesla.com', + 'fleet-api.prd.na.vn.cloud.tesla.com', + 'fleet-auth.prd.vn.cloud.tesla.com', + ]), + ) expect(mockModule.createSecretHostApprovalToken).not.toHaveBeenCalled() }) From 6ac5e5b4a6f75aeb10ea6f3776d1106572b8ad05 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 14:58:30 +0000 Subject: [PATCH 3/4] Remove redundant Vitest mock clearing Co-authored-by: Kent C. Dodds --- packages/worker/src/app/handlers/account-secrets.node.test.ts | 2 -- 1 file changed, 2 deletions(-) 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 b9875ccb7e..9f45fb9f63 100644 --- a/packages/worker/src/app/handlers/account-secrets.node.test.ts +++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts @@ -128,7 +128,6 @@ function createEnv() { } test('connect oauth returns direct host approval links for saved token secrets', async () => { - vi.clearAllMocks() const handler = createAccountSecretsApiHandler(createEnv()) const response = await handler.action({ request: new Request('https://example.com/account/secrets.json', { @@ -233,7 +232,6 @@ test('connect oauth returns direct host approval links for saved token secrets', }) test('connect oauth omits direct host approval links when hosts are already approved', async () => { - vi.clearAllMocks() mockModule.listSecrets.mockResolvedValueOnce([ { name: 'teslaAccessToken', From a2c7d89d09781f27b9a5b83cbddc4bee5207d6a9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 15:05:42 +0000 Subject: [PATCH 4/4] Fix OAuth host approval links Co-authored-by: Kent C. Dodds --- .../src/app/handlers/account-secrets.ts | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/worker/src/app/handlers/account-secrets.ts b/packages/worker/src/app/handlers/account-secrets.ts index 43db1ecfc1..04547402a3 100644 --- a/packages/worker/src/app/handlers/account-secrets.ts +++ b/packages/worker/src/app/handlers/account-secrets.ts @@ -315,6 +315,16 @@ async function handleConnectOauthAction(input: { ) } + const approvedHostsBySecretName = new Map( + ( + await listSecrets({ + env: input.env, + userId: input.user.mcpUser.userId, + scope: 'user', + storageContext: null, + }) + ).map((secret) => [secret.name, new Set(secret.allowedHosts)]), + ) const accessSaved = await saveSecret({ env: input.env, userId: input.user.mcpUser.userId, @@ -381,6 +391,7 @@ async function handleConnectOauthAction(input: { userId: input.user.mcpUser.userId, allowedHosts, secretNames: approvalSecretNames, + approvedHostsBySecretName, }) } catch (error) { console.error('Failed to build OAuth host approval links.', { @@ -406,6 +417,7 @@ async function buildConnectOauthHostApprovalLinks(input: { userId: string allowedHosts: Array secretNames: Array + approvedHostsBySecretName?: Map> }) { const uniqueHosts = Array.from(new Set(input.allowedHosts)).slice( 0, @@ -415,15 +427,18 @@ async function buildConnectOauthHostApprovalLinks(input: { 0, maxConnectOauthApprovalSecrets, ) - const secrets = await listSecrets({ - env: input.env, - userId: input.userId, - scope: 'user', - storageContext: null, - }) - const approvedHostsBySecretName = new Map( - secrets.map((secret) => [secret.name, new Set(secret.allowedHosts)]), - ) + const approvedHostsBySecretName = + input.approvedHostsBySecretName ?? + new Map( + ( + await listSecrets({ + env: input.env, + userId: input.userId, + scope: 'user', + storageContext: null, + }) + ).map((secret) => [secret.name, new Set(secret.allowedHosts)]), + ) const baseUrl = getAppBaseUrl({ env: input.env, requestUrl: input.request.url, @@ -459,10 +474,10 @@ async function buildConnectOauthHostApprovalLinks(input: { return links .filter((link): link is ConnectOauthHostApprovalLink => link !== null) .sort((left, right) => { - return ( - left.secretName.localeCompare(right.secretName) || - left.host.localeCompare(right.host) - ) + return ( + left.secretName.localeCompare(right.secretName) || + left.host.localeCompare(right.host) + ) }) }