Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,25 @@ export async function buildServer(opts: BuildOptions = {}): Promise<FastifyInsta
// Open CORS by default: arbitrary AI-generated sites need to call this API
// cross-origin. Set CORS_ORIGIN to a comma-separated allowlist to lock it down.
const corsOrigin = opts.corsOrigin !== undefined ? opts.corsOrigin : config.corsOrigin;
await app.register(cors, { origin: corsOption(corsOrigin) });
// Allow every HTTP method the API uses for cross-origin browsers, accept the
// custom request headers it reads, and expose the response headers clients need
// (ETag for If-Match optimistic concurrency, X-Request-Id, rate-limit headers,
// Content-Disposition for downloads) — @fastify/cors doesn't expose these by default.
await app.register(cors, {
origin: corsOption(corsOrigin),
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Authorization', 'Content-Type', 'If-Match', 'X-Project-Id', 'X-User-Token', 'X-Request-Id'],
exposedHeaders: [
'ETag',
'X-Request-Id',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
'Retry-After',
'Content-Disposition',
],
maxAge: 86400, // cache preflight for a day
});
await app.register(multipart, { limits: { fileSize: config.maxUploadBytes } });
await app.register(websocket);

Expand Down
18 changes: 18 additions & 0 deletions test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,24 @@ test('cors: CORS_ORIGIN restricts which origins are reflected', async () => {

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();
}
Expand Down
Loading