secureDefaults enforcement
Think of it like… a smoke alarm that beeps when it smells smoke and refuses to be permanently silenced. You can mute it for one cooking session by holding a clearly labelled button (acknowledgeInsecureDefaults: true), but it logs that you did and re-arms itself. The alarm also refuses to be installed at all if you try to give it a 4-byte battery (a JWT secret shorter than 32 bytes).Daloy ships a focused slice of cross-cutting bake-ins from the secure-by-default initiative. Three items are implemented now; the remaining cross-cutting bullets (single-source helpers for cookie / client IP / time-claim / secret comparison, the __Secure- cookie without TLS refuse-to-boot, the daloy doctor --audit-secrets subcommand, and the zero-runtime-dependency governance CI grep gate) remain tracked on the roadmap and will land in subsequent additive 0.26.x releases.
1. secureDefaults: false master-flag enforcement
The wholesale escape hatch for the entire secure-by-default surface now refuses-to-construct in production unless you also pass acknowledgeInsecureDefaults: true. This closes the well-documented "developer flipped the flag off while debugging and shipped to production" footgun by forcing an explicit two-step opt-in:
- 01constructnew App({ env: 'production' })with secureDefaults: false
- 02no ackRefuses to constructacknowledgeInsecureDefaults missing
- 03ack: trueConstructs, audit-loggedevent: "secure_defaults.disabled"
Any time the flag is off, a once-per-process error log is emitted with event: "secure_defaults.disabled" enumerating every default it disabled, so the blast radius is loud at boot even when the option was set deep in shared configuration:
- auto
secureHeadersinstall - cross-origin guard for state-changing requests
- crash-on-unhandled-rejection (production)
- first-request
X-Forwarded-*/trustProxyguard session()+ state-changing route requirescsrf()boot guard- weak session secret refuse-to-boot
cors({ origin: '*' })refuse-to-boot- anonymous stateful plugin refuse-to-boot
Per-feature opt-outs (secureHeaders: false, corsCrossOriginGuard: false, crashOnUnhandledRejection: false, trustProxy: false, csrf: "off") remain available without the production refusal, prefer those when you only need to disable one default rather than the whole surface. Tests can reset the audit-log latch via the exported _resetInsecureDefaultsLogForTests() helper, mirroring the existing _resetCrashHandlersForTests pattern.
2. JWT HS-secret length refuse-to-construct (RFC 7518 §3.2)
createJwtSigner() and createJwtVerifier() now refuse Uint8Array HS-shaped secrets shorter than 32 bytes at construction time. RFC 7518 §3.2 sets the floor at the hash output size (32 bytes for HS256), and Daloy applies the same floor to HS384 and HS512 because a shorter key does not buy a stronger HMAC, it only reduces the effective entropy.
3. secureHeaders() refuses dual framing-defense disable
secureHeaders() ships two layered defenses against clickjacking: the X-Frame-Options header (legacy browsers) and a CSP frame-ancestors directive (modern spec). The helper now refuses to construct when both are disabled simultaneously, that combination silently re-opens the clickjacking surface the helper exists to close:
If you only want to disable one of the two defenses, keep the other one on, the helper's defaults already wire both layers, so the common case (no options passed) needs no changes.
4. Mandatory hardware-backed 2FA for publish access
Daloy's supply-chain posture now mandates hardware-backed 2FA for every contributor with publish access, documented in SECURITY.md as a release-checklist item:
- GitHub organization level:
Settings → Authentication security → Require two-factor authenticationis enforced on the@daloyjsorg; every account with write access must have a hardware-backed factor (passkey or security key, TOTP-only accounts are off-boarded). - npm registry level:
npm access 2fa-requiredis set on@daloyjs/coreandcreate-daloy; OIDC trusted publishing from the protectednpm-publishenvironment means publishes themselves carry no long-lived token, but every maintainer who can approve the environment still needs hardware-backed 2FA on the registry account. - Off-boarding: when a maintainer leaves rotation, their org membership, publish grants, and granular tokens are revoked in the same change.
- Release-checklist audit gate: before tagging a release the maintainer running the release verifies that every contributor who approved the
npm-publishEnvironment for that release has 2FA enabled at both levels (the mandatory-2FA audit gate).
What's next
The remaining cross-cutting bullets stay tracked on the roadmap and will land in subsequent 0.26.x additive patches: single source of truth for cookie writes / client IP / time-claim validation / secret comparison; the __Secure- cookie without TLS refuse-to-boot guard; the daloy doctor --audit-secretssubcommand; the zero-external-runtime-dependency governance CI grep gate; and the timing-safe-comparison CI grep gate. Together these items remove the last "developer remembered to do X but not Y" failure modes by making the framework's security surface internally self-consistent.