Skip to main content

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 on api.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 in api/ must handle CORS manually. Follow this pattern:
Key rules:
  1. Every response must include ...cors in its headers — including errors, rate-limit 429s, and 500s.
  2. Preflight (OPTIONS) must return 204 with CORS headers and no body.
  3. getCorsHeaders(req, methods) — pass a custom methods string if the endpoint supports more than GET, 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:
  1. Update the origin policy in api/_cors.js, server/cors.ts, and workers/api-cors-preflight/src/index.js. For an app host, update APP_ORIGIN_PATTERN and the app-host inventory in convex/payments/returnUrlOrigin.ts. The Worker overrides CORS on api.worldmonitor.app; its deployment must be updated with the functions.
  2. Update the behavioral parity tests in tests/cors-fail-closed.test.mts.
  3. 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-Type
  • Authorization
  • X-WorldMonitor-Key (API key for desktop/third-party access). See API Key Gating for key management details.
  • X-Api-Key
  • X-Widget-Key
  • X-Pro-Key
  • X-WorldMonitor-Desktop-Timestamp
  • X-WorldMonitor-Desktop-Signature
  • Idempotency-Key
  • Mcp-Session-Id
  • MCP-Protocol-Version
  • Last-Event-ID
To allow additional headers, update 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.