Skip to content

Hide approved OAuth host links - #135

Merged
kentcdodds merged 4 commits into
mainfrom
cursor/approved-secrets-links-visibility-1012
Apr 2, 2026
Merged

kentcdodds merged 4 commits into
mainfrom
cursor/approved-secrets-links-visibility-1012

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • filter OAuth success host approval links to only show secret/host pairs that are not already approved
  • add a regression test covering already-approved Tesla token hosts
  • remove redundant vi.clearAllMocks() calls now that shared Vitest config already enables clearMocks: true

Testing

  • npm run test -- packages/worker/src/app/handlers/account-secrets.node.test.ts
  • npm run test -- --skip-nx-cache packages/worker/src/app/handlers/account-secrets.node.test.ts
  • Seed-script manual UI setup was attempted earlier, but local tools/seed-test-data.ts currently fails against local Wrangler with Couldn't find a D1 DB with the name or binding 'APP_DB' in your Wrangler configuration file.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes

    • Stops creating redundant host-approval links for hosts already authorized on a user's tokens, reducing unnecessary approval prompts.
  • Tests

    • Added a test confirming existing token secrets with approved hosts succeed without producing new approval links and without triggering approval-token creation.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 2, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Preloads user secrets and uses them to skip generating OAuth host-approval links for hosts already present in a secret's allowedHosts. Adds a test verifying connect_oauth returns success with no approval links when relevant hosts are already approved and ensures no host-approval token is created.

Changes

Cohort / File(s) Summary
OAuth Host Approval Link Generation
packages/worker/src/app/handlers/account-secrets.ts
Preload all user secrets via listSecrets, build a Map<secretName, Set<allowedHosts>>, pass it into buildConnectOauthHostApprovalLinks, treat pairs without newly-required approvals as null, filter out null links, and sort remaining links by secretName and host.
Host Approval Link Generation Tests
packages/worker/src/app/handlers/account-secrets.node.test.ts
Added test for connect_oauth when token secrets already include requested Tesla hosts: asserts 200 status, accessTokenSaved/refreshTokenSaved true, hostApprovalLinks: [], and verifies createSecretHostApprovalToken is not invoked.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through secrets, checked each trusted host,
If Tesla was already loved, I spared it from my post.
Tokens tucked in snug and neat,
No extra links, my job complete. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Hide approved OAuth host links' directly and clearly describes the main change: filtering out already-approved OAuth host approval links from the response.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/approved-secrets-links-visibility-1012

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review April 2, 2026 14:55
@github-actions

github-actions Bot commented Apr 2, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-135.kentcdodds.workers.dev

Worker: kody-pr-135
D1: kody-pr-135-db
KV: kody-pr-135-oauth-kv

Mocks:

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Host approval links never generated in production
    • Captured approved hosts before persisting updates and reused that snapshot when building OAuth host approval links, so newly written hosts no longer suppress link generation.
Preview (a2c7d89d09)
diff --git a/packages/worker/src/app/handlers/account-secrets.node.test.ts b/packages/worker/src/app/handlers/account-secrets.node.test.ts
--- a/packages/worker/src/app/handlers/account-secrets.node.test.ts
+++ b/packages/worker/src/app/handlers/account-secrets.node.test.ts
@@ -230,3 +230,83 @@
 		}),
 	)
 })
+
+test('connect oauth omits direct host approval links when hosts are already approved', async () => {
+	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)
+	const payload = await response.json()
+	expect(payload).toMatchObject({
+		ok: true,
+		accessTokenSaved: true,
+		refreshTokenSaved: true,
+		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()
+})

diff --git a/packages/worker/src/app/handlers/account-secrets.ts b/packages/worker/src/app/handlers/account-secrets.ts
--- a/packages/worker/src/app/handlers/account-secrets.ts
+++ b/packages/worker/src/app/handlers/account-secrets.ts
@@ -315,6 +315,16 @@
 		)
 	}
 
+	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 @@
 			userId: input.user.mcpUser.userId,
 			allowedHosts,
 			secretNames: approvalSecretNames,
+			approvedHostsBySecretName,
 		})
 	} catch (error) {
 		console.error('Failed to build OAuth host approval links.', {
@@ -406,6 +417,7 @@
 	userId: string
 	allowedHosts: Array<string>
 	secretNames: Array<string>
+	approvedHostsBySecretName?: Map<string, Set<string>>
 }) {
 	const uniqueHosts = Array.from(new Set(input.allowedHosts)).slice(
 		0,
@@ -415,6 +427,18 @@
 		0,
 		maxConnectOauthApprovalSecrets,
 	)
+	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,
@@ -422,6 +446,9 @@
 	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 +471,14 @@
 			}),
 		),
 	)
