All versions since [0.6.1]
[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.