The gateway API key authorizer matched the presented token against every
configured key with `item.key === token`, a variable-time string
comparison that returns on the first differing byte. The repository
already avoids that shape for its other secrets: context-archive.ts,
management-server.ts and media/service.ts each compare tokens through a
local timingSafeEqual helper with a length guard.
Route the gateway API key lookup through the same helper shape so all
secret comparisons behave identically. The length guard is kept so a
token of a different length is rejected rather than making
timingSafeEqual throw.
Behaviour is unchanged: valid keys authorize, unknown keys still return
401 "Invalid API key.", expired keys still return 401 "API key is
expired.", and the refresh-on-miss reload path is preserved.
Local-agent login imports (Codex API et al.) declare the provider protocol
at the top level of the payload (protocol: "openai_responses") and carry no
capabilities array. parseProviders dropped that field, so the saved provider
ended up with neither protocol nor capabilities and the gateway fell back to
the default openai_chat_completions adapter. For Responses-only backends
(chatgpt.com/backend-api/codex) every request then 404s against a
non-existent /chat/completions route.
Synthesize a single capability from the top-level protocol (and the
provider's base URL) when no explicit capabilities are configured, reusing
the existing protocol normalization used for capability items. Explicit
capabilities still win. Fixes#1619.
The onboarding wizard and the Add/Edit Provider dialog render the same
AddProviderForm, but the wizard is wired up differently, and several
defects lived in those differences.
Model probe never fired during onboarding. The model-probe useEffect in
App.tsx guarded on `providerAddOpen` only, which is set true solely when
opening the manual Add/Edit Provider dialog. It is never set during the
onboarding wizard, so after entering an endpoint + API key on the Get
Started flow and reaching the "Pick models" step,
probeProviderCandidates() never fired and the model list stayed empty
with no loading state. Regressed in 9f704fc ("Guide onboarding through
granular provider setup steps"), which narrowed the guard from
`providerAddOpen || (activeView === "onboarding" && onboardingStep === "provider")`
down to `providerAddOpen`. Restore the broader guard so the probe runs
both when the dialog is open and when onboarding is on the provider step.
A rejected API key produced an empty model list with no feedback. Typing
an API key switches the probe from mode "protocols" to mode "models",
but the error-reporting branch was guarded on `probeMode !== "models"`,
so it never ran in exactly that mode. The backend still runs full
protocol probing in models mode, so a 401 came back as `supported: false`
with a real message that was then discarded. Report the failure, while
staying quiet when models were discovered, since a provider can expose a
working catalog while a protocol probe endpoint 404s.
"Added models" faked a loading state for data it already had. The panel
renders local draft state, yet showed a skeleton and hid its whole
toolbar whenever the catalog probe re-ran, leaving the header badge
reading a real count above shimmer rows and removing the "Custom model"
button, the only way to add models on a provider with no catalog. Keep
both panels' controls mounted, disabling the catalog search while
loading, which also removes a layout shift and mid-typing focus loss.
An empty catalog gave no way forward, so point at "Custom model" (en+zh).
Onboarding's "Check Connection" spent real credits with no confirmation.
The dialog wraps onCheck in a confirm step that warns about account
balance, lets the user pick models, and shows per-model results;
onboarding passed onCheckProvider straight through, so one click fired a
billable request against every configured model and discarded the
report. Extract that step as ProviderConnectivityCheckDialog and use it
from both surfaces.
ProviderConnectionStatusRow hardcoded bg-emerald-50 / border-emerald-200
/ bg-amber-50 with no dark variant, so the "Verify connection" status
chips rendered as bright light blobs on the dark card. Use alpha fills,
matching the pattern used elsewhere in the file.
The provider error banner was not announced; add role="alert".
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
Review on #1604: preferring `claudeAiOauth` was not enough. A record holding
only `mcpOAuth` entries still produced a token, because the fallback ran a
recursive `findOauthTokenSet()` over the whole record and `mcpOAuth`
per-plugin records carry their own `accessToken`. That imports a plugin
token as a Claude Code provider and recreates the 401 the change was meant
to avoid. It also short-circuited the plaintext-file fallback, so a usable
`~/.claude/.credentials.json` was ignored in favour of the plugin token.
Restrict both lookups to root-level token fields instead of reordering the
recursive search. Root-level excludes *any* nested container, so a future
sibling of `mcpOAuth` cannot reintroduce the hole, and the pre-`claudeAiOauth`
layout that kept tokens at the record root still resolves.
Extract `readOauthTokenSetFields()` out of `findOauthTokenSet()` in shared.ts
rather than duplicating the field list. `findOauthTokenSet()` keeps its
behaviour, so codex/grok/kimi/opencode/zcode are unaffected.
Apply the same restriction to the credentials-file path: Claude Code writes
that file with the identical record shape, mcpOAuth included, so the hazard
was present there too.
Adds the two regression tests from the review verbatim.
Refs #1601, #1567