-	return links.sort((left, right) => {
-		return (
-			left.secretName.localeCompare(right.secretName) ||
-			left.host.localeCompare(right.host)
-		)
-	})
+	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: {

You can send follow-ups to this agent here.

Comment thread packages/worker/src/app/handlers/account-secrets.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/app/handlers/account-secrets.ts (1)

318-327: Consider extracting approved-host map loading into a shared helper.

The same listSecrets(...).map(secret => [secret.name, new Set(secret.allowedHosts)]) logic appears twice. A helper would reduce drift risk.

Also applies to: 430-441

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/app/handlers/account-secrets.ts` around lines 318 - 327,
Extract the repeated logic that builds a Map of secretName -> Set(allowedHosts)
into a shared helper function (e.g., buildApprovedHostsMap or
getApprovedHostsBySecret) that accepts the same parameters passed to listSecrets
(env, userId, scope, storageContext) or the returned secret array; replace the
inline uses that create approvedHostsBySecretName and the other occurrence
(lines referenced around 430-441) to call this helper instead of duplicating
listSecrets(...).map(...), and ensure the helper returns Map<string,
Set<string>> and is exported/placed in a shared util file so both
account-secrets.ts call sites can reuse it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/app/handlers/account-secrets.ts`:
- Around line 318-327: The preloading call to listSecrets that builds
approvedHostsBySecretName can throw and currently runs outside the existing
fallback try/catch, which allows transient failures to block the connect_oauth
flow; wrap the listSecrets call in a try/catch (or move it inside the existing
fallback block) and on error log the failure and fall back to an empty Map for
approvedHostsBySecretName so token save/update and link generation still
proceed; refer to the approvedHostsBySecretName variable and the listSecrets
invocation when making this change.

---

Nitpick comments:
In `@packages/worker/src/app/handlers/account-secrets.ts`:
- Around line 318-327: Extract the repeated logic that builds a Map of
secretName -> Set(allowedHosts) into a shared helper function (e.g.,
buildApprovedHostsMap or getApprovedHostsBySecret) that accepts the same
parameters passed to listSecrets (env, userId, scope, storageContext) or the
returned secret array; replace the inline uses that create
approvedHostsBySecretName and the other occurrence (lines referenced around
430-441) to call this helper instead of duplicating listSecrets(...).map(...),
and ensure the helper returns Map<string, Set<string>> and is exported/placed in
a shared util file so both account-secrets.ts call sites can reuse it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0251a47-da5c-46d7-b50e-35b56f27e82f

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac5e5b and a2c7d89.

📒 Files selected for processing (1)
  • packages/worker/src/app/handlers/account-secrets.ts

Comment on lines +318 to +327
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)]),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preload failure can now block the entire OAuth connect flow.

Line 318 runs listSecrets outside the existing fallback try/catch (Line 387+), so a transient read failure now fails connect_oauth before token save/update work runs. This was previously non-blocking behavior for link generation.

Suggested fix
-	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)]),
-	)
+	let approvedHostsBySecretName: Map<string, Set<string>> | undefined
+	try {
+		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)]),
+		)
+	} catch (error) {
+		console.error('Failed to preload approved OAuth hosts.', {
+			userId: input.user.mcpUser.userId,
+			error,
+		})
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)]),
)
let approvedHostsBySecretName: Map<string, Set<string>> | undefined
try {
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)]),
)
} catch (error) {
console.error('Failed to preload approved OAuth hosts.', {
userId: input.user.mcpUser.userId,
error,
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/app/handlers/account-secrets.ts` around lines 318 - 327,
The preloading call to listSecrets that builds approvedHostsBySecretName can
throw and currently runs outside the existing fallback try/catch, which allows
transient failures to block the connect_oauth flow; wrap the listSecrets call in
a try/catch (or move it inside the existing fallback block) and on error log the
failure and fall back to an empty Map for approvedHostsBySecretName so token
save/update and link generation still proceed; refer to the
approvedHostsBySecretName variable and the listSecrets invocation when making
this change.

@kentcdodds
kentcdodds merged commit 685d6a4 into main Apr 2, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/approved-secrets-links-visibility-1012 branch April 17, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants