Overview
Every API response must include CORS headers so browsers allow the frontend to read it. Standalone edge functions, the sebuf gateway, and the API Worker share the production origin allowlist. The Worker overrides CORS headers onapi.worldmonitor.app.
Allowed Origins
The production policies use the same origin patterns:
Trailing DNS dots and Google Translate rewrites of these app hosts are supported. Translated origins must use the default HTTPS port. Other subdomains do not inherit app-origin trust.
Requests from any other origin receive a 403 response when the handler calls
isDisallowedOrigin(req). Requests with no Origin header (server-to-server, curl) are allowed through — the isDisallowedOrigin check only blocks when an origin is present and not on the allowlist.
CORS is a browser response boundary, not credential isolation. The shared Domain=.worldmonitor.app session and legacy tester-key cookies also reach sibling hosts. Narrowing the origin allowlist does not change that cookie transport. Anonymous wm-session tokens do not provide Pro identity; privileged tester keys remain a separate credential.
Adding CORS to a New Edge Function
Every standalone edge function inapi/ must handle CORS manually. Follow this pattern:
- Every response must include
...corsin its headers — including errors, rate-limit 429s, and 500s. - Preflight (
OPTIONS) must return204with CORS headers and no body. getCorsHeaders(req, methods)— pass a custom methods string if the endpoint supports more thanGET, OPTIONS(e.g.,'POST, OPTIONS').
Sebuf Gateway (RPC Endpoints)
RPC endpoints defined in.proto files do not need manual CORS handling. The gateway (server/gateway.ts) calls getCorsHeaders() and isDisallowedOrigin() from server/cors.ts automatically for every request. CORS headers are injected into all responses including error boundaries.
Adding a New Allowed Origin
To allow a new origin:- Update the origin policy in
api/_cors.js,server/cors.ts, andworkers/api-cors-preflight/src/index.js. For an app host, updateAPP_ORIGIN_PATTERNand the app-host inventory inconvex/payments/returnUrlOrigin.ts. The Worker overrides CORS onapi.worldmonitor.app; its deployment must be updated with the functions. - Update the behavioral parity tests in
tests/cors-fail-closed.test.mts. - If the origin is a new production subdomain, also add it to the Cloudflare R2 CORS rules (see MEMORY.md notes on R2 CORS in the repo root).
Allowed Headers
Both implementations allow these request headers:Content-TypeAuthorizationX-WorldMonitor-Key(API key for desktop/third-party access). See API Key Gating for key management details.X-Api-KeyX-Widget-KeyX-Pro-KeyX-WorldMonitor-Desktop-TimestampX-WorldMonitor-Desktop-SignatureIdempotency-KeyMcp-Session-IdMCP-Protocol-VersionLast-Event-ID
Access-Control-Allow-Headers in both files.
Browser-visible response headers exposed via Access-Control-Expose-Headers include Mcp-Session-Id, WWW-Authenticate, Retry-After, X-Billing-Verification, the idempotency headers (Idempotency-Key, Idempotent-Replayed), Location, and both rate-limit header families — the IETF fields (RateLimit, RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) and the legacy X-RateLimit-* fields (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, plus X-RateLimit-Mode on fail-open limiter degradation) — so MCP clients can continue sessions, re-authenticate, respect backoff hints, self-throttle, tell a retryable billing-verification blip from a terminal lapse, and distinguish degraded limiter grants from healthy ones without parsing the response body. See Error handling and Rate limits.
Railway Relay CORS
The Railway relay (scripts/ais-relay.cjs) has its own CORS handling with the ALLOW_VERCEL_PREVIEW_ORIGINS env var. See RELAY_PARAMETERS.md for details.