diff --git a/README.md b/README.md index 58bc242..0b039fa 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,13 @@ curl -X POST localhost:3737/v1/import \ | `AUTH_JWT_ISSUER` / `AUTH_JWT_AUDIENCE` | _(unset)_ | Required `iss` / `aud` enforced on tokens (recommended) | | `LOG_LEVEL` | `info` | Log verbosity: `trace`…`fatal`, or `silent` | +CORS is browser-ready out of the box: every REST method (`GET`/`POST`/`PUT`/`PATCH`/ +`DELETE` + `OPTIONS` preflight) is allowed cross-origin, the API's request headers +(`Authorization`, `Content-Type`, `If-Match`, `X-Project-Id`, `X-User-Token`) are +accepted, and its response headers (`ETag`, `X-Request-Id`, the rate-limit headers, +`Content-Disposition`) are exposed so the SDK can read them. `CORS_ORIGIN` only +restricts *which origins* may call the instance. + ### Data store — SQLite (default) or Postgres Data lives in a single SQLite file under `DATA_DIR` out of the box. For durable or diff --git a/src/server.ts b/src/server.ts index 00650dd..02ac3ed 100644 --- a/src/server.ts +++ b/src/server.ts @@ -158,7 +158,25 @@ export async function buildServer(opts: BuildOptions = {}): Promise { const denied = await fetch(url, { headers: { origin: 'https://evil.example' } }); assert.notEqual(denied.headers.get('access-control-allow-origin'), 'https://evil.example'); + + // Preflight allows every method the API uses + the custom request headers, + // and the response exposes ETag etc. so the SDK can read them cross-origin. + const preflight = await fetch(`http://127.0.0.1:${port}/v1/data/notes/x`, { + method: 'OPTIONS', + headers: { + origin: 'https://good.example', + 'access-control-request-method': 'PATCH', + 'access-control-request-headers': 'authorization,if-match,x-user-token', + }, + }); + const allowMethods = preflight.headers.get('access-control-allow-methods') ?? ''; + for (const m of ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) assert.match(allowMethods, new RegExp(m)); + const allowHeaders = (preflight.headers.get('access-control-allow-headers') ?? '').toLowerCase(); + for (const h of ['authorization', 'if-match', 'x-user-token', 'x-project-id']) assert.match(allowHeaders, new RegExp(h)); + + const exposed = (allowed.headers.get('access-control-expose-headers') ?? '').toLowerCase(); + for (const h of ['etag', 'x-request-id']) assert.match(exposed, new RegExp(h)); } finally { await server.close(); }