> ## Documentation Index
> Fetch the complete documentation index at: https://www.worldmonitor.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CORS

> How cross-origin request protection works in World Monitor and what to do when adding new API endpoints — allowlists, preflights, and Worker gate.

***

## 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`.

| File | Used by | Methods |
| - | - | - |
| `api/_cors.js` | Standalone edge functions (`api/*.js`) | `GET, OPTIONS` (configurable) |
| `server/cors.ts` | Sebuf gateway (`api/[domain]/v1/[rpc].ts`) | `GET, POST, OPTIONS` |
| `workers/api-cors-preflight/src/index.js` | Cloudflare Worker for `api.worldmonitor.app` | Union of supported API methods |

## Allowed Origins

The production policies use the same origin patterns:

| Pattern | Matches |
| - | - |
| HTTPS app hosts | `worldmonitor.app`, `www.`, `app.`, `api.`, `tech.`, `finance.`, `commodity.`, `happy.`, `energy.` |
| `worldmonitor-*-eliewm.vercel.app` | Vercel preview deploys (literal `-eliewm` suffix) |
| `localhost:*` / `127.0.0.1:*` | Local development only when `NODE_ENV !== "production"` |
| `tauri.localhost:*` / `*.tauri.localhost:*` | Desktop app (Tauri v2) |
| `tauri://localhost` / `asset://localhost` | Desktop app (Tauri v2 asset protocol) |

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:

```js theme={null}
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';

export default async function handler(req) {
  const cors = getCorsHeaders(req);

  // 1. Block disallowed origins
  if (isDisallowedOrigin(req)) {
    return new Response(JSON.stringify({ error: 'Forbidden' }), {
      status: 403,
      headers: { 'Content-Type': 'application/json', ...cors },
    });
  }

  // 2. Handle preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, { status: 204, headers: cors });
  }

  // 3. Spread cors into every response
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json', ...cors },
  });
}
```

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](/docs/api-key-deployment) 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](/docs/usage-errors) and [Rate limits](/docs/usage-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](/docs/relay-parameters) for details.
