Review fix round 1.
- Secret box v2 tokens: one HKDF key per purpose (byok, server-api-key,
mcp) and AES-GCM AAD bound to the row (user id + provider; server id +
field). MCP env and headers are sealed per field. open() refuses v1
tokens; the data upgrade re-seals v1 keys, v1 plaintext MCP values and
the draft secretCipher, and marks ai_secrets_v2 only when nothing
legacy is left (retried until the previous secret is back).
- Unreadable secrets are reported (admin: unreadableSecrets with server
and field; user: byok.keyUnreadable), logged at startup, and kept on
save unless replaced or cleared (clearApiKey, clearSecrets). The admin
and account pages show the warnings.
- allowedOrigins and provider resolution no longer open MCP secrets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
env/headers are sealed into secretCipher with the BYOK AES-GCM/HKDF box.
The admin GET returns masked values; a mask posted back for the same
server and key keeps the stored value, any other mask is a 400. The chat
and the test button use the stored values. upgradeAiData seals plaintext
v1 rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A user's own key goes only to https://api.anthropic.com, https://api.openai.com/v1
or an origin listed in STC_AI_ALLOWED_BASE_URLS / the server config's
allowedBaseUrls; anything else is a 400, and a stored key whose origin was
later removed stops being used.
- User-scope provider calls never follow redirects, and their errors reach the
user only as the HTTP status plus a fixed message; the full text is logged.
- The admin's server-scope provider may still use any base URL.
- Admin UI: allowed origins field; MCP stdio env editable, HTTP headers kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>