Skip to content

All versions since [0.8.0]

[0.8.0]

Added

  • validate() now validates option keys at factory time. (#320) The options parameter (second argument) is validated against the recognized keys (formats, allErrors, coerceTypes, ajv) using the shared lib/validate-options.js utility. Typos like {format: ['email']} now emit an ERGO_VALIDATE_UNKNOWN_OPTION warning with a “did you mean?” suggestion, matching the behavior of all other middleware factories. The ValidateOptions TypeScript interface is tightened to list all four accepted fields explicitly (index signature removed).

Changed

  • OTEL span attributes use stable HTTP semantic conventions. (#324) Replaced deprecated pre-v1.20 attribute names with the stable conventions declared in November 2023: http.methodhttp.request.method, http.urlurl.path + url.query, http.status_codehttp.response.status_code. The url.query attribute is set conditionally (only when the request URL contains a ?). Named constants are defined in a new lib/otel-attributes.js module to avoid a runtime dependency on @opentelemetry/semantic-conventions.

  • Wire-format primitives delegated to @centralping/ergo-wire. (#369) Link header formatting, pagination parse/serialize, idempotency key parse/format, and quoted-string sanitization now re-export from @centralping/ergo-wire for symmetric client/server alignment. Server-only code (IdempotencyStore, generateFingerprint, offsetResponse, cursorResponse) remains in ergo. Existing @centralping/ergo/lib/* import paths are unchanged.

  • csrf() validates secret at construction time. (#313) The required secret parameter is now validated at factory time (typeof + length check). Missing or invalid secret throws TypeError immediately instead of deferring the error to the first request. This is the first http/ middleware factory with factory-time required-parameter validation.

Fixed

  • csrf() encoding option now forwarded to verify(). (#308) The encoding factory option was forwarded to issue() but omitted from verify(), causing unconditional CSRF verification failure when a non-default encoding (e.g. 'hex') was configured. verify() now receives the same encoding used at issuance time.

  • Body middleware now eagerly parses all content types within its error boundary. (#323) Compressed JSON, form-urlencoded, and multipart bodies previously used a self-replacing lazy getter on result.parsed that deferred parse execution outside the body middleware’s try/catch scope. When a compressed JSON body was malformed, the parse error propagated to handler.js’s catch-all, producing a 500 Internal Server Error for what is semantically a 400 Bad Request. All paths now parse eagerly within the middleware — malformed compressed JSON correctly returns 400.

  • Authorization middleware now uses explicit {value: info} return wrapping. (#288) Previously, http/authorization.js returned the opaque info object directly on authorization success. If the user’s authorizer returned an object containing value or response keys, extractReturn() in compose-with.js would misinterpret the return as a compose protocol-form object — extracting info.value as the domain result or merging info.response into the response accumulator. The fix wraps the return as {value: info}, consistent with other domain-producing middleware that handle user-controlled data (paginate.js, tracing.js, idempotency.js). No change to composed pipeline behavior — acc.auth still receives the full info object.

  • createDispatcher() prototype poisoning vulnerability. (#254) The scheme-to-handler map in lib/authorization.js used a plain {} reduce accumulator, inheriting Object.prototype. Crafted Authorization headers with scheme names matching prototype properties (e.g., Constructor, __proto__) would bypass the strategy-not-found guard and crash with TypeError — a denial-of-service vector. Replaced with Object.create(null) to align with the project-wide null-prototype policy enforced in all other user-input-keyed parsers.

  • Response compression now recognizes RFC 6838 structured syntax suffixes (+json, +xml). (#307) application/problem+json (ergo’s error format), application/vnd.api+json (JSON:API), and other structured suffix types are now correctly identified as compressible. Previously, only exact application/json and application/xml subtypes triggered compression. The \b word boundary also prevents false matches on types like application/jsonp.

  • Logger double-logging when response stream emits error followed by close. (#312) The error event handler now calls cleanup() before logging, deregistering sibling listeners (finish, close) to prevent the subsequent close event from triggering a spurious “aborted” log entry. All three terminal event handlers (finish, abort via close, error) now follow the same pattern: deregister first, then log. Ensures exactly one structured log entry per request lifecycle outcome.

  • Corrected Vary token typo in CORS preflight responses: Access-Control-Request-Methods (plural) → Access-Control-Request-Method (singular). The previous value referenced a non-existent HTTP header, preventing correct cache-key variance for preflight method negotiation. (#259)

Removed

  • Removed utils/observables module and ./utils/observables export. (#333, #334, #336) The push-based generator coroutine module had zero internal or external consumers. The multipart body parser uses the pull-based utils/iterables/buffer-split instead. Resolves three design findings: dead infrastructure (#333), chain/buffer-split protocol incompatibility (#334), and incorrect “Observable” terminology (#336).

[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).

[Unreleased] Latest

Fixed

  • logger() lowercases header name options at construction. (#316) headerRequestIdName / headerRequestIpName are normalized with .toLowerCase() so req.headers[...] lookups match Node’s lowercased incoming header keys (RFC 9110 §5.1). Mixed-case values such as 'X-Request-Id' no longer silently miss the proxy header and fall through to UUID / missing IP. Non-string values throw TypeError at construction (logger(): "…" option must be a string), matching the rate-limit / timeout / handler factory-time value-validation pattern.

  • http/timeout validates option values at construction and names default constants. (#303, #306) Named defaults DEFAULT_TIMEOUT_MS / DEFAULT_STATUS_CODE (via STATUS_CODE_REQUEST_TIMEOUT / STATUS_CODE_GATEWAY_TIMEOUT) replace magic-number destructuring. Factory throws TypeError when ms is not a positive finite number, when ms exceeds MAX_TIMEOUT_MS (Node setTimeout signed 32-bit max — larger values clamp to 1 ms), or when statusCode is not 408 or 504 (keys-only validateOptions was previously the only check). Matches the rate-limit factory-time value-validation pattern.

  • handler() throws TypeError at construction when pipeline or onResponse is not a function. (#309) Factory previously accepted invalid types and deferred failure to the first request (pipeline → perpetual 500; truthy non-function onResponse → silently swallowed). Matches the rate-limit / idempotency construction-time value-validation pattern.