Skip to content

All versions since [Unreleased]

[Unreleased] Latest

Changed

  • Transport HSTS default uses DEFAULT_HSTS_MAX_AGE_SECONDS from ergo. (#284 companion) lib/transport/security-headers.js imports the named constant from @centralping/ergo/lib/security-headers instead of duplicating 31536000. Requires @centralping/ergo >=0.9.0 (named export shipped in ergo 0.9.0).

  • Transport security-headers nullish config merge. (#185) Optional fields passed as undefined (e.g. {hsts: undefined} from spreads) no longer overwrite TRANSPORT_DEFAULTS and silently disable HSTS.

  • Transport HSTS trustProxy uses leftmost X-Forwarded-Proto hop. (#186) Multi-value headers such as https,http now treat the client-facing hop as authoritative for HTTPS detection.

  • Uses resolveTimingConfig from @centralping/ergo/lib/response-time. (#304) auto-wrap.js no longer inlines timing option resolution; it imports the shared pure resolver so the boolean | {header?, precision?} contract has one implementation. Peer floor raised to @centralping/ergo >=0.9.0 <1.0.0.

  • BREAKING: Transport rate-limit store hit() return type requires resetAt. Mirrors @centralping/ergo store contract (ergo#263): custom stores must return {count, resetMs, resetAt} where resetAt is absolute milliseconds in the store’s clock domain. TransportRateLimitOptions.store types updated accordingly.

  • OTEL span attributes use stable HTTP semantic conventions. (#175) Span finalization in auto-wrap.js now sets http.response.status_code (via ATTR_HTTP_RESPONSE_STATUS_CODE from @centralping/ergo/lib/otel-attributes) instead of the deprecated pre-v1.20 name http.status_code. Aligns router telemetry with ergo’s handler() path.

Refactored

  • Extracted rejectWithProblem helper in dispatch(). (#172) The duplicated endWithProblem + invokeTransportHook + return pattern across 4 dispatch reject sites (strictBody 415, strictPatch 415, 405, 404) is now consolidated into a single module-private helper function. No behavioral change — the same functions are called with the same arguments in the same order. Reduces maintenance liability when modifying the short-circuit response path.

Added

  • redactHeaders option on createRouter() and per-route config. (#158) Controls which response header names are replaced with '[REDACTED]' in responseInfo.headers passed to onResponse hooks. Accepts a Set<string>. Router-level sets the default; route-level overrides. Pass an empty Set to disable redaction. Requires @centralping/ergo >=0.7.0 for lib/redact-headers.js shared primitive.

  • Typed body generics on definePost/definePut/definePatch helpers. (#133) A second generic parameter B narrows acc.body.parsed from unknown to a user-specified type: definePost<typeof config, MyBody>(config, handler). All six define* helpers and InferAccumulator<C, B> / AutoPostAccumulator<C, B> accept the optional B parameter (defaults to unknown for backward compatibility). Requires @centralping/ergo >=0.6.1 for BodyResult<T>.

  • definePut, definePatch, defineDelete typed route helpers. (#132) Method-specific aliases for IDE discoverability. definePut and definePatch are type-identical to definePost (auto-include {body: BodyResult}); defineDelete is type-identical to defineGet (auto-include {url: UrlResult}). Use whichever matches the HTTP method for intuitive autocomplete — e.g., router.put('/path', definePut({...}, handler)).

Changed

  • onResponse hook now fires for transport-level short-circuit responses. (#135) The router-level onResponse hook now fires for 404 (unknown path), 405 (method not allowed), 415 (unsupported content-type), 429 (rate limited), OPTIONS 204, and CORS preflight 204 responses — previously only pipeline-routed responses triggered the hook. A new responseInfo.source field distinguishes 'transport' from 'pipeline' responses. The domainAcc parameter is undefined for transport responses (no pipeline ran). Hook errors are swallowed on transport paths, matching the existing pipeline behavior. Zero overhead when onResponse is not configured.

  • Preset types are now narrowed per-preset. (#138) Each preset in the Presets interface (jsonApi, sse, webhooks, public) has a dedicated type interface (JsonApiPreset, SsePreset, WebhooksPreset, PublicPreset) that declares exactly which transport and defaults keys are present. Hovering over presets.jsonApi.defaults now shows the specific keys (accepts, timeout) instead of the full RouteConfigDefaults | undefined. Accessing a key not present in the preset (e.g., presets.jsonApi.defaults.cors) is now a type error, accurately reflecting the runtime shape. To opt back into the wide type, annotate explicitly: const opts: RouterOptions = presets.jsonApi. Per-preset types are exported for use as standalone type annotations.

  • Presets now include a default request timeout. (#131, #137) presets.jsonApi, presets.webhooks, and presets.public include timeout: Object.freeze({}) in their defaults, applying ergo’s built-in 30-second request timeout. Previously, routes using these presets had no timeout unless one was manually added. Handlers that take longer than 30 seconds will now receive a 408 Request Timeout response. To adjust: use defaults: {...preset.defaults, timeout: {ms: N}} for a custom duration, or timeout: false to disable per-route. presets.sse is unchanged (timeout is already explicitly disabled for long-lived connections).

  • Bumped @centralping/ergo peer dependency floor to >=0.7.0 <0.8.0 (was >=0.6.1 <0.7.0). Floor bumped to 0.7.0 for DEFAULT_REDACTED_HEADERS import from lib/redact-headers. (#158)

Fixed

  • Programmatic routeOpts now validated at registration time. (#171) The third argument to route registration methods (router.get(path, pipeline, routeOpts)) was never validated — wrong-type values like {noSend: 'yes'} or {catchHandler: {}} silently passed registration and produced unexpected runtime behavior. Programmatic route options now receive the same key validation (with Levenshtein “did you mean?” suggestions) and type checks (send: object, noSend: boolean, catchHandler: function, onResponse: function, redactHeaders: Set) as declarative route config objects. The inline type checks in validateRouteConfig() are refactored to delegate to the shared validateRouteOpts() function. Respects the router’s strict setting (throw vs warn for unknown keys).

  • strictPatch Content-Type enforcement now runs after route matching. (#153) Previously, strictPatch enforcement ran before route matching in dispatch(), returning 415 for PATCH requests to nonexistent paths (instead of 404) and method-mismatched paths (instead of 405). Now runs inside the matched-route block, consistent with strictBody (POST/PUT).

  • Route table no longer shows body as enabled for non-body-method routes. (#155) route-table.js incorrectly reported body middleware for GET/DELETE routes that explicitly configured body: true, but pipeline-builder.js never includes body parsing for non-BODY_METHODS routes. The route table condition now matches pipeline-builder exactly.

  • onResponse hook headers now redacted by default (security fix). (#158) The three buildResponseInfo() call sites in auto-wrap.js and router.js were not forwarding the redactSet parameter added in ergo#181. responseInfo.headers in onResponse hooks now replaces authorization, proxy-authorization, cookie, and set-cookie values with '[REDACTED]' by default — matching ergo’s standalone handler() behavior. This is a behavioral change: consumers that previously read raw sensitive header values from responseInfo.headers will now see '[REDACTED]'. To restore the previous behavior, pass redactHeaders: new Set() to createRouter() or the per-route config.

  • Request-ID trustProxy check now uses strict boolean comparison. (#162) The createRequestId factory used a loose truthiness check (if (trustProxy)) to gate proxy trust behavior, while the sibling createSecurityHeaders factory used a strict boolean check (config.trustProxy === true). A non-boolean truthy value (e.g., 'false' from an env var) would silently enable proxy trust in request-id but not in security-headers. Changed to if (trustProxy === true) to align with the established pattern.

  • send() error boundary in auto-wrap.js. (#154) send() was called outside the try/catch block in both pipeline execution paths (catchFn success and non-catchFn). If send() threw (e.g., JSON.stringify on a circular reference, res.setHeader after headers sent), the error propagated as an unhandled rejection, the OTEL span leaked, and onResponse hooks never fired. Both call sites are now wrapped in try/catch that emits to error listeners, records the exception on the OTEL span, and ends the response with 500 if not already ended. Matches the established pattern in ergo’s handler.js.

  • generateOpenAPI() now produces method-aware default response status codes. (#152) buildOperation() unconditionally defaulted to {200: {description: 'Successful response'}} for all HTTP methods. POST routes now default to 201 Resource created and DELETE routes default to 204 No content, matching the runtime behavior of DEFAULT_STATUS in lib/router.js. GET, PUT, and PATCH routes continue to default to 200 Successful response. Annotation responses still override these defaults.

  • Request-ID generate() validation now uses VCHAR allowlist instead of CRLF/null denylist. (#160) The HEADER_UNSAFE_RE denylist (/[\r\n\0]/) only rejected three characters, allowing other control characters (DEL, ESC), non-ASCII bytes, and whitespace through to HTTP response headers. Replaced with VCHAR_RE (/^[\x21-\x7E]+$/), which enforces RFC 9110 §5.5 visible ASCII characters. Custom generate() functions returning characters outside the \x21-\x7E range will now throw a TypeError. The default crypto.randomUUID generator is unaffected (UUID characters are within the VCHAR range).

  • timing option table rendering on npm. (#134) The pipe character in the union type description (boolean \| {header?, precision?}) was interpreted as a table cell separator by npm’s markdown renderer, splitting the description across columns. Replaced with prose “or” to match how other table rows express types.