Skip to content

All versions since [0.2.0]

[0.2.0]

Added

  • OpenTelemetry pipeline-builder integration. tracing config key enables W3C trace context propagation and per-request ergo.pipeline spans via @centralping/ergo’s tracing middleware. Placed first in Stage 1 (before logger) for trace ID correlation in log output. auto-wrap.js ends the span after send() with http.status_code attribute and appropriate OTEL status. Supports all code paths: success, error, catchHandler, and noSend. Zero overhead when no OTEL SDK is registered (no-op spans). (#63)
  • OpenAPI 3.1 specification generation. generateOpenAPI(router, options) produces a standards-compliant OpenAPI 3.1 document from registered route metadata. Extracts validation schemas (params, query, body), authorization strategies, content types, and manual openapi annotations. Resolves config keys against router defaults using the same precedence as the pipeline builder. Available via @centralping/ergo-router/openapi sub-path export. (#54)
  • openapi annotation key for declarative route configs. Pass-through object for per-route OpenAPI metadata (summary, description, tags, operationId, deprecated, responses, externalDocs). Validated at registration time as a plain object. (#54)
  • Route metadata registry (router._routes). Stores {method, path, config} entries for all registered routes, enabling introspection without coupling to the routing engine. (#54)
  • use config key for custom per-route middleware: declarative route configs accept a use array of [fn, setPath] tuples (or bare functions) that run after Stage 3 (Validation) and before Stage 4 (Execution). Router defaults.use entries are concatenated before route-level entries; use: false disables all custom middleware. (#51)
  • Pipeline debug tracing. Pass {debug: true} in router options to enable pipeline tracing. When enabled, responseAcc._trace is initialized on each request. The compose-with serial and concurrent runners record each middleware label in steps and set breakAt to the label that triggered a pipeline break. On error responses (>= 400), _trace appears as an RFC 9457 extension member. (#59)
  • Typed Router interface: createRouter() returns a fully typed object with get, post, put, patch, delete, use, mount, handle, and listen methods instead of object. Route methods accept RouteConfig type for declarative pipeline config. (#50)
  • RouteConfig typedef: exported from lib/pipeline-builder.js with typed properties for all 18 pipeline keys, 3 route option keys, and a typed execute callback signature. (#50)
  • Typed graceful() options: exit, onStartup, onShutdown, and log parameters have specific function/object types instead of Function. (#50)
  • CI type validation: tsconfig.check-types.json validates generated .d.ts files with strict: true and skipLibCheck: false. check-types script added to package.json. (#50)
  • Config validation at registration time: declarative route config objects, createRouter() options, and options.defaults are validated for unknown keys with Levenshtein-based “did you mean?” suggestions. Unknown keys throw by default (strict: true) or warn (strict: false). (#49)
  • Missing or non-function execute in declarative route configs now throws at registration time with a descriptive error naming the route (method + path), instead of producing a 500 at request time. (#55)
  • strict option on createRouter() to control config validation strictness (default true).
  • Auto-included middleware documentation in README.md Route Config section. (#52)
  • TypeScript usage example alongside the JavaScript Quick Start in README.md. (#46)
  • CI peer-compat job that validates the peer dependency contract against published @centralping/ergo versions (minimum and newest). (#35)
  • Import surface smoke test (lib/peer-surface.spec.unit.js) that asserts every @centralping/ergo import used by ergo-router is available at module load time. (#35)
  • Contract tests for PATCH application/merge-patch+json and application/json-patch+json body parsing through declarative pipeline routes. (#36)
  • Docs dispatch step in release workflow for automatic docs site rebuild on release. (#40)

Changed

  • BREAKING: Renamed route config key auth to authorization for consistency with the authorization() middleware factory name. The accumulator path acc.auth is unchanged. (#53)
  • Bumped @centralping/ergo peer dependency range to >=0.2.0 <0.3.0 (was >=0.1.0 <0.2.0). Floor bumped to 0.2.0 for OpenTelemetry tracing imports (tracing main entry, statusFromHttp from lib/tracing). (#63)
  • Bounded @centralping/ergo peer dependency range to >=0.1.0-beta.4 <0.2.0 (was unbounded >=0.1.0-beta.3). Floor bumped from beta.3 to beta.4 for PATCH content-type body parsing support. (#35)

Fixed

  • instance field on all error paths. The RFC 9457 instance field (urn:uuid:{requestId}) is now populated from the x-request-id response header on all auto-wrap error paths — pipeline breaks, caught errors (both default and catchHandler), and endWithProblem short-circuit responses (404, 405, 415, 429, 500). Previously, instance was only populated in the default catch block. (#59)
  • Bumped @centralping/ergo peer dependency floor from >=0.1.0-beta.1 to >=0.1.0-beta.3 to match actual import surface (idempotency export requires beta.3). (#34)
  • README license link changed from relative path to absolute URL (broken on npm). (#40)
  • CI dispatch now includes client-payload identifying ergo-router for docs site deploy. (#40)

[0.3.0]

Added

  • paginate pipeline config key. Declarative route configs accept a paginate key that wires ergo’s paginate() middleware into Stage 1 (Negotiation), after URL parsing. Automatically includes URL parsing when active (regardless of HTTP method). When enabled, send() receives paginate: true for automatic Link header and X-Total-Count generation. Supports offset-based (paginate: true) and cursor-based (paginate: {strategy: 'cursor'}) pagination strategies via ergo’s pagination primitives. (#71)

  • presets.jsonApi convenience export. A deeply frozen RouterOptions object that enables transport-level request ID and security headers, and restricts content negotiation to application/json. Consumers spread it into createRouter() for one-liner JSON API configuration. Excludes deployment-specific concerns (auth, CORS origin, rate limiting). Available via import {presets} from '@centralping/ergo-router'. (#70)

  • Typed RouteConfig with generic accumulator. Hand-written TypeScript declaration overrides (types-override/) provide precise type information for declarative route configuration. Each middleware key is typed with its specific ergo option interface (e.g., AcceptsOptions, BodyOptions) instead of generic object | boolean. The execute callback’s domain accumulator accepts a generic type parameter RouteConfig<A> (defaults to Record<string, unknown>), enabling consumers to annotate their accumulator shape for type-safe property access. RouterOptions, TransportOptions, GracefulOptions, and the Router instance are also fully typed. Adopts the types-override pattern established in @centralping/ergo. (#68)

Changed

  • Config value type validation at registration time. Declarative route config values, createRouter() options, and options.defaults are now validated for correct types at registration time. Invalid values (e.g., timeout: 'five seconds', rateLimit: 42, validate: [1,2,3]) throw immediately with descriptive error messages instead of causing cryptic runtime errors at request time. Pipeline middleware keys accept boolean | object; route option keys enforce their specific types (send: object, noSend: boolean, catchHandler: function); router option keys enforce per-key types. Value type errors always throw regardless of strict setting. (#69)
  • Bumped @centralping/ergo peer dependency range to >=0.3.0 <0.4.0 (was >=0.2.0 <0.3.0). Floor bumped to 0.3.0 for paginate middleware factory import. (#71)

[0.4.0]

Changed

  • BREAKING: Pipeline builder uses config objects instead of tuples. (#83) Aligned with @centralping/ergo@0.4.0 compose-with API change. Domain-producing middleware now uses {fn, setPath} config objects. Response-only middleware (rateLimit, precondition, securityHeaders, cacheControl, validate, jsonApiQuery) are plain functions. RouteConfig.use accepts Array<function|{fn: function, setPath: string}>. Requires @centralping/ergo >= 0.4.0 < 0.5.0.

Fixed

  • Transport Referrer-Policy default aligned with shared primitive. Changed TRANSPORT_DEFAULTS.referrerPolicy from 'strict-origin-when-cross-origin' to 'no-referrer', matching @centralping/ergo’s lib/security-headers.js default, the pipeline middleware, and all website documentation. Added contract test for Referrer-Policy header. (#80)
  • Generic propagation to route methods. Router route methods (get, post, put, patch, delete) now propagate the RouteConfig<A> generic type parameter, allowing TypeScript consumers to pass typed route configs with full domainAcc inference in execute callbacks. Previously, route methods accepted only RouteConfig (defaulting to Record<string, unknown>), which caused type errors under strictFunctionTypes when passing RouteConfig<SpecificType>. The default A = Record<string, unknown> preserves backward compatibility. (#79)

[0.4.1]

Added

  • Accumulator type inference via defineGet/definePost/defineRoute helpers. (#91) New exported functions that infer the domain accumulator type from enabled middleware config keys, providing fully typed acc in execute callbacks without manual generic annotation. defineGet auto-includes {url: UrlResult} for GET/DELETE; definePost auto-includes {body: BodyResult} for POST/PUT/PATCH; defineRoute is method-agnostic. Keys set to false correctly suppress their accumulator type. paginate transitively includes URL types.

    import {defineGet} from '@centralping/ergo-router';
    router.get(
    '/users/:id',
    defineGet({authorization: true, url: true}, (req, res, acc) => {
    acc.auth; // AuthorizationResult
    acc.url.query; // Record<string, string | string[]>
    acc.route.params; // Record<string, string>
    })
    );

    Known limitation: Middleware enabled via createRouter({defaults: {...}}) is not visible to type inference. Add the key explicitly to the route config for typed access.

  • New type exports: RouteConfigBase, InferAccumulator<C>, AutoGetAccumulator<C>, AutoPostAccumulator<C> — available for advanced use cases and custom inference helpers.

  • timing option on createRouter() for X-Response-Time header. (#93) Pass timing: true for defaults or timing: {header?, precision?} for custom configuration. Measures pipeline execution time via a res.writeHead interception using the shared applyResponseTiming primitive from @centralping/ergo/lib/response-time. Zero overhead when disabled (default). Short-circuit responses (404, 405, 415, 429) are intentionally excluded — timing measures pipeline execution, not transport/routing overhead.

Changed

  • Bumped @centralping/ergo peer dependency floor to >=0.4.1 <0.5.0 (was >=0.4.0 <0.5.0). Floor bumped to 0.4.1 for applyResponseTiming import from lib/response-time. (#93)

[0.5.0]

Added

  • Three new presets: presets.sse, presets.webhooks, presets.public. (#106) The presets namespace now includes configurations for Server-Sent Events (disables compression and timeout, restricts to text/event-stream), webhook receivers (requires Idempotency-Key header, restricts to application/json), and public read-only APIs (restricts to application/json, enables rate limiting, sets Cache-Control: public, max-age=300). All presets are deeply frozen RouterOptions objects following the same spread-override semantics as presets.jsonApi. SSE routes should set noSend: true per-route (route option, not valid in defaults).
  • onResponse post-send lifecycle hook at router and route levels. (#140) Observation callback fired after send() completes. Configurable at router level (createRouter({onResponse})) for all routes, and/or per-route ({onResponse} in route config). Both levels fire independently — route-level first, then router-level. Hook errors are swallowed. Receives (req, res, responseInfo, domainAcc). Does not fire when catchHandler takes over (error path).
  • catchHandler now receives domain accumulator as 4th argument. (#105) Custom error handlers (catchHandler) are now called with (req, res, err, domainAcc) instead of (req, res, err). The domain accumulator contains route params, parsed body, auth identity, and other pipeline data available at the time the error was thrown. The accumulator may be partially populated if the error occurred mid-pipeline. Backwards compatible — existing 3-argument handlers continue to work unchanged.
  • Semantic config validation at registration time. (#104) Routes with body: false and validate: {body: schema} now throw at registration time instead of producing a guaranteed 500 error on every request. The check uses resolved values (route config > defaults) so contradictions originating from defaults are also caught. Always throws regardless of strict setting (same rationale as value type errors).
  • New type export: GracefulLog. (#102) Interface for the logger shape accepted by graceful() and guaranteed in lifecycle callbacks. Consumers can use GracefulLog to type a custom logger without extracting it from GracefulOptions.

Changed

  • Bumped @centralping/ergo peer dependency ceiling to >=0.5.0 <0.7.0 (was >=0.5.0 <0.6.0). Ceiling expanded to include @centralping/ergo@0.6.0.

Fixed

  • Declared @types/node as optional peer dependency for TypeScript consumers. (#103) TypeScript consumers compiling with skipLibCheck: false received errors from ergo-router’s .d.ts files because import('node:http') type references require @types/node. The package is now declared as an optional peer dependency (>= 22, matching engines.node), following the ecosystem standard used by Express, Fastify, and Koa. JavaScript-only consumers are unaffected.
  • openapi sub-path export now has TypeScript declarations. (#101) The ./openapi sub-path export was missing its .d.ts file because openapi.js was not included in the tsconfig.json compilation input. TypeScript consumers importing @centralping/ergo-router/openapi now get proper type information instead of any.
  • graceful() callbacks receive non-optional log parameter. (#102) The onStartup and onShutdown callback context now correctly types log as always-defined (GracefulLog), matching the runtime behavior where log defaults to console. Previously, log was typed as GracefulLog | undefined, forcing consumers to use optional chaining or non-null assertions.

Documentation

  • Route Config table now includes all valid config keys. Added noSend, send, and catchHandler (Route Option Keys) and idempotency (Pipeline Key) to the README Route Config table. Previously only Pipeline Keys and Annotation Keys were listed. (#98)
  • router.use() documented in Route Methods section. Added signature, behavior description (prepended to every declarative/array pipeline), and the array-pipelines-only caveat. (#98)
  • @example in index.js now uses router.listen() instead of raw node:http. (#115) The package entry point example previously demonstrated http.createServer(router.handle()) which is inconsistent with the README Quick Start and website guides. Updated to use the built-in router.listen() convenience method.

[0.6.0]

Added

  • router.routeTable() introspection method. (#122) Returns a formatted multi-line string summarizing registered routes, enabled middleware (in pipeline stage order), and transport configuration. Designed for startup logging via onStartup: ({log}) => log.info(router.routeTable()). Non-declarative routes (raw functions or arrays) are marked (custom). Methods are right-padded for visual alignment.

Changed

  • resolve() extracted to shared lib/resolve-config.js utility. The config resolution function (route config > defaults > omitted) was duplicated in pipeline-builder.js and openapi.js. Now shared by all three consumers: pipeline-builder, openapi, and route-table. Internal refactor — no behavior change.

  • Route-level send now merges with router-level send instead of replacing it. (#120) Previously, auto-wrap.js used nullish coalescing (routeOpts.send ?? routerOptions.send) which treated any defined route-level send object as a complete replacement for router-level send options. When addRoute() auto-injected {paginate: true} or {prefer: true} into route-level send, router-level options like errorFormatter were silently dropped. Send resolution now uses spread-merge ({...routerOptions.send, ...routeOpts.send}) — router-level is the base, route-level overrides on conflict. This aligns with the route-config-over-defaults precedence model. Routes that need to suppress a router-level send option can set it to undefined explicitly (e.g., send: {errorFormatter: undefined}).

Fixed

  • Typed generateOpenAPI signature and OpenAPIDocument return type. (#124) The generateOpenAPI function’s router parameter and return type were both object in the generated .d.ts, forcing consumers to cast. The router parameter is now typed as Router and the return type is OpenAPIDocument, a new interface modeling the OpenAPI 3.1 document structure at the depth generateOpenAPI produces. Supporting interfaces (OpenAPIInfo, OpenAPIOperation, OpenAPIPathItem, OpenAPIParameter, OpenAPIRequestBody, OpenAPISecurityScheme, OpenAPIComponents, GenerateOpenAPIOptions) are exported from the main entry point and the ./openapi sub-path. All interfaces use template-literal index signatures ([key: \x-${string}`]: unknown`) restricting extension members to the `x-*` prefix required by OpenAPI 3.1 §4.1.

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