Skip to content

Comments

feat(desktop): add Anthropic OAuth re-auth flow in model picker#1739

Merged
Kitenite merged 4 commits intosuperset-sh:mainfrom
Kitenite:kitenite/model-selector-auth
Feb 24, 2026
Merged

feat(desktop): add Anthropic OAuth re-auth flow in model picker#1739
Kitenite merged 4 commits intosuperset-sh:mainfrom
Kitenite:kitenite/model-selector-auth

Conversation

@Kitenite
Copy link
Collaborator

@Kitenite Kitenite commented Feb 24, 2026

Summary

  • add Anthropic OAuth PKCE/session helpers and token exchange support in packages/chat
  • add auth tRPC procedures for Anthropic status/start/complete/cancel and wire chat service methods
  • add a dedicated desktop AnthropicOAuthDialog using @superset/ui input-group components
  • add Anthropic gear action in model picker with tooltip, re-auth support, back navigation, copy/open URL icon actions, and paste support
  • keep OAuth URL opening manual (no auto-open on start)

Validation

  • bun run --cwd packages/chat typecheck
  • bun run --cwd apps/desktop typecheck

Summary by CodeRabbit

  • New Features
    • Anthropic OAuth: connect or re-authenticate Anthropic from the app via an in-picker dialog (open/copy URL, paste code, connect).
    • Per-provider grouping in the model picker with provider headings, logos, and per-provider connection controls.
    • Models that require Anthropic auth are disabled until linked; connection status updates immediately after linking.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end Anthropic OAuth: PKCE session creation and token exchange, ChatService APIs and TRPC auth endpoints, agent credential storage and expiry, a useAnthropicOAuth hook and AnthropicOAuthDialog UI, and provider-based grouping in ModelPicker with per-provider OAuth wiring.

Changes

