CORS
Lugas ships an app-level, opt-in CORS policy. The safe default is no
cross-origin access: an app without a cors key sends no CORS headers and
behaves byte-identically to pre-M8 releases. When configured, the policy is
enforced at the compile boundary — every compiled handler and the serve-time
fallback are wrapped once during defineApp() — so Bun’s native router
(ADR-0004) stays the
request-path router and 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; see defaults below 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: { "/api/items": { GET: route({ handler: () => json(200, { items: [] }) }), POST: route({ body: itemSchema, handler: (ctx) => json(201, ctx.body) }), }, },});Origin decisions (fail-closed)
Section titled “Origin decisions (fail-closed)”origin accepts:
- an exact origin string (
"https://app.example.com") — echoed when the request’sOriginmatches exactly (scheme + host + port); - an allowlist array of such strings —
"*"cannot be mixed with concrete origins (LUGAS_CORS_002); - the wildcard
"*"— sendsAccess-Control-Allow-Origin: *; never combinable withcredentials: true(LUGAS_CORS_003); - a callback
(origin, request) => boolean | string, sync or async:trueechoes the request origin, a non-empty string echoes that canonical origin,false(or an empty/"*"-under-credentials result) denies.
Denied requests — and requests without an Origin header — receive no
Access-Control-* headers. They are not blocked: the response keeps its
normal status; the browser enforces the denial.
What configured apps guarantee
Section titled “What configured apps guarantee”Vary: Originon every response — merged with any handler-setVary, never duplicated — including denies, errors (onError500), not-found 404s, and startup-gate 503s, so shared caches cannot serve one origin’s authorization to another.- Preflight interception:
OPTIONSrequests carryingAccess-Control-Request-Methodare answered204before application handlers — including paths with no declaredOPTIONSentry (they reach the wrapped fallback through Bun’s own routing). Allowed preflights get the full header set; denied ones get204withVaryonly, so the browser fails them. PlainOPTIONS(no preflight marker) passes through to application handlers with headers applied. - Defaults:
methodsdefaults toGET, HEAD, POST, PUT, PATCH, DELETE(OPTIONSis implied by the mechanism);allowedHeadersdefaults to reflectingAccess-Control-Request-Headers. When configured, both gate strictly; a preflight approving a method the route then 404s is possible — authorization is unaffected, only preflight precision (see ADR-0022). - Coverage: Lugas
route()descriptors, native function handlers (path-level and per-method), any-method entries, modules, the not-found fallback, and a user-suppliedserve({ fetch })all enforce the same policy.
Scope boundary: pipeline-bypass values (fail-closed)
Section titled “Scope boundary: pipeline-bypass values (fail-closed)”Static route values bypass the framework response pipeline — their bodies
cannot be re-wrapped without losing native serving semantics. Configuring
cors alongside them is rejected at startup (LUGAS_CORS_004):
- static
Responsevalues andBun.file()/Blobvalues in any route position; - native
{ dir }mounts; assetsconfiguration (ADR-0018).
Convert such routes to handlers, or serve static content from a separate app
without cors. The rejection is deliberate and reversible; silently partial
enforcement is not.
Manifest
Section titled “Manifest”lugas-manifest-v1 records routing, not policy: CORS adds no routes and
changes no manifest facts.
Evidence
Section titled “Evidence”Behavior and diagnostics are pinned by tests/cors/cors.test.ts and
tests/cors/config.test.ts; implementation evidence lives in
docs/reports/issues/M8-001.md.