Skip to content

Search docs

Jump between documentation pages.

Browse docs

API reference: Security & auth

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.

Three families of helpers
security surface@daloyjs/core security helpershardening, outbound guards, credentials
inboundHardening primitivessafeJsonParse, header guards, HMAC
outboundEgress guardsfetchGuard(), safeRedirect()
identityCredentialsJWT / JWK, session(), passwordHash
Inbound primitives sanitize what arrives, egress guards constrain where your app can send users and requests, and the credential helpers verify who is calling.

Security primitives

ts
// Body & parser hardening
readBodyLimited(req: Request, limit: number): Promise<Uint8Array>;
safeJsonParse(text: string | Uint8Array): unknown;          // refuses __proto__, constructor, prototype keys
isForbiddenObjectKey(key: string): boolean;
hasMongoOperatorKeys(value: unknown): boolean;
assertNoMongoOperators(value: unknown, where?: string): void; // refuses $-prefixed keys on user input

// Headers
sanitizeHeaderName(name: string): string;
sanitizeHeaderValue(value: string): string;
assertNoDuplicateSingletonHeaders(headers: Headers): void;
assertNoReservedInternalHeaders(headers: Headers): void;
const RESERVED_INBOUND_HEADER_PREFIXES: readonly string[];
const SMUGGLING_SINGLETON_HEADERS: readonly string[];

// Comparisons & tokens
timingSafeEqual(a: string | Uint8Array, b: string | Uint8Array): boolean;
randomId(): string;

// Secrets
assertStrongSecret(value: string | Uint8Array, where: string): void;
const MIN_PROD_SECRET_BYTES = 32;
const WEAK_SECRET_STRINGS: ReadonlyArray<string>;

// Webhook HMAC
type WebhookHmacAlgorithm = "sha256" | "sha384" | "sha512";
const WEBHOOK_DEFAULT_TOLERANCE_SECONDS = 300;
signWebhookPayload(opts: { secret; payload; algorithm?; timestamp?; }): Promise<string>;
verifyWebhookSignature(opts: {
  secret; payload; signature; algorithm?;
  timestamp?: string | number;
  toleranceSeconds?: number;
  now?: () => number;
}): Promise<boolean>;

// Filesystem
sanitizeFilename(name: string): string;
assertSafeRelativePath(p: string, where?: string): void;    // refuses .. escape, absolute, NUL

SSRF guard

ts
fetchGuard(opts?: FetchGuardOptions): typeof fetch;
  // returns a fetch-compatible wrapper that refuses loopback / RFC1918 /
  // link-local / cloud-metadata addresses unless explicitly allowed.

interface FetchGuardOptions {
  fetch?: typeof fetch;
  allowLoopback?: boolean;
  allowPrivate?: boolean;
  allowLinkLocal?: boolean;
  allowUniqueLocal?: boolean;
  allowAddresses?: readonly string[];   // CIDR or single IP
  denyAddresses?:  readonly string[];   // wins over allow + class flags
  allowHosts?:     readonly string[];
  allowProtocols?: readonly string[];   // default: ["http:", "https:"]
  maxRedirects?:   number;              // default: 5; each hop re-validated
  resolve?: (host: string) => Promise<string[]>;
}

type SsrfBlockReason =
  | "protocol-not-allowed" | "host-not-allowed" | "dns-resolution-failed"
  | "address-not-allowed"  | "too-many-redirects" | "invalid-url";

class SsrfBlockedError extends Error { readonly url; readonly reason: SsrfBlockReason; readonly address?: string }

Open-redirect guard

ts
safeRedirect(target: string, opts: SafeRedirectOptions): Response;

interface SafeRedirectOptions {
  allowedPaths?: readonly string[];     // exact-match same-origin paths
  allowedOrigins?: readonly string[];   // strict origin equality
  fallback?: string;                    // returned instead of throwing on rejection
  status?: 301 | 302 | 303 | 307 | 308; // default: 303
  headers?: HeadersInit;
}

type SafeRedirectBlockReason =
  | "empty-target" | "invalid-control-characters" | "protocol-relative"
  | "backslash-path" | "path-not-allowed" | "origin-not-allowed"
  | "scheme-not-allowed" | "parse-failed";

class OpenRedirectBlockedError extends Error { readonly reason; readonly target }

Cookies

