Secure-by-default
Daloy is the first release in the “secure-by-default” series. It flips secure headers and cross-origin write protection on by default, adds a per-route content type opt-in, and keeps a single master escape hatch (secureDefaults: false) plus per-feature opt-outs for the rare cases where you genuinely need the old behavior.
What flipped
1. secureHeaders() is now auto-applied
Every new App() instance ships secureHeaders() with the same sensible defaults the middleware has always had: HSTS, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, a strict Referrer-Policy, and a baseline CSP. No code change required.
If you call app.use(secureHeaders(...))with your own configuration, the auto-installed instance is automatically removed so your overrides win instead of being silently shadowed by the framework's defaults.
Want the headers configured at construction time instead? Pass a secureHeaders object to new App():
To opt out entirely (e.g. you serve content from a CDN that injects its own headers):
2. Cross-origin POST / PUT / PATCH / DELETE require cors()
State-changing requests carrying an Origin header from a different origin than the request URL are now rejected with 403 problem+json unless the matched route has a cors() policy that allows that origin. Read-only methods (GET, HEAD, OPTIONS), same-origin requests, and requests without an Origin header (or with Origin: null from a sandboxed iframe) pass through unchanged.
Per-route opt-in works too — register the cors() hook on the specific routes that need it via route({ hooks: cors({...}) }).
To disable the guard entirely (you handle cross-origin admission another way, e.g. via csrf() with the fetch-metadata strategy):
3. Per-route accepts field
New route({ accepts: [...] }) field overrides the global allowedContentTypes allowlist for a single route. Useful for legacy form-encoded webhook receivers without loosening the default allowlist for the rest of your app.
The master escape hatch
If you're upgrading from 0.15.x and need to ship the upgrade without any behavior changes, pass secureDefaults: false to restore the pre-0.16 behavior wholesale:
This is intentionally one-shot: there is no per-feature granular master flag because the per-feature opt-outs already exist (secureHeaders: false, corsCrossOriginGuard: false). Use secureDefaults: false as a time-boxed migration hatch, not a permanent posture.
Detection markers (advanced)
The framework detects secureHeaders() and cors() registration via two exported symbols. If you wrap these middleware in your own helpers, stamp the marker on your returned hooks to get the same behavior:
Migration checklist
- Audit any custom
secureHeaders()call sites. Behavior is the same — the auto-installed instance is automatically replaced when you register your own. - Audit any cross-origin
POST/PUT/PATCH/DELETEtests / integrations. Registercors()(recommended) or passcorsCrossOriginGuard: false(if you handle cross-origin admission viacsrf({ strategy: 'fetch-metadata' }), for example). - For legacy form-encoded routes, add
accepts: ["application/x-www-form-urlencoded"]on the route definition. - If you must ship the upgrade with zero behavior change while you triage, set
secureDefaults: falseas a temporary escape hatch.
What's not in this slice
The full secure-defaults plan in the roadmap lists many additional flips (CSP nonces default-on, per-content-type body caps, response-schema validation in development, conditional/openapi.json in production, frame-ancestors 'none' as immovable, trailing-slash canonicalization, etc.). Those will land in additive 0.16.x patches and follow-up minor releases. The four-item slice above is what shipped first because it is the highest-impact-per-line-of-breaking-change subset.