Skip to content

@centralping/ergo

Version [0.9.0]

Added

  • resolveTimingConfig export from lib/response-time. (#304) Pure factory-time resolver for the timing option (boolean | {header?, precision?}). Returns {header, precision} or undefined when disabled. Consumed by http/handler.js and ergo-router auto-wrap.js so option interpretation lives with the timing primitive.

  • DEFAULT_HSTS_MAX_AGE_SECONDS export from lib/security-headers. (#284) Named constant for the default HSTS max-age (one year = 31_536_000 seconds). buildHstsDirective and JSDoc reference the constant; ergo-router transport should import it instead of duplicating the magic number.

  • STATUS_PROCESSING / STATUS_COMPLETE exports from lib/idempotency. (#271) Named lifecycle constants for IdempotencyStore entry status. Middleware and custom store consumers should compare against these instead of raw string literals. Wire values remain 'processing' / 'complete' (additive, non-breaking).

Changed

  • BREAKING: buildSecurityHeaderTuples validates option values at construction. (#283, #331) Constrained headers (xFrameOptions, referrerPolicy, xContentTypeOptions, xXssProtection) reject non-enum values with TypeError. Free-form directives (CSP, Permissions-Policy, HSTS string) require a non-empty string without CTL characters. HSTS object form requires a non-negative integer maxAge (empty {} no longer silently defaults). Enable guards are unified to Pattern A (value !== false && value); String() coercion on xXssProtection is removed. Invalid undocumented inputs ('', 0, null) throw instead of emitting or omitting.

  • BREAKING: Rate-limit store hit() must return absolute resetAt. (#263) Pluggable stores now return {count, resetMs, resetAt} where resetAt is the absolute window-reset time in milliseconds in the store’s own clock domain. checkRateLimit uses resetAt for X-RateLimit-Reset and no longer calls Date.now(). Custom stores that previously returned only {count, resetMs} must add resetAt (typically now + resetMs using the same clock that produced resetMs). Non-finite resetAt/resetMs fail fast with TypeError.

  • BREAKING: Bearer authorizer failure info uses RFC 6750 property names. (#256) type / desc / uri are renamed to error / error_description / error_uri. The internal errorPropMap translation layer is removed — wire attribute names match the authorizer contract. Update Bearer authorizers accordingly.

  • lib/authorization multi-strategy authenticate is always a string. (#290) When no matching scheme is found, challenges are joined with a comma and space (RFC 7235 §4.1) instead of returning string[]. Aligns with http/authorization header tuples ([string, string][]) and send.js setHeader semantics.

  • lib/authorization scheme dispatch uses a null-prototype map instead of Proxy. (#255) Fixed {basic, bearer, $default} lookup via ?? — same defaulting as the former Proxy Object.hasOwn trap, without Proxy overhead.

  • lib/csrf validates secret/uuid with direct typeof checks. (#269) Replaces TypeError-as-default-parameter sentinels and the utils/type.js import with the same typeof x !== 'string'throw new TypeError(...) pattern used by http/csrf and other lib/ constructors. Error messages for missing parameters are unchanged. Non-string values that previously reached createHmac now fail with the same TypeError. Empty-string secrets remain allowed at the lib primitive (the HTTP factory still rejects them at construction).

Removed

  • http/main.js no longer provides a default export object. (#297) The manually mirrored export default { ... } had drifted from named exports (createResponseAcc, mergeResponse were named-only) and was already unreachable from the package entry (export * does not re-export default). Named exports are unchanged. Affects only the ./http/main subpath for any consumer of import x from '@centralping/ergo/http/main'.

  • http/main.js no longer declares @module @centralping/ergo. (#299) Canonical package module identity remains on http/index.js only; main.js now uses @module http/main like other internal http/ modules.

Fixed

  • http/cache-control validates structured options at construction and exports DEFAULT_DIRECTIVES. (#296, #300, #302) Named constant DEFAULT_DIRECTIVES ('private, no-cache') replaces the magic-string fallback. Factory throws TypeError when a delta-seconds option (maxAge, sMaxAge, staleWhileRevalidate, staleIfError) is not a non-negative integer, when public and private are both set, or when noStore is combined with freshness directives. Raw directives strings remain unvalidated. Matches the csrf / rate-limit factory-time value-validation pattern.

  • http/rate-limit validates option values at construction and includes X-RateLimit-* on 429. (#264, #265, #327) Named defaults DEFAULT_MAX_REQUESTS / DEFAULT_WINDOW_MS replace magic-number destructuring. Factory throws TypeError for invalid max, windowMs, store, or keyGenerator (keys-only validateOptions was previously the only check). Limited responses now return the same three X-RateLimit-* header tuples as allowed responses (parity with transport rate-limit).

  • http/idempotency skip paths return undefined instead of {}. (#319) Method-not-applicable and optional missing-key paths now use bare return so compose-with leaves acc.idempotency unset (DECISIONS: undefined/null skip all merges). Previously return {} wrote an empty object via extractReturn. IdempotencyResult no longer includes Record<string, never>.

  • http/idempotency validates methods and keyGenerator at construction. (#321) Throws TypeError when methods is not a non-empty Set or Array of non-empty strings (rejects the new Set('POST') string footgun) or when keyGenerator is provided but not a function. Matches the csrf / IdempotencyStore factory-time value-validation pattern. Validated methods are always copied into a new Set so caller mutation after construction cannot change middleware behavior.

  • http/compress forwards the Writable.end callback on the compression path. (#314) Previously compressedEnd dropped cb when piping through zlib and the compressor 'end'/'error' handlers called origEnd() with no callback. The patched res.end now normalizes Node overloads, captures the callback, invokes it on compressor finish, and passes the error to the callback on compressor failure.

  • http/compress threshold uses the string encoding argument for byte length. (#317) Buffer.byteLength(chunk) always measured UTF-8; res.end(str, 'latin1') (and other non-UTF-8 encodings) could compress or skip incorrectly. Size is now Buffer.byteLength(chunk, encodingArg).

  • CSRF UUID cookie locks httpOnly: true after cookieOptions spread. (#318) The token cookie already forced httpOnly: false and sameSite: 'Strict' after the spread; the UUID cookie only forced sameSite. Callers passing cookieOptions: {httpOnly: false} could make the UUID cookie JavaScript-readable. Both cookies now lock their httpOnly/sameSite attributes symmetrically. Regression tests cover UUID httpOnly and both cookies’ sameSite locks.

  • utils/set uses strict digit-only array-index detection. (#353) Intermediate nodes are Arrays only when the next path segment matches /^\d+$/. Previously, permissive Number() coercion treated '', -1, Infinity, 0x1, and 1e2 as numeric and created Arrays with non-index properties (reachable via query paths like fields[a..b]=x).

  • utils/set throws a descriptive path-conflict TypeError. (#354) Reusing a primitive or null intermediate now throws with message context and stable code: 'ERGO_SET_PATH_TRAVERSE' instead of an opaque engine assignment error. New trySet() returns false for library-minted path conflicts only (identity-branded errors; rethrows unexpected errors including code-spoofed TypeErrors); lib/query.js uses it for first-wins skip so inputs like a=42&a[b]=99 no longer 500 through url() middleware. First-wins is bidirectional over path containers (#379, #380, #381, #382): nested-then-scalar preserves earlier nests; Array↔object shape is locked (non-index under Array and index under plain object both skip) while numeric indices under Arrays (role[0]=user&role[1]=admin) and sibling object keys remain allowed; scalar-then-[] also first-wins.

  • utils/set rejects __proto__ / prototype / constructor path segments. (#383) Function intermediates remain valid for ordinary own properties (handler.timeout), but those three segments always throw ERGO_SET_PATH_TRAVERSE so callers cannot write through .prototype onto shared builtins.

  • lib/query.js accumulates pairs in a Map so first-wins follows source order. (#384) Intermediate accumulation previously used a null-prototype object + Object.entries, which reorders integer-like keys (V8 integer-index order). That inverted 1[a]=x&1=y into a scalar win. Pair iteration now preserves insertion order; the returned accumulator remains Object.create(null).

  • lib/query.js first-wins covers path aliases and occupied leaves. (#385) Distinct wire forms that normalize to the same dotted path (a.b=1&a[b]=2) no longer last-write via trySet. Any own value already at the destination path wins (scalars, containers, and array slots), so a[]=1&a[0]=2 also keeps the first slot value.

  • utils/set rejects unsafe intrinsic intermediates and Array length assignment. (#386–#389) Path root and reuse reject constructor .prototype objects, host objects reachable from globalThis at module load (depth-limited graph of own keys / prototype accessors — Intl, Proxy, crypto.subtle, process._events, Array.prototype.push, Intl.NumberFormat.prototype.format, …), and Proxies. Proxy and host checks run before Array/null-proto shortcuts so Proxy(Array.prototype) and null-proto hosts cannot write through. Ordinary functions (including per-instance bound methods created after module load), non-host null-proto objects, Arrays, and user class instances remain valid intermediates. Host matching is identity-based within the JavaScript realm where the module initializes; callers must not pass host-owned globals or intrinsics from another realm (for example, a node:vm context) as roots or intermediates (#395). Assigning or creating length on an Array, TypedArray, DataView, Buffer, or arguments (leaf or intermediate — TypedArray/Buffer/DataView expose non-own length, so intermediate create must reject before shadowing) is forbidden (#393); digit indices above MAX_ARRAY_INDEX (1024) are rejected by numeric value only when indexing an Array (leading zeros allowed when in range; plain-object / top-level digit keys unconstrained; sparse DoS bound; full numeric-bracket design remains #280). Missing intermediates are defined as own data properties so inherited setters cannot hijack traversal; existing own leaves keep ordinary assignment semantics. Path planning snapshots accessor results once so stateful getters cannot flip container type between Array-index bounds checks and writes. Plain-object .length remains allowed.

  • lib/query.js options use own properties only. (#392) Parser options are copied into a null-prototype via Object.assign so polluted Object.prototype.maxPairs / maxLength / split cannot disable DoS caps or alter comma-split defaults.

  • utils/set clarity refactor preserves return value. (#355) The dense parenthesized assignment body is split into explicit statements while still returning the assigned value (public @centralping/ergo/utils/set export — non-breaking).