ts
type CookieSameSite = "Strict" | "Lax" | "None";
interface CookieAttributes {
  sameSite?: CookieSameSite;   // default: "Strict"
  secure?: boolean;            // default: true (required for __Secure-/__Host-)
  httpOnly?: boolean;          // default: true (set false for client-readable tokens)
  path?: string;               // default: "/" (must be "/" for __Host-)
  domain?: string;             // forbidden with __Host-
  maxAgeSeconds?: number;      // Max-Age= seconds; 0 omits it on writes
  partitioned?: boolean;       // Partitioned (CHIPS); default: false
}

serializeCookie(name: string, value: string, attrs?: CookieAttributes): string;  // URI-encodes value
serializeClearCookie(name: string, attrs?: CookieAttributes): string;            // Max-Age=0
readRequestCookie(header: string | null | undefined, name: string): string | null;
  // null if absent OR the name appears more than once (cookie-tossing defense)
assertCookieAttributes(opts: {
  scope: string; name: string; attributes: CookieAttributes; isProduction?: boolean;
}): void;

JWT signer & verifier

ts
type JwtAlgorithm =
  | "HS256" | "HS384" | "HS512"
  | "RS256" | "RS384" | "RS512"
  | "PS256" | "PS384" | "PS512"
  | "ES256" | "ES384" | "ES512"
  | "EdDSA";                            // "none" deliberately absent

type JwtKeyMaterial = CryptoKey | Uint8Array | JsonWebKey;

createJwtSigner(opts: JwtSignerOptions): {
  sign(payload: Record<string, unknown>, opts?): Promise<string>;
};

createJwtVerifier(opts: JwtVerifierOptions): {
  verify(token: string, opts?): Promise<JwtVerified>;
};

interface JwtVerified { readonly header: Record<string, unknown>; readonly payload: Record<string, unknown> }
class JwtError extends Error { readonly code: string }

const DEFAULT_JWT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;  // 30d

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 | resolver
interface 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[];
  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;
}

Temporal claim assertions

ts
interface TemporalClaims { iat?: number; nbf?: number; exp?: number }
type TemporalClaimErrorCode =
  | "missing-exp" | "expired" | "not-before" | "issued-in-future"
  | "invalid-exp" | "invalid-nbf" | "invalid-iat"
  | "lifetime-too-long";

assertTemporalClaims(claims: TemporalClaims, opts?: AssertTemporalClaimsOptions): void;
class TemporalClaimError extends Error { readonly code: TemporalClaimErrorCode }

Security-scheme builders (OpenAPI 3.1)

ts
// Re-exported from @daloyjs/core for convenience (also live in /openapi).
httpBearerScheme(opts?:   HttpBearerSchemeOptions):   HttpBearerScheme;
httpBasicScheme(opts?:    HttpBasicSchemeOptions):    HttpBasicScheme;
apiKeyScheme(opts:        ApiKeySchemeOptions):       ApiKeyScheme;
oauth2Scheme(opts:        OAuth2SchemeOptions):       OAuth2Scheme;
openIdConnectScheme(opts: OpenIdConnectSchemeOptions): OpenIdConnectScheme;

type ApiKeyLocation = "header" | "query" | "cookie";
interface OAuth2Flows {
  authorizationCode?: OAuth2AuthorizationCodeFlow;
  clientCredentials?: OAuth2ClientCredentialsFlow;
  implicit?:          OAuth2ImplicitFlow;
  password?:          OAuth2PasswordFlow;
}

type SecurityScheme = HttpBearerScheme | HttpBasicScheme | ApiKeyScheme | OAuth2Scheme | OpenIdConnectScheme;
const REQUIRE_PAYLOAD_AUTH_EXTENSION = "x-daloy-require-payload-auth";
securitySchemeRequiresPayloadAuth(scheme: SecurityScheme): boolean;
toOpenAPISecurityScheme(scheme: SecurityScheme): unknown;

@daloyjs/core/session

ts
session(opts: SessionOptions): Hooks;
rotateSession(opts?: RotateSessionOptions): Hooks;   // refresh ID on login/privilege change
signValue        (value: string, secret: string | Uint8Array): Promise<string>;
verifySignedValue(value: string, secret: string | Uint8Array): Promise<string | null>;

class MemorySessionStore implements SessionStore {}

interface SessionStore {
  get   (id: string): Promise<SessionRecord | undefined>;
  set   (id: string, record: SessionRecord): Promise<void>;
  delete(id: string): Promise<void>;
  touch?(id: string, expiresAt: number): Promise<void>;
}

@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.

Next up: feature modules.