fix(ai): Codex OAuth flow — correct issuer + api-key exchange

The "Connect with ChatGPT" button never actually completed an OAuth
flow. The implementation hit `chatgpt.com/authorize` and
`chatgpt.com/oauth/token`, which are not OAuth endpoints — ChatGPT
silently rendered its normal web app, dropped our params on the
floor, and the user landed on a regular logged-in chat session with
nothing else happening. There was no error and no callback, which is
why the regression survived multiple releases.

The canonical issuer for ChatGPT-OAuth is `auth.openai.com`. The
official `openai/codex` CLI confirms this in
`codex-rs/login/src/auth/manager.rs` and `codex-rs/login/src/server.rs`.
The flow is also slightly more involved than a vanilla PKCE
authorisation:

  1. PKCE authorize against `auth.openai.com/oauth/authorize` with the
     `openid profile email offline_access api.connectors.read
     api.connectors.invoke` scope set, plus `id_token_add_organizations`
     and `codex_cli_simplified_flow` flags so the resulting `id_token`
     carries the ChatGPT-subscription claims.
  2. Token exchange against `auth.openai.com/oauth/token` (form-
     urlencoded body, NOT JSON) yields `{ id_token, access_token,
     refresh_token }`.
  3. **Second token exchange** with grant
     `urn:ietf:params:oauth:grant-type:token-exchange`, swapping the
     `id_token` for an OpenAI API key (`requested_token=openai-api-key`).
     This key bills against the ChatGPT subscription rather than a
     separate API plan — exactly what the v1.2/v1.3 design intended.
  4. HealthLog stores the API key in `codexAccessTokenEncrypted`
     (re-purposing the existing column to avoid a migration) and the
     OAuth refresh token in `codexRefreshTokenEncrypted`. When the
     key is about to age out we refresh + re-exchange transparently
     before the next request.

The downstream provider is now a regular `OpenAIClient` tagged with
`providerType: "codex"` so logs and analytics still distinguish
ChatGPT-subscription billing from plain BYOK. The bespoke
`codex-client.ts` (which hit the chatgpt.com/backend-api/codex/responses
endpoint with the OAuth access token) is still on disk for now so the
prisma columns and the provider tag stay stable, but is unreferenced
by the active path.

The public Codex CLI client ID `app_EMoamEEZ73f0CkXaXp7hrann` is
shipped as a default; operators who registered a private OAuth app
with OpenAI can override via `CODEX_OAUTH_CLIENT_ID`. The
`isCodexOAuthConfigured()` gate is a no-op now because the default
always has a value, so the UI surface no longer silently hides the
"Connect with ChatGPT" button.

Tests cover the new shape end-to-end: PKCE generation, the
authorize-URL parameters, the form-encoded token exchange, the
api-key exchange, the refresh path, and the encrypt/decrypt
round-trip. The provider integration test is updated to expect a
`providerType: "codex"` OpenAIClient instead of the legacy
`CodexClient`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 09:31:13 +02:00
1 parent 3bca8267cc
commit 2f7f487228
10 files changed
+472 -208

No files matched your search

