All versions since [0.1.0-beta.1]
[0.1.0-beta.1]
Changed
- BREAKING: Renamed package from
ergoto@centralping/ergo. - Updated
content-typedependency to v2.0.0 (string-only parse API). - Added TypeScript declaration files (
.d.ts) generated from JSDoc. - Added
CHANGELOG.mdto published npm tarball. - Updated release workflow to support pre-release dist-tags.
[0.1.0-beta.4]
Added
- JSON Schema
formatkeyword support viaajv-formats. Standard formats (email,uri,date-time,uuid, etc.) are validated by default. Opt out withformats: falseor select specific formats with an array (e.g.formats: ['email', 'uri']). (#58)
Fixed
- BREAKING:
accepts()now defaultsthrowIfFailtotrue, enforcing 406 responses for unsatisfied content negotiation per the Fast Fail pipeline contract. SetthrowIfFail: falseto restore the previous informational-only behavior. (#55) body()default types now includeapplication/merge-patch+json(RFC 7386) andapplication/json-patch+json(RFC 6902), resolving 415 rejections for valid PATCH content types that ergo-router’sstrictPatchallows. (#56)validate()params validation now checksacc.route.params(ergo-router convention) with fallback toacc.params(standalone), fixing silent no-op validation when used with ergo-router. (#57)
[0.2.0]
Added
- OpenTelemetry tracing integration.
@opentelemetry/apias a regular dependency with pipeline-level distributed tracing. Newtracing()middleware factory starts anergo.pipelinespan per request, ends it aftersend(), and propagates W3C trace context. Logger automatically includestraceIdandspanIdwhen the tracing middleware is active. Per-stage child spans available viaperStage: trueoption. Zero overhead when the tracing middleware is not included in the pipeline. (#89) - Pipeline debug tracing. Pass
{debug: true}as the second argument tohandler()to enable pipeline tracing. When enabled,responseAcc._traceis initialized with{steps: [], breakAt: undefined}. Thecompose-withserial and concurrent runners record each middleware label instepsand setbreakAtto the label that triggered a pipeline break. On error responses (>= 400),_traceappears as an RFC 9457 extension member. (#86) - compose() accumulator type inference.
compose()now infers the domain accumulator type from[fn, setPath]tuples. For pipelines with 1–12 tuples, TypeScript infers which keys exist on the accumulator and their types. Accessing a key from middleware not in the pipeline is a compile-time error. Pipelines with >12 tuples or only plain functions fall back toPromise<object>.compose.all()has the same overloaded inference. (#87) - Middleware result type exports. New consumer-facing type interfaces for middleware
accumulator values:
UrlResult,BodyResult,CookieJar,LogEntry,AcceptsResult,PreferResult,RateLimitResult,ResponseAccumulator, andMiddlewareTuple. Import from@centralping/ergo/types. (#87) - Hand-written type override system.
types-override/directory holds hand-written.d.tsfiles that replace auto-generated declarations aftertscruns. Used forcompose-with.d.tswhere JSDoc cannot express the required variadic generic types. (#87) - Pagination helpers. New
paginatenamespace export withparseOffsetParams,parseCursorParams,offsetResponse, andcursorResponse. Parses query parameters with bounded defaults, generates RFC 8288 Link headers andX-Total-Count, and returns pipeline-compatible response objects. Available viaimport {paginate} from '@centralping/ergo'orimport {parseOffsetParams} from '@centralping/ergo/lib/paginate'. (#85) - CI type-checking gate validates generated
.d.tsfiles withskipLibCheck: falseandstrict: truevianpm run check-types. Prevents shipping broken type declarations. (#83) - TypeScript usage example alongside the JavaScript Quick Start in
README.md. (#74)
Fixed
formatLinkHeader()now rejects href values containing CR, LF, or NUL characters with aTypeError, preventingERR_INVALID_CHARcrashes when malformed hrefs reachres.setHeader(). (#103)instancefield on all error paths. The RFC 9457instancefield (urn:uuid:{requestId}) is now populated from thex-request-idresponse header on all error paths — pipeline breaks (return-value), caught errors, andendWithProblem(412). Previously,instancewas only populated in thecatchblock, missing the v2 return-value error model entirely. (#95)validate()now emits a one-timeprocess.emitWarningdiagnostic with codeERGO_VALIDATE_UNKNOWN_KEYwhen the schema map contains unrecognized keys (e.g.validate({schemas: {body: ...}})instead ofvalidate({body: ...})). Previously, unrecognized keys were silently ignored and validation became a no-op. (#84)- BREAKING:
validate()now returns a 500 response when a body schema is configured butacc.bodyis absent, indicatingbody()was not placed beforevalidate()in the pipeline. A one-timeprocess.emitWarningdiagnostic is emitted with codeERGO_VALIDATE_NO_BODY. Previously, body validation was silently skipped, allowing invalid request data to reach the execute handler unchecked. Correctly ordered pipelines are unaffected. (#93) - BREAKING:
idempotency()now distinguishes missing from malformedIdempotency-Keyheaders. A malformed header (present but not a valid RFC 8941 sf-string) always returns400with format guidance, regardless of therequiredoption. Previously, a malformed header withrequired: falsesilently passed through as if the header were absent. (#90) - BREAKING (types only): All middleware factory
.d.tsdeclarations now emit inferred function signatures instead ofFunction.compose()/composeWith()return(...args) => Promise<object>instead ofFunction.csrf()andlogger()emit full object types instead ofobject. No runtime behavior changes. (#75) .d.tsdeclarations now compile withskipLibCheck: false. Middleware parameter types forcookie(),cors(),url(), andlib/corsare specific instead of{}. (#83)
[0.3.0]
Added
http/paginate.jsdeclarative pagination middleware factory. Wrapslib/paginate.jsutilities into a pipeline-compatible middleware supporting offset and cursor strategies. Placed in Stage 1 (Negotiation) afterurl. Reads parsed query fromdomainAcc.url.queryand stores structured pagination parameters atacc.paginate. (#114)send()paginateoption. Whentrue, readsdomainAcc.paginateandresponseAcc.paginateto auto-generate RFC 8288 Link headers (first,prev,next,last) andX-Total-Countfor offset pagination responses. Supports both offset and cursor strategies. (#114)'paginate'added toSEND_RESERVEDto prevent pagination metadata from leaking as RFC 9457 extension members. (#114)errorFormatteroption forsend()andhandler(). Pluggable error body formatter forstatusCode >= 400responses. When provided, receives the RFC 9457 Problem Details object (plain object) and{requestId, statusCode, method}context. The return value becomes the response body, serialized asapplication/jsoninstead ofapplication/problem+json. Applies to both the main error path and 412 conditional responses (endWithProblem). Teams with existing error contracts can adopt ergo without changing their client-facing error format. (#110)redactErrorsoption forhandler(). Controls whether caught 5xx exception messages appear in the RFC 9457 responsedetailfield. Defaults totrue(secure — generic status text only). Set tofalseduring development to surfaceerr.messagein error responses without exposing stack traces. (#109)- Typed middleware options and results. All 21 middleware factory functions now have
hand-written
.d.tsoverrides intypes-override/http/with named options interfaces (AcceptsOptions,BodyOptions,CorsOptions,ValidateOptions, etc.) and precise return types. Consumers get autocomplete and type-checking for factory parameters and pipeline accumulator values withoutas anycasts. Import options/result types from@centralping/ergo/types. (#108) - New result type interfaces:
AuthorizationResult,TracingResult,IdempotencyResult. (#108) - New utility types:
AjvFormatName(26-member string literal union tracking ajv-formats 3.x),AuthorizationStrategy,ValidateSchemas,CookieJar(now includes jar methods). (#108) types-override/http/main.d.tsoverride provides typed re-exports for all middleware,httpErrors,fromConnect, andpaginatenamespace. (#108)
Changed
- Breaking:
paginateexport from@centralping/ergochanged fromlib/paginate.jsutility namespace ({parseOffsetParams, parseCursorParams, offsetResponse, cursorResponse}) tohttp/paginate.jsfactory function. Consumers usingpaginate.parseOffsetParams()must switch to deep import:import {parseOffsetParams} from '@centralping/ergo/lib/paginate'. (#114)
Fixed
envelopecallback type signature corrected. Changed from(body: unknown, statusCode: number) => unknownto(body: unknown, ctx: {requestId: string; statusCode: number; method: string}) => unknownto match the actual implementation which passes a context object, not a bare status code. AffectsSendOptionsandHandlerOptions. (#110)PreferResultnow matches runtime. Corrected from{[k: string]: {value?: string}}to{[k: string]: string | true}— the runtime returns flat preference values, not wrapped objects. (#108)CookieJarnow includes jar methods. Addedset(),get(),clear(),toHeader(),size, andisJarto match the actual cookie jar API. Previously typed as a bare index signature. (#108)LogEntrynow includes trace correlation fields. Added optionaltraceIdandspanIdproperties populated when the tracing middleware is active. (#108)
Removed
RateLimitResultremoved. The rate-limit middleware only returns{response: {headers}}— it never stores a domain accumulator value. The interface described a shape that did not exist at runtime. (#108)
[0.4.0]
Changed
- BREAKING: Middleware composition uses config objects instead of tuples. (#117)
Domain-producing middleware now uses
{fn, setPath}config objects instead of[fn, setPath]tuples. Response-only middleware (cors, securityHeaders, cacheControl, rateLimit, precondition, validate, jsonApiQuery) are plain functions — no wrapper needed.normalizeOp()now discriminates viatypeof op === 'function'(wasArray.isArray)MiddlewareTupletype renamed toMiddlewareOpwith{fn, setPath}shape- All 20 middleware inner functions are now named (
fn.nameprovides trace labels) - Migration:
[myFn(), 'key']→{fn: myFn(), setPath: 'key'}
Fixed
- handler.js JSDoc and
HandlerOptionstype now document thepaginateoption forwarded tosend(). (#118)
[0.4.1]
Added
- Factory-time option key validation with Levenshtein suggestions. (#126)
All 18 middleware factories now validate incoming option keys at factory invocation
time via a shared
lib/validate-options.jsutility. Unknown keys emit a deduplicatedprocess.emitWarningwith{type: 'ErgoWarning'}and a “did you mean?” suggestion when the Levenshtein edit distance is within threshold.handler()validates the union of its own keys andsend()keys;send()validates independently. timingoption onhandler()forX-Response-Timeheader. (#127) Passtiming: truefor defaults ortiming: {header?, precision?}for custom configuration. Measures the full request lifecycle (pipeline + error handling + send) via ares.writeHeadinterception. Zero overhead when disabled (default). Shared primitivelib/response-time.jsavailable for deep import by@centralping/ergo-router.
Changed
- README TypeScript Quick Start updated to reflect shipped type infrastructure. (#131)
Removed the forward-looking caveat about future type inference improvements — all
prerequisite features have shipped (compose overloads #87, typed middleware #108,
ergo-router accumulator inference ergo-router#91). Simplified the TS example by removing
the
AuthResultinterface and explicitIncomingMessage/ServerResponseimports. Replaced the caveat with a factual note explaining whenaccannotations are needed (standalonecompose()with interleaved bare functions) and directing users to@centralping/ergo-router’sdefineGet/definePosthelpers for annotation-free inference.
[0.5.0]
Added
-
nowclock option onMemoryStoreconstructor. (#150) Accepts an optional clock function (() => number) for deterministic time control. Default isDate.now— no behavior change for existing consumers. Enables portable test patterns without Node-specific mock timers. -
responseSchemaoption forsend()andhandler(). (#137) Schema-based response body projection that strips undeclared properties before JSON serialization. Accepts a map of status code (or range key like'2xx','default') to JSON Schema objects. Projectors are compiled at factory time for zero per-request schema parsing overhead. Only applies to Object bodies withstatusCode < 400. Resolution order: exact match → range → default. New shared primitivelib/response-schema.jsavailable via deep import@centralping/ergo/lib/response-schema. -
Multi-runtime CI: Deno 2.x and Bun 1.x test jobs. (#138) CI now runs the full test suite on Deno and Bun alongside Node.js 22/24. Both runtimes pass all contract tests (100%) and nearly all unit tests (Deno 96.7%, Bun 99.4%) via their Node.js compatibility layers. Jobs use
continue-on-erroras informational checks. README documents runtime support status and known gaps. -
onResponsepost-send lifecycle hook forhandler(). (#140) Observation callback fired aftersend()completes. Receives(req, res, responseInfo, domainAcc)whereresponseInfois a snapshot of{statusCode, headers, method, url, bodySize, duration}. Hook errors are swallowed — cannot affect the response. Async hooks are awaited. OTEL span duration includes hook execution time. -
lib/response-info.jsshared primitive. (#140) Pure functionbuildResponseInfo(req, res, startTime)for constructing response info snapshots. Consumed byhttp/handler.jsandergo-router/lib/auto-wrap.js. Exported via@centralping/ergo/lib/response-info. -
validate()shorthand form for body-only schemas. (#135) Pass a raw JSON Schema object directly tovalidate()instead of wrapping it in{body: schema}. The shorthand is detected when the first argument contains JSON Schema keywords (e.g.type,properties,required) and none of the targeted keys (body,query,params). The targeted form is unchanged and takes precedence when any targeted key is present. Objects that match neither form still emitERGO_VALIDATE_UNKNOWN_KEYwarnings. -
Multi-runtime CI: Deno 2.x and Bun 1.x test jobs. (#138) CI now runs the full test suite on Deno and Bun alongside Node.js 22/24. Both runtimes pass all contract tests (100%) and nearly all unit tests (Deno 96.7%, Bun 99.4%) via their Node.js compatibility layers. Jobs use
continue-on-erroras informational checks. README documents runtime support status and known gaps.
Documentation
- Naming discoverability audit:
send,accepts,compose— keep decision. (#139) Evaluated all three names against ecosystem conventions, alternative candidates, and affected surface area. Conclusion: names are accurate, alternatives are equally or more ambiguous, and the perceived confusion stems from architectural differences with Express/Fastify/Koa — not naming errors.
Fixed
-
Test suite portability for Deno and Bun. (#150) Replaced Node-specific test infrastructure (
@opentelemetry/sdk-trace-node,mock.method(),mock.timers) with portable patterns in three test files:http/tracing.spec.unit.js(mock tracer via{tracer}factory option),http/validate.spec.unit.js(directprocess.emitWarningassignment),lib/rate-limit.spec.unit.js(injectable clock). No production behavior changes except thenowparameter. -
Declared
@types/nodeas optional peer dependency for TypeScript consumers. (#134) TypeScript consumers compiling withskipLibCheck: falsereceived errors from ergo’s.d.tsfiles becauseimport('node:http')type references require@types/node. The package is now declared as an optional peer dependency (>= 22, matchingengines.node), following the ecosystem standard used by Express, Fastify, and Koa. JavaScript-only consumers are unaffected — npm prints an informational warning but does not fail.
[0.6.0]
Added
-
Intrinsic
setPathon built-in middleware factory return values. (#153) Domain-producing middleware factories (url,logger,body,accepts,cookie,authorization,tracing,prefer,paginate,idempotency) now attach a non-enumerablesetPathproperty to their returned middleware function.normalizeOpincompose-with.jsreads this property, allowing bare functions to be composed without explicit{fn, setPath}wrappers:// Before: compose({fn: url(), setPath: 'url'}, {fn: body(), setPath: 'body'})// After: compose(url(), body())The explicit
{fn, setPath}form remains fully supported for custom middleware. TypeScript declarations updated with& {readonly setPath: '...'}intersection types andMiddlewareOp<K, F>union type in compose-with overloads.
Fixed
- JSDoc
@exampleblocks inhandler,tracing, andindexnow include theimport http from 'node:http'line. (#155)
[0.6.1]
Documentation
-
JSDoc
@exampleblocks updated to 0.6.0 bare compose API. (#161) Replaced explicit{fn, setPath}wrappers with bare function calls for built-in middleware in orchestration examples acrosshttp/main.js,http/index.js,http/handler.js, andhttp/validate.js. Corrected@fileoverviewcompose reference inhttp/main.js(stale tuple pattern from pre-v0.4.0). -
README Quick Start updated to 0.6.0 bare compose API. (#160) Replaced explicit
{fn, setPath}wrappers with bare function calls for built-in middleware in both JS and TS examples. Added a note explaining that{fn, setPath}config objects remain available for custom middleware. Expanded the middleware overview table with 4 missing entries:handler,tracing,paginate,idempotency.
[0.7.0]
Changed
-
IdempotencyStoreeviction lookup is now O(1) via internal side index. (#239) Added a_completeKeysSet as a secondary index tracking keys withcompletestatus. Eviction inset()now uses_completeKeys.values().next().valueinstead of scanning the entire_entriesMap. Under sustained load where all entries are in-flight (processing), worst-case eviction was previously O(maxKeys); it is now O(1) for the common case (complete entries available) with unchanged fallback behavior (oldest processing entry evicted with warning). Internal optimization only — no public API changes; custom store implementations are unaffected. -
Prefer header parser normalizes preference names and values to lowercase. (#235) Preference names are lowercased per RFC 7240 §2 (case-insensitive comparison). Preference values are also lowercased as a Postel’s Law leniency for practical interoperability — all IANA-registered preference values are lowercase tokens. A client sending
Prefer: Return=Minimalnow produces{return: 'minimal'}instead of{Return: 'Minimal'}. Non-breaking for well-behaved clients: standard RFC 7240 inputs already use lowercase. -
IdempotencyStoreeviction is now status-aware with generation token validation. (#225)set()now prunes expired entries before checking capacity, preferentially evictscompleteentries overprocessingentries, and returns a generation token (string).complete(key, response, generation)accepts the generation token and returnsboolean(trueon success,falsewhen the entry is absent, evicted, or the generation token mismatches). Passingundefinedornullasresponsereturnsfalsewithout mutating the entry. When all entries areprocessingand a new entry must be stored, the oldestprocessingentry is evicted and a one-timeprocess.emitWarningis emitted with{type: 'ErgoWarning', code: 'ERGO_IDEMPOTENCY_PROCESSING_EVICTED'}. Custom store implementations must updateset()to return astringandcomplete()to accept a thirdgenerationparameter and returnboolean. -
Prefer header parser enforces RFC 7240 token/quoted-string grammar. (#219) Replaced the loose regex with a character-by-character scanner that enforces RFC 9110 §5.6.2
tokenand §5.6.4quoted-stringgrammars. Preference names now accept the fulltcharset (digits,!,#, etc. at any position). Quoted values handle backslash escapes (quoted-pair) and reject bare control characters. Commas inside quoted values no longer break parsing. Malformed preferences are silently skipped (graceful degradation). Non-breaking: valid RFC 7240 inputs produce identical output; previously-accepted invalid inputs may now be skipped. -
formatLinkHeaderhref validation replaced with RFC 3986 URI-reference character allowlist. (#207) Previously rejected only CR, LF, NUL, and>via a four-character denylist. Now validates against the full RFC 3986 §2 URI-reference character repertoire — only unreserved, reserved, and well-formed percent-encoded triplets (%XX) are accepted. Bare%signs and malformed sequences like%GGor%2are rejected. Callers passing href values with spaces,<,{,},\,^,`, or|will now receive aTypeError. Callers producing IRIs must percent-encode non-ASCII characters before callingformatLinkHeader.
Added
-
keyGeneratoroption onidempotency()for store key scoping. (#227) Transforms the parsedIdempotency-Keyheader value into a scoped store key via(parsedKey, req, domainAcc) => string. Enables multi-tenant isolation by binding keys to auth principal, route, or HTTP method — per IETF draft-ietf-httpapi-idempotency-key-header-07 §5 composite key recommendation. Defaults to identity (unscoped), preserving existing behavior. Follows therateLimit()keyGeneratorpattern. -
redactHeadersoption onhandler()for onResponse hook header redaction. (#181) Controls which response headers are replaced with'[REDACTED]'in theresponseInfosnapshot passed toonResponsehooks. Defaults toauthorization,proxy-authorization,cookie,set-cookie— the same set used bylogger(). Pass an emptySetto disable redaction. -
lib/redact-headers.jsshared redaction primitive. (#181) ExportsDEFAULT_REDACTED_HEADERSandredactHeaders(headers, redactSet). Extracted fromhttp/logger.js’s private implementation. Available via deep import@centralping/ergo/lib/redact-headers. -
Factory-time warning for CORS wildcard + credentials misconfiguration. (#177)
cors({origins: '*', allowCredentials: true})now emits a one-timeprocess.emitWarningwith{type: 'ErgoWarning', code: 'ERGO_CORS_WILDCARD_CREDENTIALS'}. Behavior is unchanged — origin reflection still works. The warning surfaces the OWASP-documented misconfiguration footgun at startup. -
MemoryStore.reset()method for test isolation. (#165) Clears all tracked keys, restoring the store to its initial state. Enables integration tests that share a single store instance to reset rate-limit counters between test cases without reconstructing the middleware or router.
Fixed
-
MemoryStoreconstructor validatesmaxKeysandnowparameters. (#230)maxKeysmust be a positive integer;nowmust be a function. Invalid values that previously caused silent misconfiguration (e.g.maxKeys: nullcoercing to0in the eviction guard, making the store behave as a single-entry cache) now throwTypeErrorat construction time. Default construction is unaffected. -
IdempotencyStoredefensive coding improvements. (#226) Three hardening fixes for the publicIdempotencyStoreprimitive: (1) constructor validatesmaxKeys(positive integer) andttlMs(positive finite number), throwingTypeErrorfor invalid types that previously caused silent misconfiguration; (2)get()returns a frozen deep clone instead of the live internal entry, preventing callers from corrupting store state via mutation of any nested field; (3)complete()deep-clones the response object viastructuredClone, preventing post-call mutation of the original (including nested objects) from affecting stored replay data. Existing valid usage is unaffected — only previously-invalid constructor arguments now throw. -
Cookie attribute validation uses per-attribute RFC grammars. (#218)
domainvalidates against RFC 1034/1123 subdomain grammar (alphanumeric labels with hyphens, dot-separated, optional leading dot).pathvalidates against RFC 6265 §4.1.2.4path-value(any CHAR except CTLs or semicolons).sameSitevalidates against the RFC 6265bis{Strict, Lax, None}enum (case-insensitive). Previously all three attributes incorrectly used thecookie-octetgrammar intended only for cookie values. Domain now rejects injection characters (!,#,$,*) that are invalid in DNS names. Path now accepts spaces and commas that are valid per RFC 6265. SameSite now rejects arbitrary strings that do not match the enum. -
validateLocationuses RFC 3986 URI-reference character allowlist. (#217) Replaces the three-character denylist (CONTROL_STRIP_RE) that only stripped tab, CR, and LF. Now rejects all characters not permitted in a URI-reference — NUL, C0 controls, DEL, non-ASCII, bare percent signs, and malformed percent-encoding are caught. Follows the same allowlist pattern applied informatLinkHeader(#207),sanitizeQuotedString(#208), and cookie-octet validation (#206). -
parseIdempotencyKeyuses RFC 8941 sf-string allowlist instead of permissive denylist. (#220) Replaced theSF_STRING_REregex (permissive negated character class[^"\\]) with a positive allowlist derived from RFC 8941 §3.3.3’sunescaped(%x20-21 / %x23-5B / %x5D-7E) andescaped("\" ( DQUOTE / "\" )) productions. CTL characters (\x00–\x1F,\x7F), non-ASCII bytes (\x80+), and HTAB (\x09) in the inner string are now correctly rejected. The denylist escape check is eliminated — the regex itself rejects invalid escape sequences at the match stage. Previously-accepted malformed keys will now produce 400 responses (only invalid inputs are affected; all valid RFC 8941 sf-strings continue to parse correctly). -
Parsed JSON bodies use null-prototype objects at all nesting levels. (#214)
acc.body.parsednow returns objects created viaObject.create(null)at every depth, aligning with the null-prototype policy enforced by query, cookie, and Prefer parsers. Applies to both the identity-encoded fast path and the compressed-body lazy getter path. -
JSDoc bare
{Buffer}replaced with{import('node:buffer').Buffer}. (#213) Two annotations inlib/idempotency.js(1) andhttp/body.spec.unit.js(1) used bareBufferwithout theimport('node:...')form required by JSDoc conventions. -
sanitizeQuotedStringuses qdtext allowlist instead of CTL denylist. (#208) Replaced the control-character denylist regex with a positive allowlist derived from the RFC 7230 §3.2.6qdtextandquoted-pairproductions. Behavior is preserved for the previous CTL cases (NUL–BS, LF–US, DEL), and the sanitizer now also strips characters outside the quoted-string latin1 allowlist. Defense-in-depth improvement: any character not explicitly allowed is now stripped rather than relying on enumerating forbidden characters. -
Cookie value validation uses RFC 6265 cookie-octet allowlist. (#206) Replaces the denylist regex (
COOKIE_VALUE_UNSAFE_RE) with an anchored allowlist (COOKIE_VALUE_RE) matching RFC 6265 §4.1.1cookie-octetgrammar. Now correctly rejects non-ASCII bytes (\x80-\xFF) that the denylist missed. Aligns with the allowlist patterns already used byassertSafeName(TOKEN_RE) and the parser (valueRFC6265). -
Location header rejects dangerous URI schemes. (#188)
send()validatesresponseAcc.locationagainstjavascript:,data:, andvbscript:schemes before setting the Location header. ThrowsTypeErrorfor blocked schemes, providing defense-in-depth against CWE-601 XSS via open redirect. -
JSDoc bare
{Array}annotations replaced with{*[]}shorthand. (#187) Five annotations inlib/paginate.js(4) andutils/flat-array.js(1) used imprecise{Array}without type parameters. Replaced with the{*[]}shorthand for consistency with the codebase’s parameterized{Array<T>}convention. -
Logger
error()callback now redacts sensitive error details by default. (#183) WhenredactErrorsistrue(default), the error callback logs generic HTTP status text instead oferr.message, and suppresseserr.stackanderr.originalError. Prevents sensitive error details (database connection strings, file paths, token validation messages) from leaking into structured log output. Mirrorshandler()’sredactErrorsbehavior for HTTP response bodies, applied to the log output boundary. -
ajv-formatsdefault mode changed from full to fast (ReDoS mitigation). (#182) The default format validation mode now uses simplified regexes that are safe for untrusted input. Full-mode regexes fordate,time,date-time,duration,uri,uri-reference,email, andidn-emailare vulnerable to ReDoS with crafted payloads. Selective format arrays (e.g.formats: ['email']) continue to use full-mode regexes —ajv-formatsdoes not support per-format mode selection. Opt in to full mode via{mode: 'full'}when strict RFC compliance is required and input sources are trusted. -
Logger preserves empty-string request IDs (nullish coalescing). (#186)
http/logger.jsrequest-ID resolution chain now uses??instead of||. An upstream proxy sendingx-request-id: ""is treated as a present value rather than falling through to UUID generation. -
Validation error
pathuses RFC 6901 empty string for root-level errors. (#186)lib/validate.jsformatErrorno longer maps AJV’s empty-stringinstancePathto'/'. Per RFC 6901, the empty string is the correct JSON Pointer representation of the root document. Consumers matchingdetails[].path === '/'for root-level errors should update to=== ''. -
buildResponseInfonow redacts sensitive response headers. (#181) Previously,buildResponseInfopassedres.getHeaders()directly into the response info snapshot without redaction. TheonResponsehook could leakset-cookie,authorization,proxy-authorization, andcookieheaders even whenhttp/logger.jscorrectly redacted them. The function now accepts an optionalredactSetparameter, andhandler()forwards itsredactHeadersoption (defaulting to the same 4-header set as the logger). -
handler()send catch block now emits errors for observability. (#179) Thesend()catch block previously used a barecatchwithout capturing the error, silently swallowing serialization failures. The error is now emitted onresvia the guardedlistenerCount('error') > 0pattern (enablinghttp/logger.jserror callbacks) and recorded on the OTEL span viaspan.recordException()for distributed tracing visibility. Matches the pipeline catch block’s established observability convention. Additionally,responseAcc.statusCodeis now set to500in the send catch so the OTEL span finalization reads the correct status instead of the pipeline’s stale value. -
Pagination
prevlink clamped to last page whenpageexceeds total. (#180)paginationLinksnow generatesprevpointing tolastPageinstead ofpage - 1when the requested page is beyond the last page, preventing clients from navigating through a chain of non-existent pages. -
Logger
redact()uses null-prototype object. (#178) Theredact()helper inhttp/logger.jsnow usesObject.create(null)instead of{}for the header copy, aligning with the null-prototype policy for user-input-derived objects. -
BodyResult.parsedtype narrowed from optional to required. (#174) Theparsedfield was incorrectly declared asparsed?: Tdespite thebody()middleware guaranteeing its presence on every success path (both the fast JSON path and the lazy-getter path). Consumers no longer need non-null assertions or optional chaining to accessacc.body.parsed.
Documentation
- README: elevate benchmark discoverability. (#166) Adds a “Benchmarked” bullet to the “Why ergo?” section, surfacing the published benchmarks page link within the first screenful of the README.
[0.8.0]
Added
validate()now validates option keys at factory time. (#320) Theoptionsparameter (second argument) is validated against the recognized keys (formats,allErrors,coerceTypes,ajv) using the sharedlib/validate-options.jsutility. Typos like{format: ['email']}now emit anERGO_VALIDATE_UNKNOWN_OPTIONwarning with a “did you mean?” suggestion, matching the behavior of all other middleware factories. TheValidateOptionsTypeScript 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.method→http.request.method,http.url→url.path+url.query,http.status_code→http.response.status_code. Theurl.queryattribute is set conditionally (only when the request URL contains a?). Named constants are defined in a newlib/otel-attributes.jsmodule 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-wirefor symmetric client/server alignment. Server-only code (IdempotencyStore,generateFingerprint,offsetResponse,cursorResponse) remains in ergo. Existing@centralping/ergo/lib/*import paths are unchanged. -
csrf()validatessecretat construction time. (#313) The requiredsecretparameter is now validated at factory time (typeof+ length check). Missing or invalidsecretthrowsTypeErrorimmediately instead of deferring the error to the first request. This is the firsthttp/middleware factory with factory-time required-parameter validation.
Fixed
-
csrf()encodingoption now forwarded toverify(). (#308) Theencodingfactory option was forwarded toissue()but omitted fromverify(), 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.parsedthat deferred parse execution outside the body middleware’stry/catchscope. When a compressed JSON body was malformed, the parse error propagated tohandler.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.jsreturned the opaqueinfoobject directly on authorization success. If the user’s authorizer returned an object containingvalueorresponsekeys,extractReturn()incompose-with.jswould misinterpret the return as a compose protocol-form object — extractinginfo.valueas the domain result or merginginfo.responseinto 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.authstill receives the fullinfoobject. -
createDispatcher()prototype poisoning vulnerability. (#254) The scheme-to-handler map inlib/authorization.jsused a plain{}reduce accumulator, inheritingObject.prototype. CraftedAuthorizationheaders with scheme names matching prototype properties (e.g.,Constructor,__proto__) would bypass the strategy-not-found guard and crash withTypeError— a denial-of-service vector. Replaced withObject.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 exactapplication/jsonandapplication/xmlsubtypes triggered compression. The\bword boundary also prevents false matches on types likeapplication/jsonp. -
Logger double-logging when response stream emits
errorfollowed byclose. (#312) Theerrorevent handler now callscleanup()before logging, deregistering sibling listeners (finish,close) to prevent the subsequentcloseevent from triggering a spurious “aborted” log entry. All three terminal event handlers (finish,abortviaclose,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/observablesmodule and./utils/observablesexport. (#333, #334, #336) The push-based generator coroutine module had zero internal or external consumers. The multipart body parser uses the pull-basedutils/iterables/buffer-splitinstead. Resolves three design findings: dead infrastructure (#333), chain/buffer-split protocol incompatibility (#334), and incorrect “Observable” terminology (#336).
[0.9.0]
Added
-
resolveTimingConfigexport fromlib/response-time. (#304) Pure factory-time resolver for thetimingoption (boolean | {header?, precision?}). Returns{header, precision}orundefinedwhen disabled. Consumed byhttp/handler.jsand ergo-routerauto-wrap.jsso option interpretation lives with the timing primitive. -
DEFAULT_HSTS_MAX_AGE_SECONDSexport fromlib/security-headers. (#284) Named constant for the default HSTSmax-age(one year =31_536_000seconds).buildHstsDirectiveand JSDoc reference the constant; ergo-router transport should import it instead of duplicating the magic number. -
STATUS_PROCESSING/STATUS_COMPLETEexports fromlib/idempotency. (#271) Named lifecycle constants forIdempotencyStoreentry 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:
buildSecurityHeaderTuplesvalidates option values at construction. (#283, #331) Constrained headers (xFrameOptions,referrerPolicy,xContentTypeOptions,xXssProtection) reject non-enum values withTypeError. Free-form directives (CSP, Permissions-Policy, HSTS string) require a non-empty string without CTL characters. HSTS object form requires a non-negative integermaxAge(empty{}no longer silently defaults). Enable guards are unified to Pattern A (value !== false && value);String()coercion onxXssProtectionis removed. Invalid undocumented inputs ('',0,null) throw instead of emitting or omitting. -
BREAKING: Rate-limit store
hit()must return absoluteresetAt. (#263) Pluggable stores now return{count, resetMs, resetAt}whereresetAtis the absolute window-reset time in milliseconds in the store’s own clock domain.checkRateLimitusesresetAtforX-RateLimit-Resetand no longer callsDate.now(). Custom stores that previously returned only{count, resetMs}must addresetAt(typicallynow + resetMsusing the same clock that producedresetMs). Non-finiteresetAt/resetMsfail fast withTypeError. -
BREAKING: Bearer authorizer failure
infouses RFC 6750 property names. (#256)type/desc/uriare renamed toerror/error_description/error_uri. The internalerrorPropMaptranslation layer is removed — wire attribute names match the authorizer contract. Update Bearer authorizers accordingly. -
lib/authorizationmulti-strategyauthenticateis always a string. (#290) When no matching scheme is found, challenges are joined with a comma and space (RFC 7235 §4.1) instead of returningstring[]. Aligns withhttp/authorizationheader tuples ([string, string][]) andsend.jssetHeadersemantics. -
lib/authorizationscheme dispatch uses a null-prototype map instead of Proxy. (#255) Fixed{basic, bearer, $default}lookup via??— same defaulting as the former ProxyObject.hasOwntrap, without Proxy overhead. -
lib/csrfvalidatessecret/uuidwith directtypeofchecks. (#269) Replaces TypeError-as-default-parameter sentinels and theutils/type.jsimport with the sametypeof x !== 'string'→throw new TypeError(...)pattern used byhttp/csrfand otherlib/constructors. Error messages for missing parameters are unchanged. Non-string values that previously reachedcreateHmacnow 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.jsno longer provides a default export object. (#297) The manually mirroredexport default { ... }had drifted from named exports (createResponseAcc,mergeResponsewere named-only) and was already unreachable from the package entry (export *does not re-exportdefault). Named exports are unchanged. Affects only the./http/mainsubpath for any consumer ofimport x from '@centralping/ergo/http/main'. -
http/main.jsno longer declares@module @centralping/ergo. (#299) Canonical package module identity remains onhttp/index.jsonly;main.jsnow uses@module http/mainlike other internalhttp/modules.
Fixed
-
http/cache-controlvalidates structured options at construction and exportsDEFAULT_DIRECTIVES. (#296, #300, #302) Named constantDEFAULT_DIRECTIVES('private, no-cache') replaces the magic-string fallback. Factory throwsTypeErrorwhen a delta-seconds option (maxAge,sMaxAge,staleWhileRevalidate,staleIfError) is not a non-negative integer, whenpublicandprivateare both set, or whennoStoreis combined with freshness directives. Rawdirectivesstrings remain unvalidated. Matches the csrf / rate-limit factory-time value-validation pattern. -
http/rate-limitvalidates option values at construction and includesX-RateLimit-*on 429. (#264, #265, #327) Named defaultsDEFAULT_MAX_REQUESTS/DEFAULT_WINDOW_MSreplace magic-number destructuring. Factory throwsTypeErrorfor invalidmax,windowMs,store, orkeyGenerator(keys-onlyvalidateOptionswas previously the only check). Limited responses now return the same threeX-RateLimit-*header tuples as allowed responses (parity with transport rate-limit). -
http/idempotencyskip paths returnundefinedinstead of{}. (#319) Method-not-applicable and optional missing-key paths now use barereturnso compose-with leavesacc.idempotencyunset (DECISIONS:undefined/nullskip all merges). Previouslyreturn {}wrote an empty object viaextractReturn.IdempotencyResultno longer includesRecord<string, never>. -
http/idempotencyvalidatesmethodsandkeyGeneratorat construction. (#321) ThrowsTypeErrorwhenmethodsis not a non-empty Set or Array of non-empty strings (rejects thenew Set('POST')string footgun) or whenkeyGeneratoris provided but not a function. Matches the csrf /IdempotencyStorefactory-time value-validation pattern. Validatedmethodsare always copied into a newSetso caller mutation after construction cannot change middleware behavior. -
http/compressforwards theWritable.endcallback on the compression path. (#314) PreviouslycompressedEnddroppedcbwhen piping through zlib and the compressor'end'/'error'handlers calledorigEnd()with no callback. The patchedres.endnow normalizes Node overloads, captures the callback, invokes it on compressor finish, and passes the error to the callback on compressor failure. -
http/compressthreshold 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 nowBuffer.byteLength(chunk, encodingArg). -
CSRF UUID cookie locks
httpOnly: trueaftercookieOptionsspread. (#318) The token cookie already forcedhttpOnly: falseandsameSite: 'Strict'after the spread; the UUID cookie only forcedsameSite. Callers passingcookieOptions: {httpOnly: false}could make the UUID cookie JavaScript-readable. Both cookies now lock their httpOnly/sameSite attributes symmetrically. Regression tests cover UUIDhttpOnlyand both cookies’sameSitelocks. -
utils/setuses strict digit-only array-index detection. (#353) Intermediate nodes are Arrays only when the next path segment matches/^\d+$/. Previously, permissiveNumber()coercion treated'',-1,Infinity,0x1, and1e2as numeric and created Arrays with non-index properties (reachable via query paths likefields[a..b]=x). -
utils/setthrows a descriptive path-conflictTypeError. (#354) Reusing a primitive ornullintermediate now throws with message context and stablecode: 'ERGO_SET_PATH_TRAVERSE'instead of an opaque engine assignment error. NewtrySet()returnsfalsefor library-minted path conflicts only (identity-branded errors; rethrows unexpected errors including code-spoofed TypeErrors);lib/query.jsuses it for first-wins skip so inputs likea=42&a[b]=99no longer 500 throughurl()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/setrejects__proto__/prototype/constructorpath segments. (#383) Function intermediates remain valid for ordinary own properties (handler.timeout), but those three segments always throwERGO_SET_PATH_TRAVERSEso callers cannot write through.prototypeonto shared builtins. -
lib/query.jsaccumulates pairs in aMapso 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 inverted1[a]=x&1=yinto a scalar win. Pair iteration now preserves insertion order; the returned accumulator remainsObject.create(null). -
lib/query.jsfirst-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 viatrySet. Any own value already at the destination path wins (scalars, containers, and array slots), soa[]=1&a[0]=2also keeps the first slot value. -
utils/setrejects unsafe intrinsic intermediates and Arraylengthassignment. (#386–#389) Path root and reuse reject constructor.prototypeobjects, host objects reachable fromglobalThisat 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 soProxy(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, anode:vmcontext) as roots or intermediates (#395). Assigning or creatinglengthon an Array, TypedArray, DataView, Buffer, orarguments(leaf or intermediate — TypedArray/Buffer/DataView expose non-ownlength, so intermediate create must reject before shadowing) is forbidden (#393); digit indices aboveMAX_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.lengthremains allowed. -
lib/query.jsoptions use own properties only. (#392) Parser options are copied into a null-prototype viaObject.assignso pollutedObject.prototype.maxPairs/maxLength/splitcannot disable DoS caps or alter comma-split defaults. -
utils/setclarity 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/setexport — non-breaking).
[Unreleased] Latest
Fixed
-
logger()lowercases header name options at construction. (#316)headerRequestIdName/headerRequestIpNameare normalized with.toLowerCase()soreq.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 throwTypeErrorat construction (logger(): "…" option must be a string), matching the rate-limit / timeout / handler factory-time value-validation pattern. -
http/timeoutvalidates option values at construction and names default constants. (#303, #306) Named defaultsDEFAULT_TIMEOUT_MS/DEFAULT_STATUS_CODE(viaSTATUS_CODE_REQUEST_TIMEOUT/STATUS_CODE_GATEWAY_TIMEOUT) replace magic-number destructuring. Factory throwsTypeErrorwhenmsis not a positive finite number, whenmsexceedsMAX_TIMEOUT_MS(NodesetTimeoutsigned 32-bit max — larger values clamp to 1 ms), or whenstatusCodeis not408or504(keys-onlyvalidateOptionswas previously the only check). Matches the rate-limit factory-time value-validation pattern. -
handler()throws TypeError at construction whenpipelineoronResponseis not a function. (#309) Factory previously accepted invalid types and deferred failure to the first request (pipeline→ perpetual 500; truthy non-functiononResponse→ silently swallowed). Matches the rate-limit / idempotency construction-time value-validation pattern.