Search docs

Jump between documentation pages.

secureDefaults enforcement

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:

ts
// ❌ refuses-to-construct in production
new App({ env: "production", secureDefaults: false });

// ✅ explicit two-step opt-in (and you still get the audit log)
new App({
  env: "production",
  secureDefaults: false,
  acknowledgeInsecureDefaults: true,
});

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 secureHeaders install
  • cross-origin guard for state-changing requests
  • crash-on-unhandled-rejection (production)
  • first-request X-Forwarded-* / trustProxy guard
  • session() + state-changing route requires csrf() 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.

ts
// ❌ refuses at construction
createJwtSigner({
  alg: "HS256",
  key: new Uint8Array(16),        // 16 bytes — too short
  maxLifetimeSeconds: 60,
});
// JwtError [weak_hs_secret]: jwt(): HS256 secret must be at least 32 bytes
// (RFC 7518 §3.2); got 16.

createJwtVerifier({
  algorithms: ["HS384"],
  key: new Uint8Array(20),        // 20 bytes — too short
});
// JwtError [weak_hs_secret]: jwt(): HS* secret must be at least 32 bytes
// (RFC 7518 §3.2); got 20.

// ✅ 32 bytes from a CSPRNG
const key = new Uint8Array(32);
crypto.getRandomValues(key);
createJwtSigner({ alg: "HS256", key, maxLifetimeSeconds: 60 });

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:

ts
// ❌ refuses
secureHeaders({
  frameOptions: false,
  contentSecurityPolicy: false,
});
// Error: secureHeaders(): refusing to construct with both frameOptions: false
// AND no CSP frame-ancestors directive — that disables every clickjacking
// defense the helper provides.

// ❌ refuses (CSP string without frame-ancestors directive)
secureHeaders({
  frameOptions: false,
  contentSecurityPolicy: "default-src 'self'",
});

// ✅ explicit frame-ancestors directive in the CSP carries the defense
secureHeaders({
  frameOptions: false,
  contentSecurityPolicy: "default-src 'self'; frame-ancestors 'none'",
});

// ✅ directives-object form is also recognised
secureHeaders({
  frameOptions: false,
  contentSecurityPolicy: {
    "default-src": ["'self'"],
    "frame-ancestors": ["'none'"],
  },
});

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 authentication is enforced on the @daloyjs org; 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-required is set on @daloyjs/core and create-daloy; OIDC trusted publishing from the protected npm-publish environment 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-publish Environment 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.