Skip to content
Rizky Zulkarnaen edited this page Sep 9, 2026 · 1 revision

CORS

Source of truth: docs/cors.md · ADR: ADR-0022

First-party, app-level, opt-in CORS. The safe default is no cross-origin access: an app without a cors key sends no CORS headers and behaves byte-identically to a pre-CORS app. When configured, the policy is enforced at the compile boundary — Bun's native router stays authoritative, no routes are synthesized.

import { defineApp, json, route } from "lugas";

defineApp({
  cors: {
    origin: "https://app.example.com", // exact string, allowlist, "*", or callback
    methods: ["GET", "POST"],          // optional; default GET/HEAD/POST/PUT/PATCH/DELETE
    allowedHeaders: ["Content-Type"],  // optional; default reflects the request
    exposedHeaders: ["X-Request-Id"],  // optional
    credentials: true,                 // optional; never with "*"
    maxAge: 600,                       // optional preflight cache (seconds)
  },
  routes: { /* ... */ },
});

Origin decisions (fail-closed)

  • Exact string — echoed when the request Origin matches exactly (scheme + host + port).
  • Allowlist array"*" cannot be mixed with concrete origins (LUGAS_CORS_002).
  • Wildcard "*" — sends Access-Control-Allow-Origin: *; never combinable with credentials: true (LUGAS_CORS_003).
  • Callback (origin, request) => boolean | string (sync or async) — true echoes the request origin; a non-empty string echoes that canonical origin; false (or "", or "*" under credentials) denies.

Denied requests — and requests without an Origin header — receive no Access-Control-* headers. The response keeps its normal status; the browser enforces the denial.

What configured apps guarantee

  • Vary: Origin on every response — merged with any handler-set Vary, never duplicated — including denies, onError 500s, not-found 404s, and startup-gate 503s.
  • Preflight interception: OPTIONS + Access-Control-Request-Method answered 204 before application handlers (including paths with no declared OPTIONS — they reach the wrapped fallback through Bun's own routing). Allowed → full header set; denied → 204 with Vary only. Plain OPTIONS passes through with headers applied.
  • Coverage: route() descriptors, native function handlers (path-level and per-method), any-method entries, modules, the not-found fallback, and a user-supplied serve({ fetch }).

Scope boundary (fail-closed)

Static route values bypass the framework response pipeline, so configuring cors alongside them is rejected at startup (LUGAS_CORS_004): static Response values, Bun.file()/Blob values, { dir } mounts, and assets configuration. Convert such routes to handlers or serve static content from a separate app without cors.

Notes

  • lugas-manifest-v1 records routing, not policy — CORS adds no routes and changes no facts.
  • Preflight Access-Control-Allow-Methods reflects the configured/default method set, not the target route's own methods (authorization is unaffected).
  • Pairs with Server-Sent Events (stream responses carry the policy) and the lugas/client/browser artifact (same-origin lane; cross-origin frontends need this policy).

Behavior and diagnostics are pinned by tests/cors/cors.test.ts and tests/cors/config.test.ts; evidence in docs/reports/issues/M8-001.md.

Clone this wiki locally