+34 -34
View File
@@ -43,59 +43,59 @@ Seiten zu Native-API, Skalierung, Dashboard-Customization.
### Tier 1 — release blockers (`docs/audit/v146-findings.md`)
| ID | Commit | Summary |
|----|--------|---------|
| T1+T2 | `8aae7d6` | tile fill + muted-foreground hierarchy |
| T3 | `a75fbc6` | primary recommendation parses chart tokens |
| T4 | `eba898f` | aiBaseUrl provider-leak guard + tests |
| T5 | `c8ee28d` | insights/generate parse error 502 → 422 |
| T6 | `4aeb8c9` | admin status-card hrefs + tightened test |
| T7 | `31959e4` | bug-report toggle now blocks /api/feedback + UI |
| T8 | `c3ca861` | data-wipe preserves AuditLog + scope copy |
| T9 | `1adda80` | per-card insight window 360 daily + 24 monthly across 7 generators |
| ID | Commit | Summary |
| ----- | --------- | ------------------------------------------------------------------ |
| T1+T2 | `8aae7d6` | tile fill + muted-foreground hierarchy |
| T3 | `a75fbc6` | primary recommendation parses chart tokens |
| T4 | `eba898f` | aiBaseUrl provider-leak guard + tests |
| T5 | `c8ee28d` | insights/generate parse error 502 → 422 |
| T6 | `4aeb8c9` | admin status-card hrefs + tightened test |
| T7 | `31959e4` | bug-report toggle now blocks /api/feedback + UI |
| T8 | `c3ca861` | data-wipe preserves AuditLog + scope copy |
| T9 | `1adda80` | per-card insight window 360 daily + 24 monthly across 7 generators |
### New feature
| Commit | Summary |
|--------|---------|
| Commit | Summary |
| --------- | --------------------------------------------------------------------- |
| `6a64df0` | chart bucketing for ranges > 1y (weekly / monthly aggregation + chip) |
### Tier 2 — polish
| Commit | Summary |
|--------|---------|
| Commit | Summary |
| --------- | --------------------------------------------------------------------------------- |
| `fda8dd8` | trend-card P1-P5 (tabular nums, KPI typography, padding parity, always-show avgs) |
| `dc6db82` | P6 + P10 (visible muted subtitle, medications card padding) |
| `5c884b3` | P7-P9 (mobile nav buffer, trend-hint title, font-mono drop) |
| `e903d9a` | P11 — redactSecrets sk-/sk-ant- |
| `c7b6005` | P12 — idempotency body-content guard |
| `4a159d2` | P13 — rate-limit moved below cache return |
| `dcc697c` | P14 — codex-client structured errors |
| `89b5b80` | P15+P16 — drop unreleased presets, i18n the AI section |
| `505f318` | P17 — feedback badges → dracula tokens |
| `86a4b52` | P18 — danger-zone colour follows mutation state |
| `2654337` | P19 — useSystemStatus / useAdminSettings isError UI |
| `dc4507a` | P20 — status-overview Promise.allSettled |
| `dc6db82` | P6 + P10 (visible muted subtitle, medications card padding) |
| `5c884b3` | P7-P9 (mobile nav buffer, trend-hint title, font-mono drop) |
| `e903d9a` | P11 — redactSecrets sk-/sk-ant- |
| `c7b6005` | P12 — idempotency body-content guard |
| `4a159d2` | P13 — rate-limit moved below cache return |
| `dcc697c` | P14 — codex-client structured errors |
| `89b5b80` | P15+P16 — drop unreleased presets, i18n the AI section |
| `505f318` | P17 — feedback badges → dracula tokens |
| `86a4b52` | P18 — danger-zone colour follows mutation state |
| `2654337` | P19 — useSystemStatus / useAdminSettings isError UI |
| `dc4507a` | P20 — status-overview Promise.allSettled |
### CI / housekeeping
| Commit | Summary |
|--------|---------|
| `bcd1de4` | drop e2e pnpm-version override (was failing every run) |
| Commit | Summary |
| --------- | ------------------------------------------------------------- |
| `bcd1de4` | drop e2e pnpm-version override (was failing every run) |
| `46e686f` | switch e2e mobile project to chromium + repair spec flakiness |
| `02b9955` | repo-wide prettier sweep before tag |
| `ecca54d` | `.planning` bootstrap |
| `02b9955` | repo-wide prettier sweep before tag |
| `ecca54d` | `.planning` bootstrap |
### Multi-agent QA follow-ups
| Commit | Summary |
|--------|---------|
| Commit | Summary |
| --------- | -------------------------------------------------------------------------------------------------------------------------- |
| `6757518` | qa pass — orange→dracula tokens, status-card cta copy honest, idempotency `sk-` regex tightening with false-positive tests |
### Release
| Commit | Summary |
|--------|---------|
| Commit | Summary |
| --------- | -------------------------------------------------------- |
| `a852612` | `chore(release): v1.4.6` (package.json bump + CHANGELOG) |
## What was deferred (v1.5 backlog)