feat: per-key CORS origin allowlist for browser embedding#36
Open
wms2537 wants to merge 1 commit into
Open
Conversation
Adds an `allowed_origins` JSON column to api_keys so a key can be safely
embedded in a public widget — anyone scraping the key from the page can
still only use it from the listed origins.
Semantics:
null -> unrestricted (server-to-server, default; existing behavior)
[] -> locked, no browser may use this key
[...] -> only these origins may use this key from a browser
["*"] -> any browser origin
Server-to-server callers (no Origin header) bypass the allowlist entirely,
so server keys keep working without configuration.
API surface:
- New lib `cors.ts`: evaluateCors(), preflightResponse(), validateOriginList()
- validateRequest + MCP validateApiKey return allowedOrigins
- Chat, /api/v1/usage, every /api/v1/tools/*, and /mcp now:
* 403 with `origin_not_allowed` if Origin header is set and not in list
* Echo Origin in ACAO + Vary: Origin on every response
* Standardised OPTIONS via preflightResponse() reflecting requested headers
* Expose all rate-limit headers via Access-Control-Expose-Headers
- PATCH /api/keys/[id] supports updating { name, allowedOrigins }
- /dashboard/keys: new OriginsEditor with per-key add/remove/lock/unrestrict
- listApiKeys returns allowed_origins so the UI can render current state
Migration 0006_api_key_origins.sql applied to remote.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets a key be safely embedded in a public widget. Stacks on top of #34 (rate-limits) — base is
feat/rate-limitsso the diff stays clean.Why
A partner is building a chat widget on a GitHub Pages docs site that hits
/api/v1/chat/completions. Their static page can't keep an API key secret. Without origin restrictions, anyone scraping the page could lift the key. With this change, the key is bound to specific origins server-side: the browser still has to send the key, but the worker rejects it from any origin not on the allowlist (and the missing ACAO on a 403 means hostile pages can't even read the response).API model
allowed_originsvaluenull(default)[]["https://docs.example.com"]["*"]Server-to-server callers (no
Originheader) ignore the column entirely, so existing CLI / cron / SDK use is unaffected.What's new
0006_api_key_origins.sql—ALTER TABLE api_keys ADD COLUMN allowed_origins TEXTapps/web/src/lib/cors.ts—evaluateCors,preflightResponse,validateOriginList,parseAllowedOriginsvalidateRequest+ MCPvalidateApiKeycarryallowedOriginsthrough/api/v1/usage, every/api/v1/tools/*, and/mcp:origin_not_allowedif Origin is set and not on the allowlistAccess-Control-Allow-Origin, setVary: Origin, expose all rate-limit headers viaAccess-Control-Expose-HeaderspreflightResponse()reflecting the requested headers/api/keys/[id]accepts{ name?, allowedOrigins? }(owner-scoped)/dashboard/keysadds an "Allowed origins" editor per key with chip-style display, edit-in-place textarea, "unrestricted" shortcut, validation errors surfaced inlinelistApiKeysreturnsallowed_originsso the UI renders current stateLive verification
arbbuilderb25cf685-0941-4f96-a160-22cd805ec8f6)ethcluj) which is now bound tohttps://stylus-developers-guild.github.io:Test plan
cd apps/web && npm test→ 20/20 passnpx tsc --noEmitclean/dashboard/keys, addhttps://example.com, save, fetch with that Origin → 200 with reflected ACAOorigin_not_allowed🤖 Generated with Claude Code