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>
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket C of the v1.4.3 polish pass.
Three independent bugs in the KI-provider settings, fixed together
because they all live on the same panel:
1. **Selecting "OpenAI" left no way to provide a key.** The schema
stored Anthropic and Local API keys, but OPENAI was a valid
`aiProvider` value with no matching credential column — the
resolver silently fell through to the admin-level OpenAI key. So
a self-hosted user who picked OpenAI and typed nothing got the
admin's key (might not exist), and a user who *wanted* their own
OpenAI key had no UI surface to enter it.
Migration `0026_user_ai_openai_key` adds `users.ai_openai_key_
encrypted`. The resolver's `OPENAI` branch now picks up the
user-level key first and only falls back to admin if the user
slot is empty. The settings UI grew an OpenAI-key field gated on
`provider === "OPENAI"` mirroring the Anthropic flow. The
`/api/ai/test` override schema accepts `openaiKey` so the
"Verbindung testen" button can validate an unsaved key without
persisting plaintext.
2. **Model field was a freetext Input, easy to typo.** Replaced
with a `<select>` populated from `MODEL_PRESETS` per provider:
gpt-4o / gpt-4o-mini / gpt-4-turbo / gpt-5 / o3-mini for OPENAI;
claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / claude-
3-5-sonnet-latest for ANTHROPIC; llama3.1:8b / llama3.1:70b /
mistral / qwen2.5 for LOCAL. An "Eigenes…" sentinel still
surfaces the freetext Input so power users can target preview
models or fine-tunes. The dropdown disables when no provider is
chosen — saves the user from filling a model that won't be used.
3. **"Mit ChatGPT verbinden" silently bounced to chatgpt.com login
with no OAuth grant.** The authorize URL was built without a
`client_id` parameter, so chatgpt.com couldn't recognise the
request as an OAuth handshake — it just rendered the regular
login form. The user came back, nothing connected, the button
"did nothing".
Added `CODEX_OAUTH_CLIENT_ID` env var. The URL builder now
throws `CodexOAuthNotConfiguredError` if it's unset; the
authorize route maps that to a 503 with an actionable message
instead of dispatching the dead URL. The settings card reads
`codexOauthConfigured` from `/api/insights/settings` and hides
the Connect button when the flag is false, replacing it with a
tiny italicised "ChatGPT-OAuth ist auf dieser Instanz nicht
konfiguriert — nutze stattdessen einen eigenen API-Key unten."
`CHATGPT_OAUTH` was also removed as a manually-selectable
provider option in the dropdown — the top connect button is the
path; having both confused the affordance.
Refresh + token-exchange both forward the client_id now too. Test
fixture seeds `process.env.CODEX_OAUTH_CLIENT_ID="test_client_id"`
in `beforeEach` so the existing URL-shape assertion still works,
plus a new test asserts the unset case throws.
Tests + typecheck green (670/670 — one new assertion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(settings): introduce dynamic-route shell + redirect /settings → /settings/account
Splits the 3218-LOC /settings monolith into a routing surface only — no
section content moves in this PR. Each of the eight slugs (account,
integrations, notifications, dashboard, ai, api, advanced, about) now lives
at its own /settings/[section] route and renders inside a new
<SettingsShell> sidebar+main layout. Every section currently shows a
placeholder ("Moving here with the next release update") so reviewers can
validate the URL structure in isolation. The historic monolith is preserved
verbatim as page.legacy.tsx for section-by-section extraction by the
follow-up PRs (A2-account, A2-about, A2-ai, A2-integrations,
A2-notifications, A2-rest).
Behaviour change: existing entry points (sidebar nav, topbar dropdown,
onboarding hand-off, dashboard checklist, Withings callback, Codex
callback) now point at /settings/<slug> instead of /settings#anchor. The
/settings root is a 308 (permanentRedirect) to /settings/account so old
bookmarks and external links keep working.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(settings): route every Codex OAuth callback redirect to /settings/integrations
Review-pass H1: only the late success/failure paths in the Codex OAuth
callback were updated to point at the new section route. The four early
branches — rate-limited, provider-supplied `?error=...`, missing params,
invalid state — still redirected to `/settings?codex_error=...`.
After the section split that path 308-redirects to `/settings/account`,
where the Codex error toast/banner doesn't live. Users hitting any of
those edge cases would land on the wrong screen with the error code
silently lost.
All six branches now go to `/settings/integrations?codex_error=...`,
matching the success and token-exchange-failure paths and the existing
Withings callback shape.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- H1: Add checkRateLimit to OAuth authorize/callback/disconnect + admin AI settings
- H2: Fix decrypt("") crash in provider.ts onTokenRefresh by using decrypt() directly
- H3: Truncate error messages in codex-oauth, codex-client, openai-client to prevent leaking upstream response bodies
- M5: Fix keyPreview to show last 4 chars of decrypted key instead of ciphertext prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>