The security helper surface: hardening primitives, the SSRF and open-redirect guards, cookie helpers, JWT/JWK verification, temporal claim assertions, security-scheme builders, sessions, and password hashing. Unless noted, these are exported from the root @daloyjs/core barrel (sessions and hashing also ship as their own subpaths). See the API reference overview for the module map.
Inbound primitives sanitize what arrives, egress guards constrain where your app can send users and requests, and the credential helpers verify who is calling.
contentDisposition() (since 1.3.7) builds a download header from an untrusted filename. It runs the name through sanitizeFilename(), emits an ASCII-only quoted filename fallback, and adds an RFC 8187 filename* parameter when the name is not plain ASCII. The result is always a valid Latin-1 header value, and it throws BadRequestError when the name sanitizes to nothing.
✓Service-to-service tokens between backends you own and deploy together.
✓Short-lived signed URLs for a download, an upload, or a one-time confirmation link.
✓Signing a webhook payload so the receiver can verify the sender.
✓Test fixtures and local development, where you need a valid token without reaching a provider.
You are building an identity provider
✗Minting an access or refresh token after checking a password you stored yourself.
✗Adding refresh-token rotation, reuse detection, and a revocation list on top of it.
✗Serving the public half of your signing key as a JWKS for other services to trust.
✗Adding sessions per device, step-up authentication, or sign-out everywhere.
The moment the right-hand column starts appearing in your backlog, you are maintaining an identity provider as a side project. Hand the issuing role to a real one and keep createJwtVerifier() on the receiving end. See Auth architecture.
JWK / JWKS verification
ts
jwk(opts: JwkOptions): Hooks; // refuses HS* (confused-deputy), caches JWKS by TTL, honors kid, enforces // issuer/audience + clock skew, then stamps ctx.state.user = { sub, scopes, claims }.type JwkAlgorithm = Exclude<JwtAlgorithm, "HS256" | "HS384" | "HS512">;type JwkSource = JwkSet | string | (() => JwkSet | Promise<JwkSet>); // object | https URL | resolverinterface JwkSet { keys: JsonWebKey[] }type JwkVerifyHook = (payload: Record<string, unknown>, ctx) => boolean | void | Promise<boolean | void>; // return false to reject (403)interface JwkOptions { jwks: JwkSource; // object, https:// URL, or resolver (http:// refused) algorithms: JwkAlgorithm[]; // required, non-empty; HS* refused at construction issuer?: string | string[]; audience?: string | string[]; // required in production from 1.5.4 allowAnyAudience?: boolean; // 1.5.4: opt out of the production audience requirement maxLifetimeSeconds?: number; // positive integer; requires exp; default: no cap clockSkewSeconds?: number; // default: 0 realm?: string; // WWW-Authenticate realm; default: "api" fetchTtlSeconds?: number; // default: 300; URL sources only maxStaleSeconds?: number; // default: 3600; 0 disables; URL sources only fetch?: typeof fetch; // pair with fetchGuard() verify?: JwkVerifyHook;}
When configured, maxLifetimeSeconds enforces exp - (iat ?? now) <= maxLifetimeSeconds. Missing expiry or an excessive lifetime returns 401 invalid_token; invalid configuration throws at construction. Omission preserves uncapped verification and permits tokens without exp. See the authentication safeguards for a short-lived service-token configuration and revocation boundaries.
session(opts: SessionOptions): Hooks;rotateSession(opts?: RotateSessionOptions): Hooks; // refresh ID on login/privilege changesignValue (value: string, secret: string | Uint8Array): Promise<string>;verifySignedValue(value: string, secret: string | Uint8Array): Promise<string | null>;class MemorySessionStore implements SessionStore {}interface SessionStore { get (sid: string): SessionRecord | null | Promise<SessionRecord | null>; set (sid: string, record: SessionRecord): void | Promise<void>; destroy(sid: string): void | Promise<void>; touch? (sid: string, expiresAt: number): void | Promise<void>; // atomically extends an existing record only; must never create one update?(sid: string, record: SessionRecord): boolean | Promise<boolean>; // atomic write-if-exists; false when missing (since 1.3.7). Recommended; // needed for full logout revocation across instances sharing the store}
@daloyjs/core/hashing
ts
passwordHash(password: string): Promise<string>; // scrypt with random salt + per-hash params; returns a self-describing PHC string. // Throws TypeError on empty input or passwords over 4096 UTF-8 bytes // (the cap blocks scrypt CPU-amplification abuse).passwordVerify(password: string, hash: string): Promise<boolean>; // timing-safe comparison; refuses to verify when scrypt parameters are below // the secure floor (forces a rehash via your application logic). // Returns false (never throws) for empty or over-4096-byte passwords.