Skip to content

Comments

feat: extensible OAuth 2.0 PKCE provider connect buttons (OpenRouter + Codex)#26

Open
ProfSynapse wants to merge 5 commits intomainfrom
feat/provider-oauth-connect
Open

feat: extensible OAuth 2.0 PKCE provider connect buttons (OpenRouter + Codex)#26
ProfSynapse wants to merge 5 commits intomainfrom
feat/provider-oauth-connect

Conversation

@ProfSynapse
Copy link
Owner

Summary

Adds OAuth 2.0 PKCE connect buttons to the provider settings UI, allowing users to authenticate with LLM providers without manually copying API keys. Architecture is provider-agnostic — any future provider can be added by implementing the IOAuthProvider interface.

Providers implemented:

  • OpenRouter (official) — PKCE flow, produces permanent sk-or-... key, port 3000
  • OpenAI Codex (experimental) — borrowed Codex CLI client ID, port 1455, token TTL ~10 days. Consent dialog required. Modeled on OpenCode's implementation.

Key technical decisions:

  • Codex inference: stream: true, store: false, input as array (all mandatory per spike testing)
  • Proactive token refresh: checks 5-min window before ~10-day TTL
  • OAuthCallbackServer binds to 127.0.0.1 only, single-use, 5-min timeout, state validation
  • PKCEUtils uses crypto.subtle (Web Crypto API) — never Math.random
  • Desktop-only via Platform.isDesktop guard

Spike validated: Full Codex OAuth flow tested end-to-end before implementation.

New files

  • src/services/oauth/IOAuthProvider, PKCEUtils, OAuthCallbackServer, OAuthService, providers
  • src/services/llm/adapters/openai-codex/OpenAICodexAdapter, OpenAICodexModels
  • src/components/llm-provider/providers/OAuthModals.ts — consent + pre-auth dialogs
  • tests/unit/ — 174 unit tests (525/525 passing)

Modified files

GenericProviderModal, LLMProviderModal, ProvidersTab, main.ts, ProviderTypes, AdapterRegistry, ModelRegistry, ProviderManager, styles.css

Test plan

  • 525/525 tests pass (351 baseline + 174 new OAuth tests)
  • TypeScript build: 0 errors
  • Manual: Settings > Providers, click "Connect" for OpenRouter
  • Manual: click "Connect" for OpenAI Codex, accept consent dialog
  • Manual: verify connected badge, disconnect, auto-clear on manual key edit

🤖 Generated with Claude Code

ProfSynapse and others added 5 commits February 21, 2026 15:43
Adds OAuth connect buttons for OpenRouter (official) and OpenAI Codex
(experimental) providers. Architecture supports any future LLM provider
via the IOAuthProvider interface.

Core OAuth infrastructure:
- IOAuthProvider interface + OAuthService singleton registry
- PKCEUtils: PKCE helpers using crypto.subtle (S256 challenge)
- OAuthCallbackServer: ephemeral 127.0.0.1 server, state validation,
  5-min timeout, EADDRINUSE handling

Providers:
- OpenRouterOAuthProvider: port 3000, JSON token exchange, permanent sk-or-... key
- OpenAICodexOAuthProvider: port 1455, form-urlencoded exchange, JWT accountId extraction,
  proactive token refresh (5-min window before ~10-day TTL)

LLM adapter:
- OpenAICodexAdapter: SSE stream parsing, Bearer + ChatGPT-Account-Id headers,
  input as array, stream:true + store:false required parameters
- OpenAICodexModels: 6 model variants (gpt-5.3-codex through gpt-5.1-codex-mini)

UI:
- OAuthConsentModal: experimental provider consent dialog
- OAuthPreAuthModal: key_label + optional credit_limit pre-auth form (OpenRouter)
- GenericProviderModal: OAuth connect button, connected banner, disconnect, auto-clear
- ProvidersTab: attachOAuthConfigs(), Platform.isDesktop guard
- styles.css: OAuth button and status badge styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comprehensive test suite for all new OAuth modules:
- PKCEUtils: verifier/challenge/state generation, base64url encoding (18 tests)
- OAuthCallbackServer: state validation, error paths, timeout (port 0), EADDRINUSE (24 tests)
- OAuthService: state machine, concurrent flow prevention, provider registry (22 tests)
- OpenRouterOAuthProvider: config, auth URL, token exchange, preAuthParams (28 tests)
- OpenAICodexOAuthProvider: config, auth URL, token exchange, JWT parsing, refresh (36 tests)
- OpenAICodexAdapter: SSE parsing, proactive refresh, header construction (32 tests)
- OAuthModals: consent dialog, pre-auth form validation (14 tests)

All 525 tests pass (351 baseline + 174 new). fetch mocked throughout.
OAuthService.resetInstance() used for test isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security:
- Add Cache-Control: no-store to all OAuthCallbackServer responses
- Use crypto.timingSafeEqual() for CSRF state comparison
- Remove id_token (PII) from stored metadata after accountId extraction
- prefers-color-scheme CSS in callback HTML (light/dark, system-ui font)

Quality:
- Add saveSettings() callback after token refresh via _onSettingsDirty
- Remove unnecessary `as any` cast in ProviderManager
- Fix provider ID mismatch: ProvidersTab now attaches Codex config to
  'openai-codex' entry (not 'openai'); full data flow verified
- Call OAuthService.resetInstance() in main.ts onunload() to prevent
  callback server leak on plugin disable/reload

Tests:
- OAuthCallbackServer: use randomized ephemeral ports (port 0 requires
  source refactor; documented)
- OAuthService: restore global.window in afterAll to prevent mock leak
- OpenAICodexAdapter: add partial SSE chunk boundary test + concurrent
  refresh deduplication test (confirms refreshInProgress lock works)
- OpenAICodexOAuthProvider: update idToken assertion to expect undefined
  (PII prevention intent now documented)
- jest.config.js: add per-file coverage thresholds for 6 OAuth source
  files (PKCEUtils 100%, others 80/75/80/80)

527/527 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Codex endpoint uses the Responses API SSE format, which supports
tool/function calling. This commit enables it end-to-end:

- getCapabilities(): supportsFunctions: true + 'tool_calling' feature
- generateStreamAsync(): forwards options.tools in Responses API flat
  format ({type, name, description, parameters})
- parseCodexSSEStream(): accumulates function_call output items from
  response.output_item.done events; all completion paths yield toolCalls
- generateUncached(): extracts tool calls from stream, sets
  finishReason: 'tool_calls' when present

Tests: 47/47 pass (8 new tool call tests added)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove explicit `Record<string, unknown>` annotation on the `.map()`
callback parameter — TypeScript infers the correct `Tool` type from
`options.tools`, making the annotation incorrect and causing a build error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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