cors
Since v2.38.0 · Top-level declaration
Grammar
cors <Name> {
allow_origins: [<origin-or-glob>, ...] # optional — "*" or "https://exact.com" or "https://*.host.com"
allow_methods: [<Method>, ...] # optional — closed catalog: GET|POST|PUT|PATCH|DELETE
allow_headers: [<"Header-Name">, ...] # optional — request headers the preflight may allow
allow_credentials: <true|false> # optional — forbidden together with any-origin (axon-T853)
max_age: <duration> # optional — "3600s"; Access-Control-Max-Age
expose_headers: [<"Header-Name">, ...] # optional — response headers browser JS may read
}
axonendpoint <Name> {
...
cors: <CorsName> # optional — "" / absent = no CORS headers, ever
}
# An unknown field in a `cors { }` block is a HARD PARSE ERROR (unlike
# `shield`'s lenient axon-W010 record-and-skip) — CORS is security-
# relevant, so a typo'd field must not silently produce a permissive
# policy.
cors declares a browser-origin policy — the
Access-Control-Allow-* response headers a cross-origin
browser request needs — as a named, top-level declaration
referenced from any number of axonendpoints via cors: <Name>, mirroring shield: exactly.
Everywhere else, CORS is one setting for the whole server,
configured once. That is wrong for a multi-tenant platform:
different tenants deploy different bundles, and a single
static, router-wide CORS policy cannot express "tenant A's
/api/chat allows app.tenant-a.com; tenant B's /api/chat
allows something else." cors resolves per the tenant's live
deployed bundle, at request time — the shape a single
process-wide knob cannot express.
The four laws
- Absent means no CORS headers, ever — secure by default.
An
axonendpointthat never declarescors:stays same-origin-only without any action required. - Wildcard + credentials is a compile-time error
(
axon-T853).allow_origins: ["*"]combined withallow_credentials: trueis the CORS specification's own forbidden pairing — browsers already reject it silently at runtime;corscatches it before deploy, naming the spec rule. - The origin glob is closed and decidable
(
axon-T854). An origin is either an exact literal or a SINGLE leading-wildcard host label ("https://*.kivi.io") — no full regex. - Same path, same policy (
axon-T857). Everyaxonendpointsharing apath:(differing only bymethod:) must reference the SAMEcors:declaration, or all leave it unset — a browser's preflight is per-path, not per-method, so divergent policies on one path are inherently ambiguous.
allow_methods reuses the closed axonendpoint method
catalog (axon-T855 on an unknown value); an undeclared
cors: reference is axon-T856. The whole-program obligation
— every reference resolves, no wildcard+credentials survivor,
no cross-method conflict — is proof-carried as
CorsPolicyConsistency, re-derived independently by the
checker, never trusted from the producer.
Example
cors PublicWebCors {
allow_origins: ["https://app.example.com", "https://*.kivi.io"]
allow_methods: [GET, POST]
allow_headers: ["Content-Type", "Authorization"]
allow_credentials: true
max_age: 3600s
}
axonendpoint ChatAPI {
method: post
path: "/v1/chat"
execute: Chat
output: FlowEnvelope<ChatOutput>
cors: PublicWebCors
}
See also: shield (the compliance/safety counterpart — cors
governs WHO may read a response cross-origin in a browser;
shield governs WHAT may leave the boundary at all).