All versions since [Unreleased]
[Unreleased] Latest
Changed
-
Transport HSTS default uses
DEFAULT_HSTS_MAX_AGE_SECONDSfrom ergo. (#284 companion)lib/transport/security-headers.jsimports the named constant from@centralping/ergo/lib/security-headersinstead of duplicating31536000. 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 overwriteTRANSPORT_DEFAULTSand silently disable HSTS. -
Transport HSTS
trustProxyuses leftmostX-Forwarded-Protohop. (#186) Multi-value headers such ashttps,httpnow treat the client-facing hop as authoritative for HTTPS detection. -
Uses
resolveTimingConfigfrom@centralping/ergo/lib/response-time. (#304)auto-wrap.jsno longer inlines timing option resolution; it imports the shared pure resolver so theboolean | {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 requiresresetAt. Mirrors@centralping/ergostore contract (ergo#263): custom stores must return{count, resetMs, resetAt}whereresetAtis absolute milliseconds in the store’s clock domain.TransportRateLimitOptions.storetypes updated accordingly. -
OTEL span attributes use stable HTTP semantic conventions. (#175) Span finalization in
auto-wrap.jsnow setshttp.response.status_code(viaATTR_HTTP_RESPONSE_STATUS_CODEfrom@centralping/ergo/lib/otel-attributes) instead of the deprecated pre-v1.20 namehttp.status_code. Aligns router telemetry with ergo’shandler()path.
Refactored
- Extracted
rejectWithProblemhelper indispatch(). (#172) The duplicatedendWithProblem + invokeTransportHook + returnpattern 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
-
redactHeadersoption oncreateRouter()and per-route config. (#158) Controls which response header names are replaced with'[REDACTED]'inresponseInfo.headerspassed toonResponsehooks. Accepts aSet<string>. Router-level sets the default; route-level overrides. Pass an empty Set to disable redaction. Requires@centralping/ergo >=0.7.0forlib/redact-headers.jsshared primitive. -
Typed body generics on
definePost/definePut/definePatchhelpers. (#133) A second generic parameterBnarrowsacc.body.parsedfromunknownto a user-specified type:definePost<typeof config, MyBody>(config, handler). All sixdefine*helpers andInferAccumulator<C, B>/AutoPostAccumulator<C, B>accept the optionalBparameter (defaults tounknownfor backward compatibility). Requires@centralping/ergo >=0.6.1forBodyResult<T>. -
definePut,definePatch,defineDeletetyped route helpers. (#132) Method-specific aliases for IDE discoverability.definePutanddefinePatchare type-identical todefinePost(auto-include{body: BodyResult});defineDeleteis type-identical todefineGet(auto-include{url: UrlResult}). Use whichever matches the HTTP method for intuitive autocomplete — e.g.,router.put('/path', definePut({...}, handler)).
Changed
-
onResponsehook now fires for transport-level short-circuit responses. (#135) The router-levelonResponsehook 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 newresponseInfo.sourcefield distinguishes'transport'from'pipeline'responses. ThedomainAccparameter isundefinedfor transport responses (no pipeline ran). Hook errors are swallowed on transport paths, matching the existing pipeline behavior. Zero overhead whenonResponseis not configured. -
Preset types are now narrowed per-preset. (#138) Each preset in the
Presetsinterface (jsonApi,sse,webhooks,public) has a dedicated type interface (JsonApiPreset,SsePreset,WebhooksPreset,PublicPreset) that declares exactly which transport and defaults keys are present. Hovering overpresets.jsonApi.defaultsnow shows the specific keys (accepts,timeout) instead of the fullRouteConfigDefaults | 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, andpresets.publicincludetimeout: Object.freeze({})in theirdefaults, 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: usedefaults: {...preset.defaults, timeout: {ms: N}}for a custom duration, ortimeout: falseto disable per-route.presets.sseis unchanged (timeout is already explicitly disabled for long-lived connections). -
Bumped
@centralping/ergopeer dependency floor to>=0.7.0 <0.8.0(was>=0.6.1 <0.7.0). Floor bumped to 0.7.0 forDEFAULT_REDACTED_HEADERSimport fromlib/redact-headers. (#158)
Fixed
-
Programmatic
routeOptsnow 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 invalidateRouteConfig()are refactored to delegate to the sharedvalidateRouteOpts()function. Respects the router’sstrictsetting (throw vs warn for unknown keys). -
strictPatchContent-Type enforcement now runs after route matching. (#153) Previously,strictPatchenforcement ran before route matching indispatch(), 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 withstrictBody(POST/PUT). -
Route table no longer shows
bodyas enabled for non-body-method routes. (#155)route-table.jsincorrectly reported body middleware for GET/DELETE routes that explicitly configuredbody: true, butpipeline-builder.jsnever includes body parsing for non-BODY_METHODSroutes. The route table condition now matches pipeline-builder exactly. -
onResponsehook headers now redacted by default (security fix). (#158) The threebuildResponseInfo()call sites inauto-wrap.jsandrouter.jswere not forwarding theredactSetparameter added in ergo#181.responseInfo.headersinonResponsehooks now replacesauthorization,proxy-authorization,cookie, andset-cookievalues with'[REDACTED]'by default — matching ergo’s standalonehandler()behavior. This is a behavioral change: consumers that previously read raw sensitive header values fromresponseInfo.headerswill now see'[REDACTED]'. To restore the previous behavior, passredactHeaders: new Set()tocreateRouter()or the per-route config. -
Request-ID
trustProxycheck now uses strict boolean comparison. (#162) ThecreateRequestIdfactory used a loose truthiness check (if (trustProxy)) to gate proxy trust behavior, while the siblingcreateSecurityHeadersfactory 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 toif (trustProxy === true)to align with the established pattern. -
send()error boundary inauto-wrap.js. (#154)send()was called outside the try/catch block in both pipeline execution paths (catchFn success and non-catchFn). Ifsend()threw (e.g.,JSON.stringifyon a circular reference,res.setHeaderafter headers sent), the error propagated as an unhandled rejection, the OTEL span leaked, andonResponsehooks 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’shandler.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 to201 Resource createdand DELETE routes default to204 No content, matching the runtime behavior ofDEFAULT_STATUSinlib/router.js. GET, PUT, and PATCH routes continue to default to200 Successful response. Annotationresponsesstill override these defaults. -
Request-ID
generate()validation now uses VCHAR allowlist instead of CRLF/null denylist. (#160) TheHEADER_UNSAFE_REdenylist (/[\r\n\0]/) only rejected three characters, allowing other control characters (DEL, ESC), non-ASCII bytes, and whitespace through to HTTP response headers. Replaced withVCHAR_RE(/^[\x21-\x7E]+$/), which enforces RFC 9110 §5.5 visible ASCII characters. Customgenerate()functions returning characters outside the\x21-\x7Erange will now throw aTypeError. The defaultcrypto.randomUUIDgenerator is unaffected (UUID characters are within the VCHAR range). -
timingoption 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.