Cohort / File(s) Summary
ModelPicker core & grouping
apps/desktop/src/renderer/.../ModelPicker/ModelPicker.tsx, apps/desktop/src/renderer/.../ModelPicker/utils/groupModelsByProvider/*, apps/desktop/src/renderer/.../ModelPicker/utils/providerToLogo/*
Refactors ModelPicker to a typed props interface, groups models by provider, and introduces provider-to-logo mapping util.
Provider group & heading UI
apps/desktop/src/renderer/.../ModelProviderGroup/ModelProviderGroup.tsx, apps/desktop/src/renderer/.../ModelProviderGroup/components/AnthropicProviderHeading/*
Adds ModelProviderGroup and AnthropicProviderHeading components to render provider-specific headings, logos, per-model items, and Anthropic connect/action controls.
Anthropic OAuth dialog & hook
apps/desktop/src/renderer/.../AnthropicOAuthDialog/*, apps/desktop/src/renderer/.../hooks/useAnthropicOAuth/*
Adds AnthropicOAuthDialog UI and useAnthropicOAuth hook that manage dialog state, TRPC calls (get/start/complete/cancel), clipboard/browser helpers, error states, and callbacks.
Backend OAuth helpers
packages/chat/src/host/auth/anthropic/oauth.ts, packages/chat/src/host/auth/anthropic/index.ts
Implements PKCE session creation, authorization-code parsing, and token exchange logic; exports session/credentials types and helper functions.
ChatService & router integration
packages/chat/src/host/chat-service/chat-service.ts, packages/chat/src/host/router/router.ts
Adds anthropicAuthSession state and TTL, ChatService methods (getAnthropicAuthStatus, startAnthropicOAuth, cancelAnthropicOAuth, completeAnthropicOAuth) and TRPC auth endpoints to drive the OAuth flow.
Agent credential handling
packages/agent/src/superagent.ts, packages/agent/src/index.ts
Introduces structured Anthropic OAuth credentials, set/clear APIs, expiry checking, and re-exports for credential management used by the service.
Re-exports / indices
multiple index.ts under ModelPicker components and utils
Adds index re-exports to expose new components, hook, and utilities via concise import paths.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as ModelPicker UI
    participant Hook as useAnthropicOAuth
    participant API as TRPC Router
    participant Service as ChatService
    participant Anthropic as Anthropic OAuth
    participant Agent as Agent Token Storage

    User->>UI: Click "Connect Anthropic"
    UI->>Hook: startAnthropicOAuth()
    Hook->>API: startAnthropicOAuth()
    API->>Service: startAnthropicOAuth()
    Service->>Service: create PKCE session (verifier, state, authUrl)
    Service-->>API: { url, instructions }
    API-->>Hook: authUrl
    Hook-->>UI: open AnthropicOAuthDialog(authUrl)
    User->>Anthropic: Visit authUrl (browser)
    Anthropic-->>User: Authorization code
    User->>UI: Paste/enter code
    UI->>Hook: onSubmit(code)
    Hook->>API: completeAnthropicOAuth({ code })
    API->>Service: completeAnthropicOAuth({ code })
    Service->>Anthropic: POST token endpoint (code, verifier)
    Anthropic-->>Service: tokens (access, refresh, expires_in)
    Service->>Agent: setAnthropicOAuthCredentials(tokens)
    Service->>Service: clear anthropicAuthSession
    Service-->>API: { success, expiresAt }
    API-->>Hook: success
    Hook-->>UI: close dialog, refresh status
    UI->>User: Anthropic models enabled/updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nudged the dialog, paw on a key,

Codes and PKCE danced straight to me.
Tokens tucked safe in a tiny den,
Anthropic hopped in — models roar again. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% 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
Title check ✅ Passed The title accurately summarizes the main change: adding Anthropic OAuth re-authentication flow to the model picker component in the desktop app.
Description check ✅ Passed The PR description provides a clear summary of changes across multiple modules and includes validation steps, but lacks sections from the template (Related Issues, Type of Change, Testing, Screenshots, Additional Notes).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/chat/src/host/auth/anthropic/oauth.ts (1)

3-6: Base64-encoding the client ID adds obfuscation, not security.

The CLIENT_ID is a public OAuth client identifier — it's not a secret. The base64 wrapping only makes the code harder to read/audit. Consider using the plaintext UUID directly.

Proposed simplification
-const CLIENT_ID = Buffer.from(
-	"OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl",
-	"base64",
-).toString("utf8");
+const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 3 - 6, The
CLIENT_ID constant is unnecessarily base64-encoded; replace the
Buffer.from(...,"base64").toString("utf8") expression with the plaintext client
ID string (the UUID) so CLIENT_ID is assigned directly as a string literal;
update the constant declaration for CLIENT_ID in oauth.ts and remove the base64
decoding logic to simplify and improve readability.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx (1)

57-70: Consider extracting OAuth state and handlers into a custom hook.

The ModelPicker now manages five OAuth state variables and four tRPC mutation/query bindings alongside the model selection logic. Extracting a useAnthropicOAuth hook would improve readability and make the OAuth flow independently testable.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx`
around lines 57 - 70, Extract the OAuth state and handlers into a new custom
hook (e.g., useAnthropicOAuth) that encapsulates oauthDialogOpen, oauthUrl,
oauthCode, oauthError, hasPendingOAuthSession and the tRPC bindings
startAnthropicOAuthMutation, completeAnthropicOAuthMutation,
cancelAnthropicOAuthMutation and refetchAnthropicStatus; implement handler
functions (startOAuth, completeOAuth, cancelOAuth, setCode, open/close dialog)
inside the hook that call the respective mutations/queries and update state,
then replace the inline state and mutation usage in ModelPicker with calls to
useAnthropicOAuth and consume the returned state and handler functions to keep
model selection logic separate and testable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/AnthropicOAuthDialog.tsx`:
- Around line 110-112: The paragraph in the AnthropicOAuthDialog component uses
Markdown backticks which render literally in JSX; update the JSX in
AnthropicOAuthDialog (look for the <p className="text-muted-foreground text-xs">
node) to render inline code using a proper <code> element instead of backtick
characters so the text displays as inline code (`code` or `code#state` should be
rendered as <code>code</code> or <code>code#state</code>); ensure surrounding
spacing and styling remain consistent with the existing className.

In `@packages/chat/src/host/auth/anthropic/oauth.ts`:
- Around line 59-77: The PKCE verifier is being leaked by setting state =
verifier in createAnthropicOAuthSession; generate a separate
cryptographically-random state value (do not reuse verifier) and place that in
the URL params as state while keeping verifier secret, add state to the
AnthropicOAuthSession return type so the session stores it, and update
parseAuthorizationCodeInput to validate the incoming state matches the
session.state before exchanging the code (reject if mismatched). Ensure you use
a secure RNG for state generation and do not change the code_challenge usage.

In `@packages/chat/src/host/chat-service/chat-service.ts`:
- Around line 119-127: The code calls exchangeAnthropicAuthorizationCode and
only persists the access token via setAnthropicAuthToken, discarding the
returned refreshToken; persist the refreshToken so the service can perform
silent refreshes later (e.g., store it on this.anthropicAuthSession or call a
corresponding setter such as setAnthropicRefreshToken), and ensure expiresAt is
tied to the stored tokens; if you intentionally defer storing refresh tokens,
add a clear TODO comment in the exchangeAnthropicAuthorizationCode handling
block explaining why and how refresh will be handled.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx`:
- Around line 57-70: Extract the OAuth state and handlers into a new custom hook
(e.g., useAnthropicOAuth) that encapsulates oauthDialogOpen, oauthUrl,
oauthCode, oauthError, hasPendingOAuthSession and the tRPC bindings
startAnthropicOAuthMutation, completeAnthropicOAuthMutation,
cancelAnthropicOAuthMutation and refetchAnthropicStatus; implement handler
functions (startOAuth, completeOAuth, cancelOAuth, setCode, open/close dialog)
inside the hook that call the respective mutations/queries and update state,
then replace the inline state and mutation usage in ModelPicker with calls to
useAnthropicOAuth and consume the returned state and handler functions to keep
model selection logic separate and testable.

In `@packages/chat/src/host/auth/anthropic/oauth.ts`:
- Around line 3-6: The CLIENT_ID constant is unnecessarily base64-encoded;
replace the Buffer.from(...,"base64").toString("utf8") expression with the
plaintext client ID string (the UUID) so CLIENT_ID is assigned directly as a
string literal; update the constant declaration for CLIENT_ID in oauth.ts and
remove the base64 decoding logic to simplify and improve readability.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 964c4a4 and 04d3eed.

📒 Files selected for processing (7)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/AnthropicOAuthDialog.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/index.ts
  • packages/chat/src/host/auth/anthropic/index.ts
  • packages/chat/src/host/auth/anthropic/oauth.ts
  • packages/chat/src/host/chat-service/chat-service.ts
  • packages/chat/src/host/router/router.ts

Comment on lines 110 to 112
<p className="text-muted-foreground text-xs">
Accepts either `code` or `code#state`.
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Backticks render as literal characters in HTML — use <code> elements instead.

The markdown-style backticks won't render as inline code in JSX. They'll appear as literal ` characters in the UI.

Proposed fix
-						<p className="text-muted-foreground text-xs">
-							Accepts either `code` or `code#state`.
-						</p>
+						<p className="text-muted-foreground text-xs">
+							Accepts either <code>code</code> or <code>code#state</code>.
+						</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/AnthropicOAuthDialog.tsx`
around lines 110 - 112, The paragraph in the AnthropicOAuthDialog component uses
Markdown backticks which render literally in JSX; update the JSX in
AnthropicOAuthDialog (look for the <p className="text-muted-foreground text-xs">
node) to render inline code using a proper <code> element instead of backtick
characters so the text displays as inline code (`code` or `code#state` should be
rendered as <code>code</code> or <code>code#state</code>); ensure surrounding
spacing and styling remain consistent with the existing className.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/ModelProviderGroup.tsx (2)

42-43: Redundant key prop on ModelSelectorGroup.

The parent (ModelPicker.tsx line 77) already sets key={provider} on <ModelProviderGroup>. The inner key={provider} on <ModelSelectorGroup> at line 43 has no effect since it's the only root element returned. You can safely remove it.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/ModelProviderGroup.tsx`
around lines 42 - 43, Remove the redundant key prop passed to the
ModelSelectorGroup inside the ModelProviderGroup component: the parent
ModelPicker already sets key={provider} on ModelProviderGroup, so delete
key={provider} from the ModelSelectorGroup JSX returned by ModelProviderGroup to
avoid a no-op prop and keep the root element unchanged.

55-84: Per-model providerToLogo call is redundant within a group.

Since groupModelsByProvider groups by exact model.provider, every model in this group shares the same provider string. So providerToLogo(model.provider) on line 56 always equals groupLogo (line 34). You can simplify by reusing groupLogo and isAnthropicProvider:

♻️ Suggested simplification
 		{models.map((model) => {
-			const logo = providerToLogo(model.provider);
-			const modelDisabled =
-				logo === ANTHROPIC_LOGO_PROVIDER && !isAnthropicAuthenticated;
+			const modelDisabled =
+				isAnthropicProvider && !isAnthropicAuthenticated;

 			return (
 				<ModelSelectorItem
 					key={model.id}
 					value={model.id}
 					disabled={modelDisabled}
 					onSelect={() => {
 						onSelectModel(model);
 						onCloseModelSelector();
 					}}
 				>
-					{logo === ANTHROPIC_LOGO_PROVIDER ? (
+					{isAnthropicProvider ? (
 						<img alt="Claude" className="size-3" src={claudeIcon} />
 					) : (
-						<ModelSelectorLogo provider={logo} />
+						<ModelSelectorLogo provider={groupLogo} />
 					)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/ModelProviderGroup.tsx`
around lines 55 - 84, The code calls providerToLogo(model.provider) for each
model in models.map even though models are grouped by provider; replace the
per-model providerToLogo call with the already-computed groupLogo and compute
modelDisabled using groupLogo === ANTHROPIC_LOGO_PROVIDER (and
isAnthropicAuthenticated) so the logo/disabled logic reuses groupLogo; keep the
rest of the JSX (ModelSelectorItem, onSelectModel, onCloseModelSelector,
ModelSelectorLogo/claudeIcon, ModelSelectorName) unchanged but reference
groupLogo for rendering and disabled checks instead of
providerToLogo(model.provider).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts`:
- Around line 62-83: openExternalUrl currently swallows errors and
unconditionally falls back to window.open, so if window.open is blocked the
caller (openOAuthUrl) never sees the failure; change openExternalUrl to detect
when the fallback window.open returns null (or otherwise fails) and throw an
error so callers can handle it. Specifically, inside openExternalUrl (which
calls electronTrpcClient.external.openUrl.mutate) keep the try/catch for the IPC
call, but after calling window.open(url, "_blank") check the return value and if
it's null/undefined or throws, throw a descriptive Error; this ensures
openOAuthUrl's catch can call setOauthError(getErrorMessage(...)) and surface
the failure to the user.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/ModelProviderGroup.tsx`:
- Around line 42-43: Remove the redundant key prop passed to the
ModelSelectorGroup inside the ModelProviderGroup component: the parent
ModelPicker already sets key={provider} on ModelProviderGroup, so delete
key={provider} from the ModelSelectorGroup JSX returned by ModelProviderGroup to
avoid a no-op prop and keep the root element unchanged.
- Around line 55-84: The code calls providerToLogo(model.provider) for each
model in models.map even though models are grouped by provider; replace the
per-model providerToLogo call with the already-computed groupLogo and compute
modelDisabled using groupLogo === ANTHROPIC_LOGO_PROVIDER (and
isAnthropicAuthenticated) so the logo/disabled logic reuses groupLogo; keep the
rest of the JSX (ModelSelectorItem, onSelectModel, onCloseModelSelector,
ModelSelectorLogo/claudeIcon, ModelSelectorName) unchanged but reference
groupLogo for rendering and disabled checks instead of
providerToLogo(model.provider).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04d3eed and 0874137.

📒 Files selected for processing (11)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/ModelProviderGroup.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/components/AnthropicProviderHeading/AnthropicProviderHeading.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/components/AnthropicProviderHeading/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/ModelProviderGroup/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/utils/groupModelsByProvider/groupModelsByProvider.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/utils/groupModelsByProvider/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/utils/providerToLogo/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/utils/providerToLogo/providerToLogo.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (5)
packages/chat/src/host/chat-service/chat-service.ts (1)

132-136: Unnecessary manual spread — pass credentials directly.

AnthropicOAuthCredentials from oauth.ts (required fields) is structurally assignable to the internal AnthropicOAuthCredentials in superagent.ts (optional fields). The spread is redundant.

♻️ Proposed simplification
-    setAnthropicOAuthCredentials({
-        accessToken: credentials.accessToken,
-        refreshToken: credentials.refreshToken,
-        expiresAt: credentials.expiresAt,
-    });
+    setAnthropicOAuthCredentials(credentials);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/chat-service/chat-service.ts` around lines 132 - 136,
The code currently calls setAnthropicOAuthCredentials by constructing a new
object with accessToken/refreshToken/expiresAt from credentials; instead, pass
the existing credentials object directly (i.e. call
setAnthropicOAuthCredentials(credentials)) because the external
AnthropicOAuthCredentials type from oauth.ts is structurally compatible with the
internal type in superagent.ts, making the manual spread redundant — update the
call sites that use setAnthropicOAuthCredentials to pass credentials directly
(refer to the setAnthropicOAuthCredentials function and the credentials variable
used in this diff).
packages/agent/src/superagent.ts (1)

22-26: AnthropicOAuthCredentials is duplicated with divergent optionality.

The same type name is defined here (internal, refreshToken?/expiresAt? optional) and in packages/chat/src/host/auth/anthropic/oauth.ts (exported, both required). The two packages are structurally compatible today but will silently diverge if either definition changes. Consider exporting the type from oauth.ts and importing it here, or moving it to a shared types module.

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

In `@packages/agent/src/superagent.ts` around lines 22 - 26, The type
AnthropicOAuthCredentials is duplicated with different optionality; replace the
local definition in superagent.ts with a single source of truth by importing the
exported type from packages/chat/src/host/auth/anthropic/oauth.ts (or move it to
a shared types module and import from there), update any references in
superagent.ts to use the imported AnthropicOAuthCredentials, and remove the
local type to prevent future divergence.
packages/chat/src/host/auth/anthropic/oauth.ts (3)

33-37: PKCE verifier is at the RFC 7636 minimum length.

randomBytes(32) encodes to exactly 43 base64url characters — the floor of the 43–128 range required by RFC 7636. Using randomBytes(33) yields 44 characters and moves off the boundary with negligible cost.

♻️ Proposed fix
 function generatePKCE(): { verifier: string; challenge: string } {
-    const verifier = base64Url(randomBytes(32));
+    const verifier = base64Url(randomBytes(33));
     const challenge = base64Url(createHash("sha256").update(verifier).digest());
     return { verifier, challenge };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 33 - 37, The
PKCE verifier generated in generatePKCE() uses randomBytes(32) which yields a
43-character base64url string — exactly the RFC 7636 minimum; increase entropy
to avoid the boundary by using randomBytes(33) so the verifier encodes to 44
characters. Update the verifier generation inside generatePKCE() to use
randomBytes(33) (leave challenge computation with createHash("sha256") as-is) so
the produced verifier meets the recommended length safely.

120-133: No timeout on the token exchange fetch — a hanging Anthropic server will block indefinitely.

The fetch to TOKEN_URL has no timeout. Add an AbortSignal.timeout to bound the call.

♻️ Proposed fix
 const response = await fetch(TOKEN_URL, {
     method: "POST",
     headers: {
         "Content-Type": "application/json",
     },
+    signal: AbortSignal.timeout(15_000),
     body: JSON.stringify({
         grant_type: "authorization_code",
         client_id: CLIENT_ID,
         code,
         state,
         redirect_uri: REDIRECT_URI,
         code_verifier: input.verifier,
     }),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 120 - 133, The
token exchange fetch to TOKEN_URL can hang indefinitely; modify the fetch call
(the POST block that sends grant_type, client_id, code, state, redirect_uri,
code_verifier) to include a signal using AbortSignal.timeout(...) (e.g., a
sensible default like 10_000 ms or a configurable timeout) so the request is
aborted on timeout; ensure the signal is passed in the fetch options and handle
the abort/timeout error where this exchange is awaited so it surfaces a clear
error instead of hanging.

3-6: CLIENT_ID base64 encoding is unnecessary obfuscation.

This is a public OAuth client ID (PKCE flows have no client secret), so there's nothing to hide. The base64 wrapping adds indirection without benefit and may mislead future readers into treating it as sensitive material.

♻️ Proposed simplification
-const CLIENT_ID = Buffer.from(
-    "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl",
-    "base64",
-).toString("utf8");
+const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 3 - 6, Replace
the unnecessary base64 decode for CLIENT_ID in oauth.ts with the plain client ID
string: remove Buffer.from(...,"base64").toString("utf8") and assign the decoded
client ID directly to the CLIENT_ID constant (keeping the same value), leaving
the identifier CLIENT_ID intact and removing the obfuscation so future readers
know it’s a public OAuth client ID.
🤖 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/chat/src/host/chat-service/chat-service.ts`:
- Around line 111-139: The completeAnthropicOAuth method can race when called
concurrently because exchangeAnthropicAuthorizationCode is awaited before
clearing this.anthropicAuthSession; to fix, capture the needed session fields
(verifier and state) into local variables, set this.anthropicAuthSession = null
immediately after the TTL check (before any await) to prevent a second caller
from using the same session, then call exchangeAnthropicAuthorizationCode with
the local verifier/state and proceed to call setAnthropicOAuthCredentials and
return the result; keep the existing TTL check using
ChatService.ANTHROPIC_AUTH_SESSION_TTL_MS and ensure no other code path relies
on anthopicAuthSession after it’s nulled.

---

Nitpick comments:
In `@packages/agent/src/superagent.ts`:
- Around line 22-26: The type AnthropicOAuthCredentials is duplicated with
different optionality; replace the local definition in superagent.ts with a
single source of truth by importing the exported type from
packages/chat/src/host/auth/anthropic/oauth.ts (or move it to a shared types
module and import from there), update any references in superagent.ts to use the
imported AnthropicOAuthCredentials, and remove the local type to prevent future
divergence.

In `@packages/chat/src/host/auth/anthropic/oauth.ts`:
- Around line 33-37: The PKCE verifier generated in generatePKCE() uses
randomBytes(32) which yields a 43-character base64url string — exactly the RFC
7636 minimum; increase entropy to avoid the boundary by using randomBytes(33) so
the verifier encodes to 44 characters. Update the verifier generation inside
generatePKCE() to use randomBytes(33) (leave challenge computation with
createHash("sha256") as-is) so the produced verifier meets the recommended
length safely.
- Around line 120-133: The token exchange fetch to TOKEN_URL can hang
indefinitely; modify the fetch call (the POST block that sends grant_type,
client_id, code, state, redirect_uri, code_verifier) to include a signal using
AbortSignal.timeout(...) (e.g., a sensible default like 10_000 ms or a
configurable timeout) so the request is aborted on timeout; ensure the signal is
passed in the fetch options and handle the abort/timeout error where this
exchange is awaited so it surfaces a clear error instead of hanging.
- Around line 3-6: Replace the unnecessary base64 decode for CLIENT_ID in
oauth.ts with the plain client ID string: remove
Buffer.from(...,"base64").toString("utf8") and assign the decoded client ID
directly to the CLIENT_ID constant (keeping the same value), leaving the
identifier CLIENT_ID intact and removing the obfuscation so future readers know
it’s a public OAuth client ID.

In `@packages/chat/src/host/chat-service/chat-service.ts`:
- Around line 132-136: The code currently calls setAnthropicOAuthCredentials by
constructing a new object with accessToken/refreshToken/expiresAt from
credentials; instead, pass the existing credentials object directly (i.e. call
setAnthropicOAuthCredentials(credentials)) because the external
AnthropicOAuthCredentials type from oauth.ts is structurally compatible with the
internal type in superagent.ts, making the manual spread redundant — update the
call sites that use setAnthropicOAuthCredentials to pass credentials directly
(refer to the setAnthropicOAuthCredentials function and the credentials variable
used in this diff).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0874137 and 8ae2b15.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/AnthropicOAuthDialog.tsx
  • packages/agent/src/index.ts
  • packages/agent/src/superagent.ts
  • packages/chat/src/host/auth/anthropic/oauth.ts
  • packages/chat/src/host/chat-service/chat-service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/components/AnthropicOAuthDialog/AnthropicOAuthDialog.tsx

@Kitenite Kitenite merged commit 6bdcadd into superset-sh:main Feb 24, 2026
11 of 14 checks passed
@Kitenite Kitenite deleted the kitenite/model-selector-auth branch February 24, 2026 06:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts (1)

151-176: Extract mutateAsync to avoid unnecessary callback re-creations from unstable mutation object.

TanStack Query's useMutation returns a non-referentially-stable object that changes on every state transition. However, mutateAsync is designed to be stable across re-renders. By including the entire cancelAnthropicOAuthMutation object in the dependency array, onOAuthDialogOpenChange recreates whenever the mutation state changes, even though only mutateAsync is used inside. Depend on the extracted mutateAsync instead to prevent these unnecessary re-creations.

♻️ Proposed refactor
+	const cancelAnthropicOAuthMutateAsync = cancelAnthropicOAuthMutation.mutateAsync;
+
 	const onOAuthDialogOpenChange = useCallback(
 		(nextOpen: boolean) => {
 			setOauthDialogOpen(nextOpen);
 			if (nextOpen) return;
 			onModelSelectorOpenChange(true);

 			setOauthCode("");
 			setOauthError(null);
 			setOauthUrl(null);

 			if (hasPendingOAuthSession) {
-				void cancelAnthropicOAuthMutation.mutateAsync().catch((error) => {
+				void cancelAnthropicOAuthMutateAsync().catch((error) => {
 					console.error(
 						"[model-picker] Failed to cancel Anthropic OAuth:",
 						error,
 					);
 				});
 				setHasPendingOAuthSession(false);
 			}
 		},
 		[
-			cancelAnthropicOAuthMutation,
+			cancelAnthropicOAuthMutateAsync,
 			hasPendingOAuthSession,
 			onModelSelectorOpenChange,
 		],
 	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts`
around lines 151 - 176, The onOAuthDialogOpenChange callback depends on the
whole cancelAnthropicOAuthMutation object which is unstable; extract mutateAsync
from cancelAnthropicOAuthMutation (e.g., const { mutateAsync:
cancelAnthropicOAuth } = cancelAnthropicOAuthMutation) and use that stable
function inside onOAuthDialogOpenChange, then replace
cancelAnthropicOAuthMutation in the useCallback dependency array with the
extracted cancelAnthropicOAuth; keep the existing references like
hasPendingOAuthSession, setHasPendingOAuthSession, onModelSelectorOpenChange,
setOauthCode, setOauthError and setOauthUrl unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts`:
- Around line 132-138: The model selector is opened via
onModelSelectorOpenChange(true) before the refetchAnthropicStatus() completes,
causing components to read a stale isAnthropicAuthenticated value; change the
order so you await refetchAnthropicStatus() first (await
refetchAnthropicStatus()), then call onModelSelectorOpenChange(true), keeping
the other state resets (setHasPendingOAuthSession(false),
setOauthDialogOpen(false), setOauthUrl(null), setOauthCode("")) as-is before or
around the await so the selector only opens after the authentication status is
refreshed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts`:
- Around line 169-173: The expiresAt calculation can be in the past when
data.expires_in < 300; adjust the logic in
packages/chat/src/host/auth/anthropic/oauth.ts so expiresAt is clamped to a
minimum future timestamp. Compute the candidateExpires = Date.now() +
data.expires_in * 1000 - 5 * 60 * 1000 and then set expiresAt =
Math.max(candidateExpires, Date.now() + 1000) (or otherwise ensure
candidateExpires >= Date.now()), referencing the expiresAt return value and
data.expires_in to prevent immediate expiry on malformed/short-lived tokens.
- Around line 129-136: The token exchange is incorrectly including the `state`
parameter in the POST body; per RFC 6749/7636 and Anthropic's token
expectations, remove `state` from the JSON payload sent in the token exchange so
the body contains only grant_type, client_id, code, redirect_uri, and
code_verifier (locate the token exchange block in
packages/chat/src/host/auth/anthropic/oauth.ts where the body is constructed for
the authorization_code exchange).

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts`:
- Around line 151-176: The onOAuthDialogOpenChange callback depends on the whole
cancelAnthropicOAuthMutation object which is unstable; extract mutateAsync from
cancelAnthropicOAuthMutation (e.g., const { mutateAsync: cancelAnthropicOAuth }
= cancelAnthropicOAuthMutation) and use that stable function inside
onOAuthDialogOpenChange, then replace cancelAnthropicOAuthMutation in the
useCallback dependency array with the extracted cancelAnthropicOAuth; keep the
existing references like hasPendingOAuthSession, setHasPendingOAuthSession,
onModelSelectorOpenChange, setOauthCode, setOauthError and setOauthUrl
unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae2b15 and 55cbc5b.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts
  • packages/chat/src/host/auth/anthropic/oauth.ts
  • packages/chat/src/host/chat-service/chat-service.ts

Comment on lines +132 to +138
await completeAnthropicOAuthMutation.mutateAsync({ code });
setHasPendingOAuthSession(false);
setOauthDialogOpen(false);
setOauthUrl(null);
setOauthCode("");
onModelSelectorOpenChange(true);
await refetchAnthropicStatus();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Model selector opens before status refetch resolves.

onModelSelectorOpenChange(true) is called on line 137 before await refetchAnthropicStatus(). If the model picker reads isAnthropicAuthenticated immediately on open, it will see the stale false value until the refetch settles, potentially showing a brief "not authenticated" state right after a successful OAuth.

Consider awaiting the refetch before reopening the selector:

♻️ Proposed fix
-			onModelSelectorOpenChange(true);
 			await refetchAnthropicStatus();
+			onModelSelectorOpenChange(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/hooks/useAnthropicOAuth/useAnthropicOAuth.ts`
around lines 132 - 138, The model selector is opened via
onModelSelectorOpenChange(true) before the refetchAnthropicStatus() completes,
causing components to read a stale isAnthropicAuthenticated value; change the
order so you await refetchAnthropicStatus() first (await
refetchAnthropicStatus()), then call onModelSelectorOpenChange(true), keeping
the other state resets (setHasPendingOAuthSession(false),
setOauthDialogOpen(false), setOauthUrl(null), setOauthCode("")) as-is before or
around the await so the selector only opens after the authentication status is
refreshed.

Comment on lines +129 to +136
body: JSON.stringify({
grant_type: "authorization_code",
client_id: CLIENT_ID,
code,
state,
redirect_uri: REDIRECT_URI,
code_verifier: input.verifier,
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Anthropic OAuth token endpoint request parameters documentation

💡 Result:

Anthropic does not appear to publish first-party public docs that list the request parameters for its OAuth token endpoint; their Help Center instead points you to the general Console docs site. [1]

What is publicly observable (from Claude/Anthropic OAuth implementations in the wild) is that the token exchange uses a standard OAuth 2.0 + PKCE shape against:

  • Token endpoint: POST https://console.anthropic.com/v1/oauth/token [2][3]

Authorization code + PKCE token exchange (form-encoded)

Send Content-Type: application/x-www-form-urlencoded with: [2]

  • grant_type=authorization_code
  • code (the authorization code)
  • client_id
  • redirect_uri
  • code_verifier (PKCE verifier)

Refresh token exchange

Implementations also use the same token endpoint to refresh, using: [3]

  • grant_type=refresh_token
  • refresh_token
  • client_id (commonly included)

Where to look (official): Anthropic’s Help Center directs API documentation requests to the Console docs site. [1]

Sources:
[1] Anthropic Help Center: “Where can I find your API documentation?” https://support.anthropic.com/en/articles/8114490-where-can-i-find-your-api-documentation
[2] GitHub issue showing Anthropic token exchange parameters (PKCE) anthropics/claude-code#11814
[3] Example helper code using https://console.anthropic.com/v1/oauth/token for exchange/refresh https://gist.github.com/actuallyepic/6dd18b829e48a5333620ca2f2b22a27c


Remove the state parameter from the token endpoint request body.

Per RFC 6749 (OAuth 2.0) and RFC 7636 (PKCE), state belongs only in the authorization request, not the token exchange. Anthropic's token endpoint does not include state in its observed request parameters (grant_type, code, client_id, redirect_uri, code_verifier). Sending it violates the OAuth 2.0 specification and may cause failures if the endpoint strictly validates parameters.

Current code
			body: JSON.stringify({
				grant_type: "authorization_code",
				client_id: CLIENT_ID,
				code,
				state,
				redirect_uri: REDIRECT_URI,
				code_verifier: input.verifier,
			}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 129 - 136, The
token exchange is incorrectly including the `state` parameter in the POST body;
per RFC 6749/7636 and Anthropic's token expectations, remove `state` from the
JSON payload sent in the token exchange so the body contains only grant_type,
client_id, code, redirect_uri, and code_verifier (locate the token exchange
block in packages/chat/src/host/auth/anthropic/oauth.ts where the body is
constructed for the authorization_code exchange).

Comment on lines +169 to +173
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

expiresAt can be in the past if expires_in < 300.

Date.now() + data.expires_in * 1000 - 5 * 60 * 1000 yields a past timestamp when expires_in is less than 300 seconds. While Anthropic returns standard values (e.g., 3600s), a malformed or unusually short-lived response would cause the token to be treated as immediately expired on the client side.

🛡️ Proposed fix
-	expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
+	expiresAt: Date.now() + Math.max(0, data.expires_in - 300) * 1000,
📝 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
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
};
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + Math.max(0, data.expires_in - 300) * 1000,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/host/auth/anthropic/oauth.ts` around lines 169 - 173, The
expiresAt calculation can be in the past when data.expires_in < 300; adjust the
logic in packages/chat/src/host/auth/anthropic/oauth.ts so expiresAt is clamped
to a minimum future timestamp. Compute the candidateExpires = Date.now() +
data.expires_in * 1000 - 5 * 60 * 1000 and then set expiresAt =
Math.max(candidateExpires, Date.now() + 1000) (or otherwise ensure
candidateExpires >= Date.now()), referencing the expiresAt return value and
data.expires_in to prevent immediate expiry on malformed/short-lived tokens.

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.

1 participant