Changelog
What changed in each release, package by package. These notes are generated from the packages’ own CHANGELOG.md files, so they describe what shipped, not what was planned.
WHAT’S NEW — SEPTEMBER 2026 RELEASE
A release for all 40 packages: every @zudojs/* package, zudojs-cli 2.1.0 and the zudojs installer package each have a new version. The theme is making the documented path work end to end: a new CLI project comes wired and passes its own tests, one API operation is served over four transports, RPC ships its transports, OpenAPI is generated from the routes you actually registered, and a test client drives the real app over HTTP. Much of it came from writing the Learn course against the published packages, where each lesson’s example had to run as written. Several fixes close insecure defaults or change behaviour; they are listed at the end of this section and marked in the package notes below. Highlights:
zudonpm install -g zudojs installs the CLI with two commands on your PATH, zudojs and the shorter zudo; new is an alias of create, and running it with no arguments in a terminal opens a numbered menu. A generated project’s src/server.ts now builds a router, serves /openapi.json and /docs, applies security headers, closed-by-default CORS and rate limiting, and shuts down in order, with typed env config, a composition root, an example CRUD resource and a test that drives it over HTTP. zudojs generate resource <name> writes the DTO, repository, service, controller, routes and test in one go, and zudojs add writes real, compiling integrations (database with Prisma 7, redis, websockets, email, docker and more). Generated projects depend on the @zudojs/* ranges this CLI build was tested with (zudojs-cli).
An operation defined once with defineOperation is now served over HTTP (createApiFetchHandler), RPC (registerApiRpcProcedures), queues (bindApiQueue) and the command line (runApiCli), all through the same executor, interceptors and schema validation, with one client-safe error shape, APIWireError. toOpenAPIRouteDescriptors documents the same operations in one call, and inline defineOperation({ input, handler }) now infers the handler’s input from the schema (@zudojs/api).
createRPCMemoryTransport(server) connects a client in the same process, createRPCHttpTransport({ url }) calls a remote server with fetch, and createRPCFetchHandler(server) is a web-standard handler any HTTP server can mount — it bounds request bodies and never sends stack traces. The client rebuilds typed errors from the wire, so a caller can catch an RPCValidationError or RPCForbiddenError as such (@zudojs/rpc).
Routes in @zudojs/http take an openapi option, and mountOpenAPI(router, options) serves /openapi.json and a docs page built from the routes the router actually registered — path templates, parameter patterns and optional segments included, hidden routes left out. @zudojs/openapi gains the transport-neutral createOpenAPIDocumentFromRoutes, and mountFetchHandler mounts any web-standard (Request) => Response handler on a router (@zudojs/http, @zudojs/openapi).
createHttpTestClient(target) is a supertest-style client: point it at a URL, a Node server, a fetch handler or an @zudojs/http router, then chain .get("/users").expect(200).expectJson({ ... }). The recording doubles now record every call path, so createTestEventBus().bus.publishEvent(...) is no longer silently missed, and createTestApplication() is silent with a fixed clock by default (@zudojs/testing).
Security fixes
A permission policy that allowed used to grant the permission on its own, so a “business hours” policy handed task:delete to an actor with no roles. A policy is now an extra condition on top of roles and rules unless it declares effect: "grant" (@zudojs/permissions). Guards that refused a request — authorize(), the tenancy guards — stopped the handler but the client still saw 200, because @zudojs/http ignored the plain { status, body } object they returned. The new guard-response contract (createGuardResponse in @zudojs/middleware) lets a framework-neutral guard return a real 401 or 403, and the router sends it. In @zudojs/api, interceptors now run before input validation: an anonymous caller used to get a 422 describing your schema instead of a 401, and an interceptor that replaced the input bypassed validation altogether.
A feature flag that is switched off now serves its off value (false for a boolean flag) instead of defaultValue, so a kill switch on a flag defaulting to true actually kills it (@zudojs/feature-flags). @zudojs/observability redacts password, token, authorization and the rest from log contexts and span attributes by default (@zudojs/observability). login() normalizes the identifier before looking the user up (@zudojs/auth), and cookie Domain and Path values are validated before they reach a header (@zudojs/security).
The RPC server and every API binding refuse a __proto__, constructor or prototype key anywhere in their input, using the new findUnsafeKey() from @zudojs/security. createApiFetchHandler answers 415 to a non-JSON body route even when the body is empty, so a cross-site HTML form cannot trigger an operation. mountFetchHandler’s origin option now pins the origin instead of letting the client’s Host header choose it, and a router group’s OpenAPI defaults no longer publish routes marked hidden. Input nested too deep is a 400 the client can see rather than a hidden 500 (@zudojs/validation).
Behaviour changes to act on
Each of these changes what existing code sees. Most fix a bug that made a failure look like success, but code written against the old behaviour will notice. The full entries are in the package notes below.
| Package | What changed | What to do |
|---|---|---|
permissions | A policy no longer grants without effect: "grant"; an allow only means “no objection”. | Add effect: "grant" to policies that establish a right on their own (an ownership check), or pass defaultPolicyEffect: "grant". |
feature-flags | A flag that is off serves offValue (default false for booleans), not defaultValue. Environment-provider keys are normalised (FEATURE_NEW_CHECKOUT → new-checkout). | Declare offValue if a killed flag must stay on; keyFormat: "preserve" for the old keys. |
observability | Redaction is on by default. | Pass redaction: false only if you really need raw values exported. |
queue, scheduler | Pending jobs, a started Worker and a started scheduler keep the Node.js process alive. The queue’s default serializer preserves types (a Date stays a Date), and a delayed job no longer jumps the line. | Call stop() / close() at shutdown, or pass keepAlive: false; preserveTypes: false for plain JSON payloads. |
rpc | error.code on every client error is the wire code (RPC_TIMEOUT, not ERR_RPC_TIMEOUT), and exposed errors keep their meaning (RPC_NOT_FOUND, RPC_CONFLICT …) instead of RPC_INTERNAL_ERROR. | Match on the RPC_* codes; instanceof checks are unchanged. |
storage | Lock acquire timeouts are 409 and connection/pool timeouts 503 (both were 504). create()/update() skip undefined properties instead of writing NULL. | Update status-code checks; send null explicitly to clear a column. |
http | An error thrown in the middleware pipeline propagates unwrapped, so code after await next() runs only if it catches. | Check instanceof directly instead of unwrapping .cause / .errors. |
logger | entry.message is the raw message; the formatted line is in entry.formatted. An unknown level name throws. | Custom transports print entry.formatted ?? entry.message (or use formatTransportLine). |
auth | ERR_ACCOUNT_LOCKED, ERR_ACCOUNT_DEACTIVATED and ERR_TOKEN_REVOKED replace ERR_FORBIDDEN; a revoked token is 401, not 403. | Update clients that match on codes or statuses; use normalizeLoginIdentifier() at registration. |
api | Interceptors run before validation, so context.input is the raw input. | Don’t trust context.input’s shape inside an interceptor. |
openapi | No invented 200: an operation with no documented responses gets default “Undocumented response” and a warning. | Declare the responses each route returns, then regenerate checked-in specs. |
testing | createTestApplication() logs to a silent spy and pins its clock to 2026-01-01. | Pass logger, clock or startTime if a test relied on output or wall-clock time. |
cqrs, events, messaging, transactions | Failures that used to look like success now throw: unwrapCommandResult/unwrapQueryResult on a failure, an abort during the last handler, committing a rollback-only transaction (TransactionRollbackOnlyError), rolling back a committed one. | Handle the new errors where you relied on the old silent result. |
config, container, runtime | NUMBER/BOOLEAN schemas coerce env strings and validate runs after transform; auto-registration refuses classes with required constructor parameters; start() enters every state and failed is no longer terminal. | coerce: false for strict config; register such classes with an inject list. |
schema, validation, database, cache | Absent optional keys stay absent and partial() no longer applies defaults; impossible dates are rejected; too-deep input and bad pagination cursors are 400s; an invalid cache tag is ERR_INVALID_INPUT. | Check code that expected an own undefined key or a default in an update schema. |
Upgrading: read the table above first — the permissions, feature-flag and observability rows change what your application allows, serves and exports, and the queue and scheduler rows change when a script exits. Projects created by an earlier CLI depend on ^1.0.0, so pnpm update (or npm update) picks these releases up. To get the new CLI, run npm install -g zudojs; a project it creates depends on a caret range of the exact versions it was built against (for example ^1.4.0 for @zudojs/http), so it can never resolve to a release that lacks the APIs its generated code uses. Full notes: per-package notes.
WHAT’S NEW — ZUDOJS-CLI 2.0.0 (SEPTEMBER 2026)
A release for one package. zudojs-cli was audited on its own — 55 findings, all fixed, each with a regression test that fails against the unfixed code — and ships as 2.0.0, the first major in the ecosystem. It is a major because several commands now refuse where they previously proceeded, and an interrupted run now exits 130 instead of 0. In every case the old behaviour was a bug that could make a broken run look successful: a project with no node_modules reported as created, another project’s build reported as yours, a cancelled scaffold that finished anyway. A script or CI job written against the old behaviour will notice. No other package changed in this release; the rest of the monorepo is as described in the round 11 notes below.
zudojs create used to downgrade an install failure to a warning, then print “Project created successfully” and exit 0, so a CI job went green with no node_modules. That job will now correctly go red. The project is still kept and the retry hint is still printed; only the exit code and the closing message changed. zudojs add already behaved this way — the two commands no longer disagree.
zudojs build refuses outside a Zudojs projectfindProjectRoot accepted any ancestor holding a bare package.json, so from an unrelated subdirectory the CLI climbed out and executed that project’s scripts.build — content from a file on disk — then reported success. It now requires a real Zudojs project and throws CLINotInProjectError otherwise. zudojs generate throws in the same situation instead of warning and writing files into the current directory, matching dev, build and add; it also walks up to the project root, so running it from a subdirectory no longer creates a second src/ tree.
@clack/prompts registers a SIGINT listener per spinner that only prints “Canceled”, which suppressed Node’s default termination — so an interrupted zudojs create used to run to completion and exit 0. An interrupt now rolls the scaffold back and exits 130, and a cancelled prompt exits 130 rather than 0. zudojs create my-api && cd my-api no longer runs the cd after you pressed Ctrl-C.
zudojs create invented example domains when no service list was given — four for a microservice project (identity, enrollment, assessment, notification) and three for a modular monolith, which was never even asked. An empty list now means no services: a microservice project gets its gateway, a modular monolith gets an empty module barrel, and both READMEs say how to add one. Ticking “Security” in the capabilities prompt now actually installs @zudojs/security; the prompt offered eight options and the command read six, so events and security were silently discarded — no dependency, no manifest entry, no message. A new --capabilities <list> flag makes the interactive and non-interactive branches produce the same project.
pnpm run test passes in a freshly created project: the sample spec was tests/index.ts, which matches no vitest include pattern, so the first thing you ran exited 1. It is now tests/app.test.ts. Per-command help works — zudojs create --help prints usage, arguments, options, shorts and defaults instead of rejecting --help as an invalid option. And generated projects install the resolved version range rather than latest: the frontend install path resolved every dependency to a pinned range and then passed only the names to the package manager, so two zudojs create --frontend react runs a month apart produced different majors.
The cmd.exe quoting hardening — an argument containing ", % or ! is now rejected rather than escaped, because a backslash is not a cmd escape — and NoDefaultCurrentDirectoryInExePath were tested as pure functions on Linux by passing "win32" explicitly. They have not been exercised on a real Windows host.
Upgrading: only zudojs-cli changed — every @zudojs/* package is still at its round 11 version, and an existing project needs no code change. What breaks is anything that reads the CLI’s exit code. zudojs create now exits 1 when dependency installation fails, where it exited 0 with a warning, and exits 130 when a prompt is cancelled or the run is interrupted, where it exited 0 after finishing anyway. zudojs build and zudojs generate exit 1 outside a Zudojs project instead of proceeding. A chain such as zudojs create my-api && cd my-api, and any CI step that trusted a 0, will now stop where it used to continue; that is the point. Check scripted names too: a project name must start with a letter or digit, and a schematic name may not start with a digit, so zudojs generate module 2fa is rejected rather than writing a syntax error into src/app.ts and exiting 0. Two API changes affect embedders only: RollbackManager.rollback() returns a RollbackResult instead of void, and CapabilityResolutionResult.conflicts is gone. Full notes: zudojs-cli 2.0.0.
WHAT’S NEW — SEPTEMBER 2026 RELEASE (ROUND 11)
A third full audit of all 39 packages. Every package has a new version: 28 carry source changes, the rest are republished against new dependency versions. As in round 10, every finding was reproduced before it was fixed and ships with a regression test, and the entries that close an insecure default or change existing behaviour say so in the package notes below. Highlights:
An OPTIONS request that matched a path only under other methods ran one of those handlers: OPTIONS /accounts/42 executed the DELETE /accounts/:id handler, behind any CSRF or auth middleware that treats OPTIONS as a safe method. The fallback now resolves to a synthetic route with no middleware that answers 204 with an Allow header (@zudojs/http). NodeHTTPRequest — the path behind createHTTPRequest, adaptNodeRequest and adaptNodeContext — read X-Forwarded-For and X-Forwarded-Proto from any client with no trust check at all, so a client connecting directly set its own request.ip and flipped request.secure to true; the hardened httpAdapter/node/ path already gated those headers, and this closes the parallel one that was left behind (@zudojs/http). Job metadata handed to queue.add() could carry a zudo:context record of its own, which the queue replayed around the middleware and the processor, so an enqueuer chose the tenant, correlation id and trace a job ran under; that key is now owned by the queue on every enqueue path (@zudojs/queue).
X-Forwarded-For and X-Forwarded-Proto are honoured only when the socket peer is a configured trusted proxy, and a forwarded protocol that is not http or https is discarded. The new trustProxy option — an address, a CIDR range, "loopback"/"linklocal"/"all", a hop count or a predicate — defaults to false, so a deployment behind a proxy has to opt in. Without it, request.ip is the socket peer and request.protocol reflects the socket’s own TLS state. @zudojs/security tightened the same way: extractClientIp no longer reads X-Forwarded-For when the chain is shorter than the configured trustProxy count, because such a chain did not pass through the proxies whose entries make it trustworthy.
An engine in @zudojs/permissions configured with a roleResolver or a permissionResolver no longer caches decisions by default: a resolver reads authorization state the engine does not own and cannot see change, none of it was in the cache key, and a grant withdrawn upstream kept being served until the entry expired. Supply the new resolverCacheKey to get caching back. createPermissionRegistry() gained the subscribe(listener) notifier the role and policy registries already had, and createPermissionEngine({ expandImplied }) now accepts the registry itself, so revoking an implication drops the decisions cached while it stood; a bare closure cannot announce a change, so an engine given one caches nothing rather than answering from a revoked implication.
@zudojs/config ran secret detection only on values arriving through a source, so a key such as db.password, api_key or a postgres://user:pw@host connection string that was seeded through initialValues or written with set() was printed in clear by toSafeObject(). Detection now runs inside ConfigStore.set() and covers every write path; pass sensitive: false to opt a key out. In @zudojs/http, createLoggingMiddleware({ includeHeaders: true }) redacts authorization, proxy-authorization, cookie, set-cookie and the rest of the @zudojs/logger secret-field set before the record reaches the logger.
A recurring shape this round: a capability that was typed, exported and documented, but that nothing ever invoked. runWithRequestContext and getCurrentRequestContext loaded their AsyncLocalStorage through globalThis.require, which does not exist in ESM, so request-context propagation was a silent no-op. calculateFreshness() / isFresh() never aged a cached response, so a stale one was reported fresh. A @zudojs/logger formatter that returned an object had its record computed and then discarded instead of reaching the transport. AdapterOperationOptions.retry, RouteDispatchOptions.preserveResponse and the router’s strictTrailingSlash were declared and then ignored; EventListenerLimitExceededError was never thrown, so every catch branch testing for it was unreachable; the worker:started, worker:stopped, worker:error and job:cancelled events that QueueEventMap declared were never emitted; a @zudojs/messaging handler in the advertised { handle(message, context) } object form always came back as a failed dispatch; handle.cancel() did not abort a running one-shot schedule; and a crypto provider’s declared capabilities were not consulted before an operation. Each of these now does what its type and its documentation said.
@zudojs/openapi emitted additionalProperties: false for every object schema, including one that merely strips unknown keys. That keyword means “reject the payload”, while strip accepts it and discards the extra key, so a client generated from such a document refused requests the service accepts. Only .strict() emits it now, which means checked-in specs need regenerating. addRoute also detects duplicates on the OpenAPI path template rather than the source path, so GET /users/:id and GET /users/{id} are recognised as the same route instead of both registering and one silently replacing the other at generation time.
priority in @zudojs/lifecycle is a real ordering barrier instead of a hint: components registered at one priority all complete a phase before the next priority starts, and shutdown mirrors startup within a level. shutdown() no longer disposes a component whose stop() is still running. clearRegistrations() and restoreSnapshot() in @zudojs/container invalidate live scopes, which used to go on serving a SCOPED instance built from a registration that had just been discarded. LifecycleManager in @zudojs/runtime with continueOnFailure: true no longer initializes or readies a module whose declared dependency failed, and the skip cascades to that module’s own dependents.
BaseError’s redaction walk is bounded at 32 levels, so a deeply nested cause taken from a parsed request body no longer raises a RangeError from inside the logging path. estimateSerializedSize defaults to a finite budget instead of Infinity; a $type tag arriving from the wire is length-checked before it is looked up and clipped before it is quoted into an error message; and the envelope trust boundary throws InvalidSerializedDataError rather than a bare Error, so hostile input can be told apart from an internal bug (@zudojs/errors, @zudojs/serialization, @zudojs/schema, @zudojs/types, @zudojs/validation).
Upgrading: three entries need action rather than just an update. request.ip and request.protocol in @zudojs/http no longer honour X-Forwarded-For / X-Forwarded-Proto unless the deployment sets trustProxy — behind a proxy you must opt in, or every request is attributed to the proxy’s own address. An engine in @zudojs/permissions built with a roleResolver or permissionResolver alongside a cache silently stops caching until you add resolverCacheKey, and so does one whose expandImplied is a bare closure rather than the permission registry. And @zudojs/openapi no longer emits additionalProperties: false for objects that are not .strict(), so any checked-in spec needs regenerating. Otherwise projects created by the CLI depend on ^1.0.0, so pnpm update (or npm update) picks these releases up; read the entries marked behaviour change in the package notes below before you upgrade.
WHAT’S NEW — SEPTEMBER 2026 RELEASE (ROUND 10)
A second full audit of all 39 packages. Every finding was reproduced before it was fixed and ships with a regression test. Most changes are fixes and additions, but several close insecure defaults and are marked behaviour change in the package notes below. Highlights:
The Node adapter guards every request and answers 400 to dot-segment and backslash paths; cookies default to Path=/; HttpOnly; Secure; SameSite=Lax; CORS refuses a wildcard origin with credentials (@zudojs/http). Tenancy refuses header- or path-only tenants by default (minimumTrust: "verified"). OpenAPI UI assets are pinned with Subresource Integrity and served with a CSP. RPC authorises on transport-verified auth, never on caller-set metadata.
Password hashing moves to @zudojs/crypto scrypt with p=5 (old hashes still verify and report needsRehash()); refresh tokens keep their session binding; login lockout reserves a failure before the password check. Permission deny rules whose condition throws now deny, and cache keys include the actor's roles and permissions. Rate-limit keys strip ports and bucket IPv6 by /64; the SSRF guards judge IPv4-embedded IPv6 as IPv4.
Error classes that packages defined locally (transactions, middleware, auth, OAuth, CLI, CQRS, observability, OpenAPI, HTTP guard) now live in @zudojs/errors and are re-exported, so instanceof matches either import. Default loggers in database, http, queue, plugins and events go through @zudojs/logger or process warnings instead of raw console calls, with secret redaction.
The runtime exits with code 1 after a fatal-error shutdown and rolls back modules that finish after a startup timeout; savepoint callbacks wait for the outermost commit; a Worker becomes the queue's only consumer; storage keys with dot or empty segments are refused; config reloads are atomic and redaction screens every source.
The package pages on this site were corrected wherever they contradicted the source, and each ends with an export index regenerated from the package's entry point.
Upgrading: projects created by the CLI depend on ^1.0.0, so pnpm update (or npm update) picks these releases up. Read the entries marked behaviour change in the package notes below before you upgrade.
PER-PACKAGE NOTES
| Package | Version | Notes |
|---|---|---|
@zudojs/adapters | 1.2.1 | Jump to notes |
@zudojs/api | 1.2.0 | Jump to notes |
@zudojs/auth | 1.3.0 | Jump to notes |
@zudojs/auth-oauth | 1.2.2 | Jump to notes |
@zudojs/cache | 1.2.0 | Jump to notes |
zudojs-cli | 2.1.0 | Jump to notes |
@zudojs/config | 1.3.0 | Jump to notes |
@zudojs/constants | 1.1.2 | Jump to notes |
@zudojs/container | 1.2.0 | Jump to notes |
@zudojs/core | 1.2.2 | Jump to notes |
@zudojs/cqrs | 1.2.0 | Jump to notes |
@zudojs/crypto | 1.3.1 | Jump to notes |
@zudojs/database | 1.3.0 | Jump to notes |
@zudojs/docs | 1.0.4 | Jump to notes |
@zudojs/errors | 1.3.0 | Jump to notes |
@zudojs/events | 1.3.0 | Jump to notes |
@zudojs/feature-flags | 1.4.0 | Jump to notes |
@zudojs/http | 1.4.0 | Jump to notes |
@zudojs/lifecycle | 1.2.1 | Jump to notes |
@zudojs/logger | 1.4.0 | Jump to notes |
@zudojs/messaging | 1.2.0 | Jump to notes |
@zudojs/middleware | 1.1.0 | Jump to notes |
@zudojs/observability | 1.2.0 | Jump to notes |
@zudojs/openapi | 1.5.0 | Jump to notes |
@zudojs/permissions | 1.4.0 | Jump to notes |
@zudojs/plugins | 1.3.0 | Jump to notes |
@zudojs/queue | 1.4.0 | Jump to notes |
@zudojs/rpc | 1.4.0 | Jump to notes |
@zudojs/runtime | 1.3.0 | Jump to notes |
@zudojs/scheduler | 1.2.0 | Jump to notes |
@zudojs/schema | 1.2.0 | Jump to notes |
@zudojs/security | 1.3.0 | Jump to notes |
@zudojs/serialization | 1.2.0 | Jump to notes |
@zudojs/storage | 1.2.0 | Jump to notes |
@zudojs/tenancy | 1.3.0 | Jump to notes |
@zudojs/testing | 1.2.0 | Jump to notes |
@zudojs/transactions | 1.2.0 | Jump to notes |
@zudojs/types | 1.2.0 | Jump to notes |
@zudojs/validation | 1.1.0 | Jump to notes |
zudojs | 1.0.1 | Jump to notes |
@zudojs/adapters v1.2.1
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0.
- Tooling correctness fixes for the testing, docs, adapters, feature-flag and CLI packages.
createStub()now returns the same no-op function for a given property on every access, sostub.handler === stub.handler. Abus.on("x", stub.handler)/bus.off("x", stub.handler)pair written against a stub now actually removes the listener instead of leaking it between tests.InMemoryTestStorage.set(key, value, 0)now treats a zero TTL as a deadline of "now" — the entry is already expired on the next read. Only an omitted TTL means "never expires". A test that wrote0to mean "already stale" previously got an entry that never expired.generateMarkdownnow HTML-escapes the deprecation blockquote (deprecatedMessage) and the**Owner:**line, as every other text position it writes already did. A document built from untrusted JSON can no longer put raw<script>/<img>tags into generated markdown that a renderer with HTML enabled would execute.AdapterRegistry.healthAll()no longer loses an adapter named__proto__: the per-adapter report is built on a null-prototype object, so the entry is present, the aggregate status reflects it, and nothing writes through toObject.prototype.AdapterRegistry.register()now refuses the names__proto__,constructorandprototypewith anAdapterConfigurationError.AdapterOperationOptions.retryis now implemented rather than merely declared.healthAll({ retry: { attempts, delay } })re-runs a check that reportsunhealthyup toattemptstimes in total, pausingdelayms between tries;timeoutstill bounds each try and an aborted signal stops the retries immediately. Withoutretrythe behaviour is unchanged (one try).valuesEqualnow compares structurally instead of byJSON.stringify: key order no longer matters, a key whose value isundefinedis no longer equal to an absent key, arrays compare element-wise,Dates compare by instant,NaNequalsNaN, and a self-referencing value is compared rather than throwing aTypeErrorout of a function typed to return a boolean.FeatureFlags.snapshot()andgetAll()now reject withFeatureFlagProviderErrorwhen the flags were never loaded, instead of resolving to an empty result that is indistinguishable from "no flags are configured". Once a load has succeeded they keep serving that data even if a later reload fails, and a provider that genuinely holds no flags still resolves empty.evaluate()is unchanged and still reportsreason: "error".- New
providerCooloffMsoption (default 5,000 ms;0restores the old behaviour) leaves a failing flag provider alone for that window instead of re-runninggetAll()andget(key)on every single evaluation during an outage. A successful call closes the window immediately andrefresh()always probes. CLIParser({ stopAtFirstArgument: true })no longer reports the first positional token as the command. The token now appears only inargs; previously it appeared in bothcommands/commandandargs.
@zudojs/api v1.2.0
- One operation, four transports, all through the same executor, interceptors and schema validation, with one client-safe error shape (
APIWireError):- HTTP:
createApiFetchHandler(operations), a web-standard fetch handler. Routes come frommetadata.http({ method, path: "/users/:id" }) and default toPOST /<name>. - RPC:
registerApiRpcProcedures(server, operations). Queues:bindApiQueue(queue, operations). CLI:runApiCli(operations, argv), which parses--field valueand--jsonand returns a sysexits-style exit code. describeApiRoutesreturns the structuralAPIOperationRoutecontract, andtoOpenAPIRouteDescriptors(operations, { basePath })feeds@zudojs/openapi'screateOpenAPIDocumentFromRoutes, success and error envelopes included. Both fetch handlers mount on@zudojs/httpwithmountFetchHandler(router, "/api", handler).TransportContextKeytells an interceptor which binding a call came through.
- HTTP:
- Security, behaviour change:
APIExecutorruns the interceptors before input validation; validation is the innermost step, immediately before the handler. Before, an anonymous call with invalid input got a422describing the schema instead of the401its authentication interceptor would have returned, logging/metrics/rate-limit interceptors never saw invalid calls, and an interceptor that replacedcontext.inputbypassed the schema.context.inputis now the input as the caller sent it, and a replacement is validated before the handler runs. - The handler's
context.signalaborts when the operation times out (reason: the504APITimeoutError) or the caller aborts (reason:OPERATION_CANCELLED). Every handler now receives a signal, even when the caller supplied none; before, the executor stopped waiting but the handler kept running. defineOperationinfers the handler'sinputfrom theinputschema (Standard Schema output type, or asafeParseschema'sdata), so inlineregistry.register(defineOperation({ input: TodoInput, handler: async (input) => input.title }))compiles. New typesInferAPISchemaOutput,APIInputSchema,DefineOperationWithSchemaOptions.- Hardening: every binding refuses
__proto__/constructor/prototypekeys in its input (400 over HTTP, a validation error elsewhere); the RPC binding no longer sends the message of anexpose: falseerror;createApiFetchHandleranswers415to a body route called with a non-JSON content type even when the body is empty, so a cross-site HTML form cannot trigger an input-less operation. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0.
@zudojs/auth v1.3.0
AccountLockedError(423) andAuthRateLimitError(429) carryretryAfterSecondsand aRetry-Afterheader, which@zudojs/httpcopies onto the response; for a lockout it is the time left on the lock.- Behaviour change for clients that match codes:
AccountLockedErrorisERR_ACCOUNT_LOCKED,AccountDeactivatedErrorisERR_ACCOUNT_DEACTIVATEDandTokenRevokedErrorisERR_TOKEN_REVOKED; all three used to beERR_FORBIDDEN.TokenRevokedErroris now401instead of403, since the client has to authenticate again. - Security:
login()normalizes the identifier beforefindUser()sees it (NFKC, trim, and lower-case for an email address). Use the newnormalizeLoginIdentifier()at registration so both sides agree;normalizeIdentifier: falsepasses the raw string. - New
createSessionForUser(userId, { method, ... })issues a session for a user authenticated outsidelogin(), such as an OAuth callback. It is off by default:methodmust be listed in the newexternalSessionMethodsoption. needsRehash()returnsfalsefor a hash made with@zudojs/crypto's ownhashPassword()defaults, which it used to flag on every login.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/permissions@1.3.0,@zudojs/crypto@1.3.0,@zudojs/constants@1.1.1.
@zudojs/auth-oauth v1.2.2
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0,@zudojs/security@1.3.0.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/security@1.2.0.
@zudojs/cache v1.2.0
- Behaviour change: an invalid tag (
tags: [""], over-long, or containing NUL) throwsERR_INVALID_INPUT, the same code as an invalid key, instead ofCACHE_OPERATION_FAILED. getStats().errorscounts rejected input: an invalid key, namespace, pattern or tag now counts and emitscache.error.failSilentlystill never hides invalid input.ttl()returns whole milliseconds, rounded down (it returned values like9999.52…). The README no longer mentions aCACHE_INVALID_KEYcode that was never thrown.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Cache and database correctness fixes.
CacheService.invalidateByPatternnow awaits the tag purge it triggers, matchingclear(). With an asynchronous tag store (a shared, Redis-backed one, for example) the tag-to-key mappings for the invalidated keys are now guaranteed to be gone by the time the call resolves, and a failure from the tag store is reported to the caller — or swallowed underfailSilently— instead of escaping as an unhandled rejection that would terminate the process.toPrismaIncludenow validates one include level per frame, so its depth bound actually applies to the nested tree. A deeply nestedincludeis refused with the documentedRangeError: Relation include depth exceeds the maximum of Nrather than overflowing the stack.toPrismaIncludeno longer treats a relation orselectfield whose name happens to be anObject.prototypemember (toString,valueOf,constructor,__proto__, …) as a duplicate or silently drops it. Such names are now handled as ordinary keys.getOrSetin the database cache no longer poisons a key permanently when the loader throws synchronously rather than returning a rejected promise. The failed load is evicted from the in-flight map and the next call invokes the loader again, as it already did for asynchronous failures.
zudojs-cli v2.1.0
- Projects work out of the box.
src/server.tsbuilds a router (registerRoutes), serves/openapi.jsonand/docswhen the openapi capability is on, applies@zudojs/securityheaders, closed-by-default CORS and rate limiting, and shuts down integrations → HTTP → runtime. Typed env config insrc/configs, a composition root insrc/container.ts, an example/api/v1/examplesCRUD resource and acreateHttpTestClienttest. Same wiring in modular-monolith modules and every microservice app. zudojs generate resource <name>writes a DTO, repository (in-memory, or Prisma when the app has it), service, controller, CRUD routes with OpenAPI metadata and a test, registered between// zudojs:*markers.route,controller,repositoryanddtowrite their layer plus any missing lower ones;--forcerewrites.zudojs addwrites real integrations:database(aliaspostgres/prisma; Prisma 7),redis,websockets,email,docker, plusqueue,scheduler,cache,messaging,observability,storage,openapi.docsandsecurityare refused with an explanation.- Generators add the
@zudojs/*packages they import to the owningpackage.json, and camelCase names keep their word boundaries (createBook→create-book). zudois an alias binary,newis an alias ofcreate, and running with no arguments in a terminal opens a numbered menu (never in CI or pipes). Thezudojsnpm package installs the CLI:npm install -g zudojs.- Parser fixes: only the first word selects a command; options before the command,
--port=and non-finite numbers are usage errors (exit 2); unknown commands exit 3 with "did you mean";--__proto__-style options are refused. @zudojs/*ranges in generated projects match this CLI build, and frontend fallbacks move to current majors (Vite 8, React 19.3, Next 16, Nuxt 4, Astro 7, Angular 22, SvelteKit 2.70); backends get TypeScript 7, Vitest 5 and @types/node 26.
- Fixes the README generated for a microservice project with no services, which told the reader to run a command 2.0.0 refuses.
zudojs generate serviceis refused in a microservice project — a service there is a whole workspace app, which the schematic does not produce — but the generated README still said “No services yet. Add one with:npx zudojs generate service <name>”, so the first thing a new project asked you to do failed. It now points atzudojs create <project> --architecture microservice --services <name>andzudojs generate module <name> --service <existing-service>.
- A full audit of the CLI: 55 findings, all fixed, each with a regression test that fails against the unfixed code. Read this before upgrading — several commands now refuse where they previously proceeded. In every case the old behaviour was a bug, but a script or CI job written against it will notice.
- Breaking — a failed dependency install now exits non-zero.
zudojs createused to downgrade an install failure to a warning and then print “Project created successfully” and exit 0, so a CI job went green with nonode_modules. The project is still kept and the retry hint is still printed; only the exit code and the closing message changed.zudojs addalready behaved this way — the two commands no longer disagree. - Breaking —
zudojs buildrefuses outside a Zudojs project.findProjectRootaccepted any ancestor holding a barepackage.json, so from an unrelated subdirectory the CLI climbed out and executed that project’sscripts.build— content from a file on disk — then reported success. It now requires a real Zudojs project and throwsCLINotInProjectErrorotherwise. - Breaking —
zudojs generateoutside a project throws instead of warning and writing files into the current directory, matchingdev,buildandadd. It also walks up to the project root, so running it from a subdirectory no longer creates a secondsrc/tree. - Breaking — cancelling a prompt exits 130, not 0.
zudojs create my-api && cd my-apino longer runs thecdafter you pressed Ctrl-C. Ctrl-C mid-scaffold is now honoured at all —@clack/promptsregisters a SIGINT listener per spinner that only prints “Canceled”, which suppressed Node’s default termination, so the run used to continue to completion; it now rolls back and exits 130. - Breaking —
CLI_ENVIRONMENTno longer carriesNODE_ENVorDEBUG: "DEBUG". Nothing read either, and honouring a bareDEBUGwould have changed behaviour. It now names the four variables the CLI really reads:ZUDOJS_DEBUG,CI,NO_UPDATE_CHECK,NPM_OFFLINE. - Breaking —
RollbackManager.rollback()returns aRollbackResultinstead ofvoid, andCapabilityResolutionResult.conflictsis gone — it was structurally incapable of being non-empty. - Breaking — a project name must now start with a letter or digit.
zudojs create -- --weirdused to create a directorycdcould not enter andrm -rfcould not remove. - Breaking — a schematic name may no longer start with a digit.
zudojs generate module 2faused to writeimport { 2faModule } …into your existingsrc/app.ts— a syntax error in the entry point — and exit 0. - Breaking — the printed app name is now
zudojsrather thanZudojs, so usage lines show the command you type. - New projects no longer arrive with services nobody asked for.
zudojs createinvented example domains when no service list was given — four for a microservice project (identity,enrollment,assessment,notification) and three for a modular monolith. A modular monolith was never even asked. An empty list now means no services: a microservice project gets its gateway, a modular monolith gets an empty module barrel, and both READMEs say how to add one. Named services are generated exactly as named. generate servicenow works in every architecture. It was broken in all three. In a monolith it wrote tosrc/<name>/while the template’s services live insrc/services/; it now nests under the template’s directory. In a modular monolith it loggedMapping "service" → "module"and then did not, producing four inert files the runtime never loaded; the mapping is now real and the module is registered inapp.ts. In a microservice project it created an app directory with nopackage.json, so pnpm skipped it andpnpm -r run buildnever compiled it; it now refuses and names the two commands that do work.- Generated projects pinned
latest. The frontend install path resolved every dependency to a pinned range and then passed only the names to the package manager, so twozudojs create --frontend reactruns a month apart produced different majors. The resolved range is now installed, and the resolver’s “no version range known” warnings are no longer discarded. - Ticking “Security” did nothing. The capabilities prompt offered eight options and the command read six;
eventsandsecuritywere silently dropped — no dependency, no manifest entry, no message. Both are now consumed. A new--capabilities <list>flag makes the interactive and non-interactive branches produce the same project. - Writes could escape the project through a symlink. Path containment was checked on the literal string only, so a symlinked subdirectory sent generated files to the symlink’s target. Containment is now re-checked after resolving the real path.
zudojs doctor’s feature check could never fail — the templates hardcodedzudojs.features: []. They now record the real capability list and install the packages backing it. The modular-monolith template ignored theenable*flags entirely, so--databaseinstalled nothing.pnpm run testfailed in a brand-new project. The sample spec wastests/index.ts, which matches no vitest include pattern, so the first thing you ran exited 1. It is nowtests/app.test.ts.- The manifest is now durable. It is written atomically, serialized by a lock so concurrent
zudojs addruns cannot lose an update, validated on read, and a corrupt manifest is reported distinctly from a missing one.addreads and validates it before touching anypackage.json, so a failure can no longer leave the project half-updated. - A framework scaffolder is no longer killed at 120 s and silently replaced by the built-in fallback template; it gets 15 minutes, the failure says whether it timed out, and the child’s real stderr is shown.
- Per-command help works:
zudojs create --helpprints usage, arguments, options, shorts and defaults, instead of rejecting--helpas an invalid option. - A flag-shaped token is no longer swallowed as an option value, so
--type --frontend reactnames the right problem. Surplus positionals are reported by every command. All four registries throw on a duplicate registration rather than silently replacing the earlier entry. - Not verified on Windows. The
cmd.exequoting hardening (arguments containing",%or!are now rejected rather than escaped, because a backslash is not a cmd escape) andNoDefaultCurrentDirectoryInExePathwere tested as pure functions on Linux by passing"win32"explicitly. They have not been exercised on a real Windows host.
- Breaking — a failed dependency install now exits non-zero.
- Tooling correctness fixes for the testing, docs, adapters, feature-flag and CLI packages.
createStub()now returns the same no-op function for a given property on every access, sostub.handler === stub.handler. Abus.on("x", stub.handler)/bus.off("x", stub.handler)pair written against a stub now actually removes the listener instead of leaking it between tests.InMemoryTestStorage.set(key, value, 0)now treats a zero TTL as a deadline of "now" — the entry is already expired on the next read. Only an omitted TTL means "never expires". A test that wrote0to mean "already stale" previously got an entry that never expired.generateMarkdownnow HTML-escapes the deprecation blockquote (deprecatedMessage) and the**Owner:**line, as every other text position it writes already did. A document built from untrusted JSON can no longer put raw<script>/<img>tags into generated markdown that a renderer with HTML enabled would execute.AdapterRegistry.healthAll()no longer loses an adapter named__proto__: the per-adapter report is built on a null-prototype object, so the entry is present, the aggregate status reflects it, and nothing writes through toObject.prototype.AdapterRegistry.register()now refuses the names__proto__,constructorandprototypewith anAdapterConfigurationError.AdapterOperationOptions.retryis now implemented rather than merely declared.healthAll({ retry: { attempts, delay } })re-runs a check that reportsunhealthyup toattemptstimes in total, pausingdelayms between tries;timeoutstill bounds each try and an aborted signal stops the retries immediately. Withoutretrythe behaviour is unchanged (one try).valuesEqualnow compares structurally instead of byJSON.stringify: key order no longer matters, a key whose value isundefinedis no longer equal to an absent key, arrays compare element-wise,Dates compare by instant,NaNequalsNaN, and a self-referencing value is compared rather than throwing aTypeErrorout of a function typed to return a boolean.FeatureFlags.snapshot()andgetAll()now reject withFeatureFlagProviderErrorwhen the flags were never loaded, instead of resolving to an empty result that is indistinguishable from "no flags are configured". Once a load has succeeded they keep serving that data even if a later reload fails, and a provider that genuinely holds no flags still resolves empty.evaluate()is unchanged and still reportsreason: "error".- New
providerCooloffMsoption (default 5,000 ms;0restores the old behaviour) leaves a failing flag provider alone for that window instead of re-runninggetAll()andget(key)on every single evaluation during an outage. A successful call closes the window immediately andrefresh()always probes. CLIParser({ stopAtFirstArgument: true })no longer reports the first positional token as the command. The token now appears only inargs; previously it appeared in bothcommands/commandandargs.
@zudojs/config v1.3.0
- Behaviour change: NUMBER and BOOLEAN schemas accept values from environment variables. String input is coerced before the type check, strictly and in decimal (
"8080"passes;"80a","0x1F90"and""do not); booleans accepttrue/false,1/0,yes/no,y/n,on/off. Set the new schema optioncoerce: falsefor the old strict behaviour. - Behaviour change:
validatereceives the final value. The order is coerce, type check, constraints,transform, thenvalidate, which matches its(value: T)signature. - A typed getter called with a fallback returns
Tinstead ofT | undefined, andget(key, fallback)is a new overload (its literal fallback is widened throughConfigWiden<T>). ScopedConfigResolverandConfigManagergainrequiredString,requiredNumber,requiredBooleanandrequiredDate.resolve()andresolveResult()take aTypedConfigSchema<T>, so each type accepts exactly the constraints the validator enforces.store.getByPrefix("db.")andgetObjectByPrefix("db.")ignore the trailing dot; they returned nothing before.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/config
- Secret detection now runs inside
ConfigStore.set(), so a key such asdb.password,api_keyor apostgres://user:pw@hostconnection string is marked sensitive however it was written — from a source, frominitialValues, fromset()/setMany()/replace()or frommanager.set(). Previously only values arriving through a source were redacted, andtoSafeObject()printed the identical key in clear when it had been seeded or set at runtime. Passsensitive: falseexplicitly to opt a key out. - A configuration source that declares no
prioritynow getsDEFAULT_CONFIG_SOURCE_PRIORITY(-1, newly exported) instead of0. Both defaulted to0before, and because a source overwrites on equal priority, any source created without a priority silently wiped a manager'sinitialValuesduringload(). Sources that declarepriority: 0or above still override them, as documented. If you relied on an undeclared source beating another source that declarespriority: 0, declare a priority on it. ConfigLoadernow deduplicates its constructor sources by name, first occurrence wins — the same ruleaddSource()andloadConfigSources()already enforced. Duplicates used to load twice, with the last one winning.initialValuesare seeded withsource: "initialValues"on every path, including a store the manager creates itself (it recorded"runtime"before).
@zudojs/logger
- A formatter that returns an object (
createStructuredLoggerFormatter()) now reaches the transport: the record is merged over the entry instead of being computed and discarded. String formatters are unchanged. - A metadata getter that throws no longer propagates out of
logger.info(...)and aborts the caller. The field becomes"[Unreadable]"(exported asLOGGER_UNREADABLE_TOKEN), the entry is still logged, and the read failure is reported like any other infrastructure failure — dropped by default, rethrown whenthrowTransportErrorsis on. - The cycle guard tracks the ancestor path instead of every object ever seen, so
{ actor: user, target: user }logs both fields; only a genuine back-edge becomes"[Circular]". Applies to redaction, serialization and the JSON formatter. MapandSetmetadata keep their contents instead of collapsing to{}: aMapserializes as an object (with per-key secret redaction) and aSetas an array.createLoggerManagerFromLogger(logger)now registers the logger with the manager's factory, somanager.flush()/manager.close()actually reach it andmanager.size/getAll()report it.LoggerManager.adopt(logger)andLoggerFactory.register(logger, name?)are new public methods.- Errors are now typed where they were generic: a transport write exceeding
transportTimeoutraisesLoggerTimeoutError(withtransportNameandtimeout), other write failuresLoggerTransportErrorwithtransportNameset, formatter failuresLoggerFormatterErrorwithformatterNameset, a closedLoggerManagerLoggerDisposedErrorinstead of a bareError, an unknown levelInvalidLoggerLevelError, an invalid entry timestampInvalidLoggerEntryError, an unresolved string formatter idLoggerFormatterNotFoundError, and a write to a closed buffered transportLoggerTransportClosedError. Code matching onRangeErroror on error message text from these paths needs updating.
- Secret detection now runs inside
@zudojs/constants v1.1.2
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0.
@zudojs/container v1.2.0
registerFactory,factoryProviderandprovideFactoryinfer the factory's parameter types from theinjectlist (newInjectedDependencies<Deps>andInjectedFactory<T, Deps>). Behaviour change: a factory that declares more parameters than its inject list supplies is a compile error.- Behaviour change:
autoRegisterClassesonly auto-registers a class whose constructor has no required parameters.resolve(NeedsDep)used to build it silently withdep = undefined; it now throwsRegistrationNotFoundErrorexplaining how to register it. - Error messages and default registration names show a symbol token by its description (
MissingService, notSymbol(MissingService)). - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/lifecycle
priorityis now a real ordering barrier instead of a hint. Components registered at one priority all complete a phase before the next priority starts, soregister(metrics, { priority: 100 })genuinely starts beforeregister(server, { priority: 0 }). Previously the whole dependency level was launched concurrently (up toconcurrency, default 10) and the sorted order was observable only atconcurrency: 1, so whichever hook happened to finish first won. Components sharing a priority still run together, so the default configuration — every component at priority 0 — is unchanged. Shutdown now mirrors startup within a level: the lowest priority stops first, the highest last. The same reversal applies to the exportedreverseTopologicalSort, which now reverses each stage's contents as well as the stage list.shutdown()no longer disposes a component whosestop()is still running. Astop()hook that blows its own componenttimeoutis abandoned rather than cancelled; shutdown only waited for such hooks before the stop phase, so one abandoned during it haddispose()run on top of it whileshutdown()resolved and reported the application DISPOSED. Each shutdown phase now waits for abandoned hooks to settle before the next one begins, still bounded by the globalshutdownTimeout, soawait shutdown(); process.exit(0)can no longer cut a drain short.- Registry and abort failures (
Cannot register components after registry is frozen,Component "x" is already registered, an unregistereddependsOntarget, and a cancelledwithAbort) now throwLifecycleErrorfrom@zudojs/errorsrather than a bareError, so they carry anErrorCodeand answerinstanceof LifecycleError. Messages are unchanged.
@zudojs/container
clearRegistrations()andrestoreSnapshot()now invalidate live scopes. Both already evicted and disposed cached singletons, but scopes were never told, so a scope went on serving the SCOPED instance built from a registration that had just been discarded — for the rest of its life, and without ever disposing it. A test harness that snapshotted, installed a SCOPED fake and then restored kept the fake. Every token that was cached when the registry is cleared or restored is now reported as invalidated, so live scopes drop and dispose their copies and the nextresolve()rebuilds from the current registration.Container "x" has already been disposed,Registrations for container "x" are frozen,Container scopes are disabled, the three disposed-scope guards, an unregistereduseExistingtarget and an unsupported provider now throwContainerError/ContainerLifecycleErrorfrom@zudojs/errorsrather than a bareError. Messages are unchanged.
@zudojs/runtime
LifecycleManagerwithcontinueOnFailure: trueno longer initializes or readies a module whose declared dependency failed. It previously consulted only the failure count, soapiwithdependencies: ["db"]had bothonInitializeandonReadyinvoked — and appeared instart().succeeded— afterdbfailed to come up. Such a module is now skipped, reported ininitialize().failedwith the blocking dependency named, and the skip cascades to its own dependents. Modules independent of the failure still continue, andcontinueOnFailure: false(the default, and whatcreateRuntime()uses) is unaffected.- Runtime option validation and
RuntimeRegistry.register()/require()now throwRuntimeError/RuntimeStateErrorrather than a bareError. Messages are unchanged.
@zudojs/core v1.2.2
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0,@zudojs/constants@1.1.2.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/constants@1.1.1.
@zudojs/cqrs v1.2.0
- Behaviour change:
unwrapCommandResult()throwsCommandFailedErrorfor a result whose status is"failure", andunwrapQueryResult()throwsQueryFailedError, instead of returning the failure payload as if it were the value. The payload is onerror.failureanderror.cause; both errors are re-exported. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/events@1.2.0,@zudojs/middleware@1.0.3.
@zudojs/crypto v1.3.1
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0,@zudojs/constants@1.1.2.
Closed four places where a security decision was made from input that could not support it, and two where a revoked grant kept answering from a cache. Every change here refuses more than it did before; none of them accepts anything new.
@zudojs/permissions
createPermissionRegistry()now hassubscribe(listener), the same change notifiercreateRoleRegistry()andcreatePolicyRegistry()already carried, and it fires ondefine, aremovethat removed something, andclear.createPermissionEngine({ expandImplied })accepts the registry itself in place of a closure — passexpandImplied: permissions— and subscribes to it, so revoking an implication drops the decisions that were cached while it stood. Previouslypermissions.remove("post:admin")left everypost:deleteit had implied answeringtruefor the whole cache TTL, whileskipCache: truecorrectly saidfalse. A bare(permission) => permissions.expandImplied(permission)still works, but it cannot announce a change, so an engine given one now caches no decisions rather than serving one made under a revoked implication.- An engine with a
roleResolveror apermissionResolverno longer caches decisions by default. A resolver reads authorization state the engine does not own and cannot see change, and none of it was in the cache key, so a grant withdrawn upstream kept being served until the entry expired. To get caching back, supply the newresolverCacheKey— a function of the actor returning something that changes whenever the resolver's answer for that actor could change (a grants-table version, anupdatedAtstamp). Returningundefinedleaves that actor uncached. Engines without a resolver are unaffected. - The README's request-metadata example imported
requireCurrentTenantfrom@zudojs/tenancy, which does not export it; it now usescreateContextManager({ storage: getDefaultStorage() }).requireCurrentTenant().id, which is where the method actually lives.
@zudojs/security
extractClientIpno longer readsX-Forwarded-Forwhen the chain is shorter than the configuredtrustProxycount. Such a chain did not pass through the proxies whose entries make it trustworthy, and the index clamp landed on the entry the client wrote — so withtrustProxy: 2a request arriving at an inner hop withX-Forwarded-For: 1.2.3.4was rate-limited as1.2.3.4, and rotating that value gave the caller a fresh bucket each time. Short chains now fall through tox-real-ipand thenremoteAddress. Chains at or above the configured length behave exactly as before.createCsrfProtectionandrequiresCsrfProtectionreject amethodslist that is empty, not an array, or contains a blank entry, withConfigurationError.methods: []used to turn CSRF off for every request in silence, which is whatprocess.env.CSRF_METHODS?.split(",").filter(Boolean) ?? []produces when the variable is unset. Omitmethodsfor the defaults.containsTraversalandvalidateRequestTargetstrip RFC 3986 path parameters before segmenting, so/a/..;/bis reported as traversal like every other spelling of it. Tomcat, Jetty and several reverse-proxy pairings resolve it to/a/../b.....//is still not a traversal, and nothing that was already caught has changed.sanitizeObjectrejects amaxDepththat is not an integer of 1 or more withConfigurationError.maxDepth: 0discarded the argument itself and returnedundefinedunder a non-optionalT.
@zudojs/crypto
- A provider's declared
capabilitiesare now consulted before every operation. A provider declaringsigning: falsehadsigncalled anyway; it now throws aCryptoErrornaming the capability and the operation.hash,hmac,encryption,signing,random,keyDerivationandpasswordHashingare all checked, including throughverifyPassword, which raises rather than reporting a missing capability as a wrong password. A provider that declares every capability it implements is unaffected. setDefaultCryptoProviderchecks that all twelve provider methods are functions and that every capability flag is a boolean, so installing a partial object fails at the call that installs it instead of throwing aTypeErrorfrom inside whichever operation reached the missing method first. A rejected provider is not installed.- New exports:
assertProviderCapability,assertCryptoProvider,assertRandomCapability,assertHashCapability,assertHmacCapability,assertPasswordHashingCapabilityandCRYPTO_PROVIDER_METHODS, for anyone writing their own provider or wrapper.
@zudojs/database v1.3.0
- Caller errors pass through transactions: a
@zudojs/errorsBaseErrorthat is not aDatabaseError(NotFoundError,ValidationError…) still rolls back, but is rethrown as the same instance instead of being wrapped in a500DatabaseError. NewisNonDatabaseBaseError(error). - Behaviour change: a missing, forged, tampered or malformed cursor throws a
400ValidationError(one issue oncursor) instead of aTypeErrorthat surfaced as a500. paginateCursorsetsmeta.previousCursorand accepts it to page backward. NewgetKeysetDirection,keysetFetchSort,reverseKeysetSort,KEYSET_BACKWARD_KEY.- The reconnect back-off wait keeps a script alive, and
disconnect()/destroy()cancel an in-progress reconnect. PrismaClientLikeaccepts a real Prisma 7 client generated into the application, without anas unknown ascast.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Cache and database correctness fixes.
CacheService.invalidateByPatternnow awaits the tag purge it triggers, matchingclear(). With an asynchronous tag store (a shared, Redis-backed one, for example) the tag-to-key mappings for the invalidated keys are now guaranteed to be gone by the time the call resolves, and a failure from the tag store is reported to the caller — or swallowed underfailSilently— instead of escaping as an unhandled rejection that would terminate the process.toPrismaIncludenow validates one include level per frame, so its depth bound actually applies to the nested tree. A deeply nestedincludeis refused with the documentedRangeError: Relation include depth exceeds the maximum of Nrather than overflowing the stack.toPrismaIncludeno longer treats a relation orselectfield whose name happens to be anObject.prototypemember (toString,valueOf,constructor,__proto__, …) as a duplicate or silently drops it. Such names are now handled as ordinary keys.getOrSetin the database cache no longer poisons a key permanently when the loader throws synchronously rather than returning a rejected promise. The failed load is evicted from the in-flight map and the next call invokes the loader again, as it already did for asynchronous failures.
@zudojs/docs v1.0.4
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0.
- Tooling correctness fixes for the testing, docs, adapters, feature-flag and CLI packages.
createStub()now returns the same no-op function for a given property on every access, sostub.handler === stub.handler. Abus.on("x", stub.handler)/bus.off("x", stub.handler)pair written against a stub now actually removes the listener instead of leaking it between tests.InMemoryTestStorage.set(key, value, 0)now treats a zero TTL as a deadline of "now" — the entry is already expired on the next read. Only an omitted TTL means "never expires". A test that wrote0to mean "already stale" previously got an entry that never expired.generateMarkdownnow HTML-escapes the deprecation blockquote (deprecatedMessage) and the**Owner:**line, as every other text position it writes already did. A document built from untrusted JSON can no longer put raw<script>/<img>tags into generated markdown that a renderer with HTML enabled would execute.AdapterRegistry.healthAll()no longer loses an adapter named__proto__: the per-adapter report is built on a null-prototype object, so the entry is present, the aggregate status reflects it, and nothing writes through toObject.prototype.AdapterRegistry.register()now refuses the names__proto__,constructorandprototypewith anAdapterConfigurationError.AdapterOperationOptions.retryis now implemented rather than merely declared.healthAll({ retry: { attempts, delay } })re-runs a check that reportsunhealthyup toattemptstimes in total, pausingdelayms between tries;timeoutstill bounds each try and an aborted signal stops the retries immediately. Withoutretrythe behaviour is unchanged (one try).valuesEqualnow compares structurally instead of byJSON.stringify: key order no longer matters, a key whose value isundefinedis no longer equal to an absent key, arrays compare element-wise,Dates compare by instant,NaNequalsNaN, and a self-referencing value is compared rather than throwing aTypeErrorout of a function typed to return a boolean.FeatureFlags.snapshot()andgetAll()now reject withFeatureFlagProviderErrorwhen the flags were never loaded, instead of resolving to an empty result that is indistinguishable from "no flags are configured". Once a load has succeeded they keep serving that data even if a later reload fails, and a provider that genuinely holds no flags still resolves empty.evaluate()is unchanged and still reportsreason: "error".- New
providerCooloffMsoption (default 5,000 ms;0restores the old behaviour) leaves a failing flag provider alone for that window instead of re-runninggetAll()andget(key)on every single evaluation during an outage. A successful call closes the window immediately andrefresh()always probes. CLIParser({ stopAtFirstArgument: true })no longer reports the first positional token as the command. The token now appears only inargs; previously it appeared in bothcommands/commandandargs.
@zudojs/errors v1.3.0
- New error classes:
CommandFailedErrorandQueryFailedError(codesERR_COMMAND_FAILED,ERR_QUERY_FAILED), thrown by@zudojs/cqrs'sunwrapCommandResult()/unwrapQueryResult().EventBusStoppedError, moved here from@zudojs/events;EventBusDisposedError's code is nowERR_EVENT_BUS_DISPOSED(wasERR_LIFECYCLE_DISPOSED).TransactionRollbackOnlyError extends TransactionRollbackError, for a commit refused because the transaction was marked rollback-only.TransactionRollbackErroraccepts an optionalmessage.
- New codes
ErrorCode.TOKEN_REVOKED,ACCOUNT_LOCKEDandACCOUNT_DEACTIVATED, used by@zudojs/auth, plusCOMMAND_FAILEDandQUERY_FAILED. SerializationDepthErrortakes an optional third argument{ statusCode?, expose? }; without it it is still an unexposed500.@zudojs/validation's depth guards now pass400/expose: true.SchemaErroris generic (SchemaError<TIssue = unknown>, likewiseSchemaErrorOptionsandcreateSchemaError); the default keeps existing code unchanged.RPCErrordeclares a readonlydetailsand accepts it as an option.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/errors
- New
EventListenerLimitExceededError(ErrorCode.EVENT_LISTENER_LIMIT_EXCEEDED), carryingpattern,countandlimit. It is reported as internal and is never exposed to a caller, because registering past a handler limit is a programming fault rather than bad input.
@zudojs/events
EventListenerLimitExceededErrorcan now actually be raised. Nothing in the package ever threw it before, so anycatchbranch testing for it was unreachable. ExceedingmaxHandlersPerPatternstill emits a one-shot warning by default; set the newenforceHandlerLimit: trueon a registry (or emitter/bus options) to refuse the registration instead, which throws the error and leaves the registry exactly as it was. The class is now owned by@zudojs/errorsand re-exported here, so existing imports keep working.- A bus or registry observer (
bus.subscribe,registry.subscribe) that throws is no longer discarded in silence. With noonErrorhook configured, the failure is now reported once per bus or registry on Node's process warning channel as aZudojsEventsWarningwith codeZUDOJS_EVENTS_OBSERVER_ERROR, matching how the handler-leak warning is already reported. A configuredonErrorhook still takes precedence and the warning is not emitted.
@zudojs/messaging
- A handler registered in object form —
{ handle(message, context) }, whichMessageHandlerLikehas always advertised — now actually runs. Previously the dispatcher invoked the handler as a function, so every dispatch to an object handler came back as a failed dispatch withhandler.handler is not a function.NamedMessageHandler.handlernow accepts either form, andthisis bound for class-based handlers. DispatchResult.handlerResultsis now a snapshot taken when the dispatch settles. A handler still running after a timeout can no longer push asuccess: truerecord into the result of a dispatch that already failed withMessageTimeoutError, so audit records and metrics derived fromhandlerResultsare stable once you have awaited the dispatch.- The
Dispatcherinterface now declaresdispose(),getRegistry()andlistMiddleware(), all of whichDefaultDispatcheralready implemented.createDispatcher().dispose()compiles without a cast.
- New
- Hardens the type and error primitives against untrusted input, and makes a handful of failure paths report the error a caller can actually act on.
BaseErrorno longer overflows the stack when a deeply nested object or array is attached as acause. The redaction walk is now bounded at 32 levels and truncates with"[MaxDepth]", exactly as metadata cloning already did, soJSON.stringify,serializeErrorwithincludeCauseandErrorHandler.toLogObjectstay safe on a parsed request body. Attacker-controlled depth could previously raise aRangeErrorfrom inside the logging path.estimateSerializedSize(value)now defaults to a finite budget (SerializationLimits.MAX_SIZE) instead ofInfinity. Because every occurrence of a shared subtree is charged, an unbounded budget let a 1 KB payload of shared references burn minutes of CPU. Pass an explicitNumber.POSITIVE_INFINITYif you need an exact measurement of input you trust; the returned value is otherwise capped at the budget.assertNoCircularReferencereports running out of depth asSerializationDepthErrorrather than dressing it up asCircularReferenceError, andJSONSerializer.serializewithpreserveTypespasses the caller'smaxDepthinto it. A deep but perfectly acyclic payload used to be rejected as a cycle on that path while the fast path reported a depth error for the same input; the two now agree.hasCircularReferencereturnsfalsefor such a graph instead oftrue.isArrayOfTypereads every index rather than relying onArray.prototype.every, which skips holes. A sparse array such asnew Array(3)no longer satisfies an arbitrary element guard.- A
$typetag arriving from the wire is checked againstSerializationLimits.MAX_TYPE_TAG_LENGTHbefore it is looked up, and is clipped before being quoted into an error message, so an over-long tag can no longer flood a log line. - The envelope trust boundary (
assertValidEnvelope,unwrapEnvelope,deserializeFromEnvelope) throwsInvalidSerializedDataErrorinstead of a bareError, a fullTransformerRegistrythrowsTransformerError, andunwrapSchemaResultthrowsSchemaErrorcarrying the recorded issues. Code that catchesErroris unaffected; code that wants to turn hostile input into a 400 can now tell it apart from an internal bug. Schema.safeParse's documentation no longer claims it never throws: a callback defect or aRangeErrorfrom stack exhaustion is still deliberately allowed to escape rather than being laundered into a validation issue.
@zudojs/events v1.3.0
- Behaviour change: in sequential dispatch, aborting the publish
signalwhile the last or only handler runs rejects withEventDispatchAbortedError; it used to resolve withhandled: true. - A handler's
timeoutMsaborts thecontext.signalthat handler received, with theEventTimeoutErroras the reason. bus.use()accepts registered middleware fromcreateEventMiddleware()and helpers such asvalidateEventMiddleware().EventBusStoppedErrorandEventBusDisposedErrorcome from@zudojs/errorsand are re-exported, soinstanceofworks from either import.EventBusDisposedError's code isERR_EVENT_BUS_DISPOSED.EventPublishResult.errorsandEventEmitResult.errorsare typedreadonly EventHandlerError[].- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/errors
- New
EventListenerLimitExceededError(ErrorCode.EVENT_LISTENER_LIMIT_EXCEEDED), carryingpattern,countandlimit. It is reported as internal and is never exposed to a caller, because registering past a handler limit is a programming fault rather than bad input.
@zudojs/events
EventListenerLimitExceededErrorcan now actually be raised. Nothing in the package ever threw it before, so anycatchbranch testing for it was unreachable. ExceedingmaxHandlersPerPatternstill emits a one-shot warning by default; set the newenforceHandlerLimit: trueon a registry (or emitter/bus options) to refuse the registration instead, which throws the error and leaves the registry exactly as it was. The class is now owned by@zudojs/errorsand re-exported here, so existing imports keep working.- A bus or registry observer (
bus.subscribe,registry.subscribe) that throws is no longer discarded in silence. With noonErrorhook configured, the failure is now reported once per bus or registry on Node's process warning channel as aZudojsEventsWarningwith codeZUDOJS_EVENTS_OBSERVER_ERROR, matching how the handler-leak warning is already reported. A configuredonErrorhook still takes precedence and the warning is not emitted.
@zudojs/messaging
- A handler registered in object form —
{ handle(message, context) }, whichMessageHandlerLikehas always advertised — now actually runs. Previously the dispatcher invoked the handler as a function, so every dispatch to an object handler came back as a failed dispatch withhandler.handler is not a function.NamedMessageHandler.handlernow accepts either form, andthisis bound for class-based handlers. DispatchResult.handlerResultsis now a snapshot taken when the dispatch settles. A handler still running after a timeout can no longer push asuccess: truerecord into the result of a dispatch that already failed withMessageTimeoutError, so audit records and metrics derived fromhandlerResultsare stable once you have awaited the dispatch.- The
Dispatcherinterface now declaresdispose(),getRegistry()andlistMiddleware(), all of whichDefaultDispatcheralready implemented.createDispatcher().dispose()compiles without a cast.
- New
@zudojs/feature-flags v1.4.0
- Behaviour change (security): the kill switch fails closed. A flag that is off —
enabled: false,state: "disabled"(not honoured at all before), a draft, archived, expired, or blocked by a dependency — serves its new optionaloffValue; without one,falsefor a boolean flag, ordefaultValuefor other types. Before, a killed flag withdefaultValue: truestayed on. DeclareoffValue: trueif you relied on that. - Behaviour change:
createEnvironmentProvidernormalises keys, soFEATURE_NEW_CHECKOUT=trueis the flagnew-checkout.get("NEW_CHECKOUT")still works; passkeyFormat: "preserve"for the old spelling. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Tooling correctness fixes for the testing, docs, adapters, feature-flag and CLI packages.
createStub()now returns the same no-op function for a given property on every access, sostub.handler === stub.handler. Abus.on("x", stub.handler)/bus.off("x", stub.handler)pair written against a stub now actually removes the listener instead of leaking it between tests.InMemoryTestStorage.set(key, value, 0)now treats a zero TTL as a deadline of "now" — the entry is already expired on the next read. Only an omitted TTL means "never expires". A test that wrote0to mean "already stale" previously got an entry that never expired.generateMarkdownnow HTML-escapes the deprecation blockquote (deprecatedMessage) and the**Owner:**line, as every other text position it writes already did. A document built from untrusted JSON can no longer put raw<script>/<img>tags into generated markdown that a renderer with HTML enabled would execute.AdapterRegistry.healthAll()no longer loses an adapter named__proto__: the per-adapter report is built on a null-prototype object, so the entry is present, the aggregate status reflects it, and nothing writes through toObject.prototype.AdapterRegistry.register()now refuses the names__proto__,constructorandprototypewith anAdapterConfigurationError.AdapterOperationOptions.retryis now implemented rather than merely declared.healthAll({ retry: { attempts, delay } })re-runs a check that reportsunhealthyup toattemptstimes in total, pausingdelayms between tries;timeoutstill bounds each try and an aborted signal stops the retries immediately. Withoutretrythe behaviour is unchanged (one try).valuesEqualnow compares structurally instead of byJSON.stringify: key order no longer matters, a key whose value isundefinedis no longer equal to an absent key, arrays compare element-wise,Dates compare by instant,NaNequalsNaN, and a self-referencing value is compared rather than throwing aTypeErrorout of a function typed to return a boolean.FeatureFlags.snapshot()andgetAll()now reject withFeatureFlagProviderErrorwhen the flags were never loaded, instead of resolving to an empty result that is indistinguishable from "no flags are configured". Once a load has succeeded they keep serving that data even if a later reload fails, and a provider that genuinely holds no flags still resolves empty.evaluate()is unchanged and still reportsreason: "error".- New
providerCooloffMsoption (default 5,000 ms;0restores the old behaviour) leaves a failing flag provider alone for that window instead of re-runninggetAll()andget(key)on every single evaluation during an outage. A successful call closes the window immediately andrefresh()always probes. CLIParser({ stopAtFirstArgument: true })no longer reports the first positional token as the command. The token now appears only inargs; previously it appeared in bothcommands/commandandargs.
@zudojs/http v1.4.0
- Guard responses are honoured. The router,
HttpMiddlewarePipelineandRouteDispatchersend a@zudojs/middlewareguard response with its status, headers and JSON body. A route middleware that returned a plain{ status, body, headers }object used to be ignored, soauthorize()and the tenancy guards refused requests that clients saw as200. New helpersapplyGuardResponseandguardResponseToContext;@zudojs/httpnow depends on@zudojs/middleware. - OpenAPI from routes. Routes take an
openapioption, andgenerateOpenAPIDocument(router, options),createRouterOpenAPIandmountOpenAPI(serves/openapi.json, optional YAML and a docs page at/docs) build the document from the routes the router actually registered.mountFetchHandler(router, basePath, handler)serves a web-standard(Request) => Responsehandler;toWebRequestis exported on its own. request.idreuses an incomingx-request-idof 1–128 characters of[A-Za-z0-9._:-](opt out withtrustRequestId: false;resolveIncomingRequestId()is exported), and the request guard's defaultrequestIdPatternmatches that rule.HttpClientretries a timed-out request for methods inretryMethods(defaultGET,HEAD,OPTIONS) under the newretryOnTimeout, with full-jitter backoff;jitter: falsewaits exactly the delay.- Route handlers may return a plain JSON value (sent as
200); route params are set before route middleware runs;createRateLimitMiddleware's429is JSON and always hasRetry-After;createHttpServertakes typedHttpServerOptions;new HttpError(415, msg)gets its code from the status (newdefaultErrorCode(status)). - Behaviour change: an error thrown in the middleware pipeline propagates as the error that was thrown, not wrapped in
HttpMiddlewareError/HttpMiddlewarePipelineError, soinstanceof NotFoundErrorworks in an outer middleware and inerrorHandler. - Fixes:
RequestContextInit.signalis honoured (the Node adapter aborts it on client disconnect); multipleSet-Cookieheaders from a returnedResponseare no longer folded into one; a streamed body is cancelled when the client disconnects. Security review:mountFetchHandler'soriginoption pins the origin instead of trustingHost, and a group'sopenapidefaults no longer publish hidden routes. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
Breaking-in-effect default:
X-Forwarded-*is no longer trusted automatically.NodeHTTPRequest— reached throughcreateHTTPRequest,NodeHTTPAdapter,createHTTPAdapter(),adaptNodeRequest()andadaptNodeContext()— used to readX-Forwarded-ForandX-Forwarded-Protofrom any client, with no trust check at all. A client connecting directly could set its ownrequest.ip(defeating an IP allowlist, per-IP rate limit, ban list or audit trail) and fliprequest.securetotrue(anX-Forwarded-Proto: wsswas enough), so an app gatingSecurecookies, HSTS or an https-only redirect onreq.securebelieved the request had arrived over TLS. The hardened Node adapter path (httpAdapter/node/) already gated these headers; this closes the parallel path that was left behind.These headers are now honoured only when the socket peer is a configured trusted proxy, and a forwarded protocol that is not
httporhttpsis discarded. If you run behind a proxy you must now opt in, with a newtrustProxyoption (address, CIDR range,"loopback"/"linklocal"/"all", hop count or predicate) that defaults tofalse:createHTTPAdapter({ trustProxy: "10.0.0.0/8" }); adaptNodeRequest(req, { trustProxy: "10.0.0.0/8" }); adaptNodeContext(req, res, { trustProxy: "10.0.0.0/8" }); createHTTPRequest(req, { trustProxy: "10.0.0.0/8" });Without it,
request.ipis the socket peer andrequest.protocolreflects the socket's own TLS state. The exportedgetRequestProtocol(request)andgetRequestIP(request)take the same value as an optional second argument.Also in this release:
- The shared agent registry can find what it created.
getAgent,hasAgentandremoveAgentlooked up a keygetOrCreateAgentnever wrote, so every lookup missed and the documented per-host teardown was a no-op that leaked the agent and its keep-alive sockets for the process lifetime. All four now build the same key;getAgent/hasAgenttake the same optional agent options, andremoveAgentwithout options destroys every agent registered for that host. createForwardedHeaderandformatKeepAliveHeaderno longer emit a raw CR or LF inside a quoted parameter. Both now escape through the package'sescapeHeaderQuotedStringand validate the finished field value, so aForwardedorKeep-Alivevalue carrying a control character throws aTypeErrorinstead of putting an attacker-chosen header on the wire.createSecurityMiddleware()with no options now emits the package's declared safe baseline (createDefaultSecurityHeaderOptions) —Content-Security-Policy,Strict-Transport-Security,Permissions-Policy, the cross-origin isolation headers andX-Permitted-Cross-Domain-Policies, on top of the three it emitted before. Explicit options still override it, anduseDefaults: falsestill emits only what you configure.guardRequestappliesmaxHeaderValueSizeand the CRLF filter to array-valued headers (set-cookie, and any header supplied as a list), which previously skipped both checks and still reportedallowed: true.createLoggingMiddleware({ includeHeaders: true })redacts credential headers —authorization,proxy-authorization,cookie,set-cookieand the rest of the@zudojs/loggersecret-field set — before the record reaches the logger. Extra names can be added withredactHeaders.- The redirect predicates accept a relative
Location.hasRedirectLoop,assertNoRedirectLoop,isSameOriginandisHTTPSthrewTypeError: Invalid URLon/a, which is both legal under RFC 9110 and what this module's owncreateRedirectemits by default. - The proxy SSRF blocklist covers
192.0.0.0/24(IETF protocol assignments) and198.18.0.0/15(benchmarking), which its JSDoc already claimed. runWithRequestContext/getCurrentRequestContextwork. TheAsyncLocalStoragebehind them was loaded throughglobalThis.require, which does not exist in ESM, so the store silently stayedundefined:runWithRequestContextmerely called its callback andgetCurrentRequestContext()always returnedundefined.request.pathand the router now agree about repeated slashes. A request for//admin/secretdispatched to the route registered at/admin/secretwhile a guard readingrequest.pathsaw//admin/secretand did not match. Repeated slashes are collapsed once, where both sides parse the request-target, sogetPathname("//admin/secret")is/admin/secret. An origin-form target is still never parsed as an authority.
- The shared agent registry can find what it created.
Harden and consolidate the HTTP query layer.
NodeHTTPRequest.query,createHTTPRequest()and theparseQueryStringthe package barrel exports all ran a second, unhardened query parser that accumulated into an object literal and readresult[key]without an own-property check. On fully attacker-controlled input that meant:?__proto__=a&__proto__=bassigned an array through the__proto__setter, replacing the returned query object's prototype. The parameter vanished from its own keys while the object silently gainedlength,mapand the rest ofArray.prototype.?constructor=xread the inheritedObjectconstructor as the "existing" value and stored it in the result, handing a handlerquery.constructor === [Object, "x"].- None of the four documented query limits applied, so a request carrying 50,000 parameters was parsed in full.
All of these paths now delegate to the hardened
httpQueryparser that the Node adapter and the router already used, so every entry point produces a null-prototype record, drops__proto__/constructor/prototype, and throwsHTTPQueryLimitError(414) on a limit breach.Also fixed in
httpQuery:getQueryStrings()threwTypeError: Cannot convert object to primitive valuefor?a[b]=1&a=2, because the parsed array holds a null-prototype object thatString()cannot coerce. It is now total over every parseable shape.getQueryString()returnednullwhile declaringstring | undefined; a literal?a=nullnow yields"null".hasQuery()andquerySize()answered from the raw search params rather than the parsed query, sohasQuery(req, "a")wasfalsefor?a[b]=1andhasQuery(req, "__proto__")wastruefor a key the parser drops. They now answer about the objectgetQuery()returns.maxKeyswas checked before comma expansion, so one parameter could expand past the cap undercommaSeparated. It now counts emitted pairs.maxTotalLengthandcommaSeparatedwere ignored when the input was aURLSearchParams; both entry points now share one tokenizer.cloneQuery()used aJSON.parse(JSON.stringify(…))round-trip, which rebuilt every level withObject.prototypeand so discarded the null prototype the parser exists to guarantee. It is now a structural deep copy.mergeQuery()assigned nested source objects by reference, so the merged result aliased its inputs. Values are deep-copied.stringifyQuery()/buildQueryString()had no depth or cycle guard and overflowed the stack with a bareRangeErroron a cyclic or deeply nested object. Both now throwHTTPQueryLimitError, and both accept amaxDepthoption.
QueryValueis now recursive (QueryPrimitive | QueryValue[] | QueryObject). The previousQueryPrimitive[]described a shape the parser could not produce, since?a[b]=1&a=2puts an object inside the array.httpQueryis split intoqueryTypes/,queryParse/,queryRequest/andquerySerialize/. The public API is unchanged and still re-exported from@zudojs/http.- Router, content negotiation and cache-control fixes.
- An
OPTIONSrequest that only matches routes registered under other methods no longer runs one of those handlers. The fallback now resolves to a synthetic route with no middleware that answers204with anAllowheader, which is whatHttpRouter.dispatch()already did. PreviouslyOPTIONS /accounts/42executed aDELETE /accounts/:idhandler — behind any CSRF or auth middleware that treatsOPTIONSas a safe method. - Headers, cookies, status and metadata that route middleware writes to
context.responseare kept when the handler runs. They used to be discarded whenever the handler returned its own response, so a guard that set a security header and callednext()had no effect on the response sent. - Route patterns are no longer truncated at the first
?, so the documented optional-parameter syntax (/account/:id?/profile,/files/{name?}) works.{name?}no longer throwsInvalidRoutePatternError, registering both/users/:idand/users/:id?no longer throws a spuriousRouteConflictError, and an optional parameter only claims a path segment when the segments after it still have input left. Request paths are unaffected: their query string is still stripped. strictTrailingSlashis honoured. A strict router now distinguishes/usersfrom/users/instead of storing the option and ignoring it.- Route precedence compares segments left to right by kind (literal, then parameter, then wildcard) instead of summing them into one score, so
/admin/*restnow wins over/:p/:q/:r/:sforGET /admin/a/b/c. Fully literal and mixed patterns rank as before. Allowhonours the router'scaseSensitiveoption, so a case-sensitive router no longer advertises a method belonging to a route that differs only by case.RouteDispatchOptions.preserveResponseis implemented: with it set, a handler's response is no longer merged into the response passed todispatch().calculateFreshness()/isFresh()age a cached response. The current age is now theAgeheader plus the time elapsed since the response'sDate, andExpiresis compared against the current time, so a stale response is finally reported stale.calculateFreshness()takes an optional third argument for the current time.getEncodingQuality()/getLanguageQuality()let the most specific preference win, so an explicitgzip;q=0is no longer overridden by*;q=1.negotiateEncoding()falls back toidentitywhen the client names only codings the server does not have, unlessidentity;q=0or a*;q=0excludes it.- New helpers are exported alongside the existing ones:
normalizeRoutePattern,normalizeMatchPath,splitRoutePattern,hasTrailingSlashandcompareSegmentSpecificity.normalizePathkeeps its current request-path behaviour.
- An
@zudojs/lifecycle v1.2.1
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes. - Republished against
@zudojs/errors@1.3.0,@zudojs/constants@1.1.2.
- @zudojs/lifecycle
priorityis now a real ordering barrier instead of a hint. Components registered at one priority all complete a phase before the next priority starts, soregister(metrics, { priority: 100 })genuinely starts beforeregister(server, { priority: 0 }). Previously the whole dependency level was launched concurrently (up toconcurrency, default 10) and the sorted order was observable only atconcurrency: 1, so whichever hook happened to finish first won. Components sharing a priority still run together, so the default configuration — every component at priority 0 — is unchanged. Shutdown now mirrors startup within a level: the lowest priority stops first, the highest last. The same reversal applies to the exportedreverseTopologicalSort, which now reverses each stage's contents as well as the stage list.shutdown()no longer disposes a component whosestop()is still running. Astop()hook that blows its own componenttimeoutis abandoned rather than cancelled; shutdown only waited for such hooks before the stop phase, so one abandoned during it haddispose()run on top of it whileshutdown()resolved and reported the application DISPOSED. Each shutdown phase now waits for abandoned hooks to settle before the next one begins, still bounded by the globalshutdownTimeout, soawait shutdown(); process.exit(0)can no longer cut a drain short.- Registry and abort failures (
Cannot register components after registry is frozen,Component "x" is already registered, an unregistereddependsOntarget, and a cancelledwithAbort) now throwLifecycleErrorfrom@zudojs/errorsrather than a bareError, so they carry anErrorCodeand answerinstanceof LifecycleError. Messages are unchanged.
@zudojs/container
clearRegistrations()andrestoreSnapshot()now invalidate live scopes. Both already evicted and disposed cached singletons, but scopes were never told, so a scope went on serving the SCOPED instance built from a registration that had just been discarded — for the rest of its life, and without ever disposing it. A test harness that snapshotted, installed a SCOPED fake and then restored kept the fake. Every token that was cached when the registry is cleared or restored is now reported as invalidated, so live scopes drop and dispose their copies and the nextresolve()rebuilds from the current registration.Container "x" has already been disposed,Registrations for container "x" are frozen,Container scopes are disabled, the three disposed-scope guards, an unregistereduseExistingtarget and an unsupported provider now throwContainerError/ContainerLifecycleErrorfrom@zudojs/errorsrather than a bareError. Messages are unchanged.
@zudojs/runtime
LifecycleManagerwithcontinueOnFailure: trueno longer initializes or readies a module whose declared dependency failed. It previously consulted only the failure count, soapiwithdependencies: ["db"]had bothonInitializeandonReadyinvoked — and appeared instart().succeeded— afterdbfailed to come up. Such a module is now skipped, reported ininitialize().failedwith the blocking dependency named, and the skip cascades to its own dependents. Modules independent of the failure still continue, andcontinueOnFailure: false(the default, and whatcreateRuntime()uses) is unaffected.- Runtime option validation and
RuntimeRegistry.register()/require()now throwRuntimeError/RuntimeStateErrorrather than a bareError. Messages are unchanged.
@zudojs/logger v1.4.0
entry.messageis again the raw message a transport receives; the formatter's rendering is in the newentry.formatted. Custom transports that printedentry.messageto get the formatted line should printentry.formatted ?? entry.message, or use the newformatTransportLine(entry).- The console transport prints the formatted line instead of a record object; with
createStructuredLoggerFormatter()it prints one JSON line per record (newtoJsonLogLine(record)). - Level names are accepted in any case wherever a level is configured:
createLogger({ level: "error" }),setLevel("DEBUG"),child({ level: "trace" })(typeLoggerLevelLike, newresolveLoggerLevel()). An unknown level now throws instead of silently disabling output. - An
Errorpassed as the second argument of a level method (logger.error("failed", err)) is logged as the entry's error with its stack instead of being dropped. createTextLoggerFormatter({ includeStackTrace: false })hides stacks everywhere, including anErrorinside metadata.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/config
- Secret detection now runs inside
ConfigStore.set(), so a key such asdb.password,api_keyor apostgres://user:pw@hostconnection string is marked sensitive however it was written — from a source, frominitialValues, fromset()/setMany()/replace()or frommanager.set(). Previously only values arriving through a source were redacted, andtoSafeObject()printed the identical key in clear when it had been seeded or set at runtime. Passsensitive: falseexplicitly to opt a key out. - A configuration source that declares no
prioritynow getsDEFAULT_CONFIG_SOURCE_PRIORITY(-1, newly exported) instead of0. Both defaulted to0before, and because a source overwrites on equal priority, any source created without a priority silently wiped a manager'sinitialValuesduringload(). Sources that declarepriority: 0or above still override them, as documented. If you relied on an undeclared source beating another source that declarespriority: 0, declare a priority on it. ConfigLoadernow deduplicates its constructor sources by name, first occurrence wins — the same ruleaddSource()andloadConfigSources()already enforced. Duplicates used to load twice, with the last one winning.initialValuesare seeded withsource: "initialValues"on every path, including a store the manager creates itself (it recorded"runtime"before).
@zudojs/logger
- A formatter that returns an object (
createStructuredLoggerFormatter()) now reaches the transport: the record is merged over the entry instead of being computed and discarded. String formatters are unchanged. - A metadata getter that throws no longer propagates out of
logger.info(...)and aborts the caller. The field becomes"[Unreadable]"(exported asLOGGER_UNREADABLE_TOKEN), the entry is still logged, and the read failure is reported like any other infrastructure failure — dropped by default, rethrown whenthrowTransportErrorsis on. - The cycle guard tracks the ancestor path instead of every object ever seen, so
{ actor: user, target: user }logs both fields; only a genuine back-edge becomes"[Circular]". Applies to redaction, serialization and the JSON formatter. MapandSetmetadata keep their contents instead of collapsing to{}: aMapserializes as an object (with per-key secret redaction) and aSetas an array.createLoggerManagerFromLogger(logger)now registers the logger with the manager's factory, somanager.flush()/manager.close()actually reach it andmanager.size/getAll()report it.LoggerManager.adopt(logger)andLoggerFactory.register(logger, name?)are new public methods.- Errors are now typed where they were generic: a transport write exceeding
transportTimeoutraisesLoggerTimeoutError(withtransportNameandtimeout), other write failuresLoggerTransportErrorwithtransportNameset, formatter failuresLoggerFormatterErrorwithformatterNameset, a closedLoggerManagerLoggerDisposedErrorinstead of a bareError, an unknown levelInvalidLoggerLevelError, an invalid entry timestampInvalidLoggerEntryError, an unresolved string formatter idLoggerFormatterNotFoundError, and a write to a closed buffered transportLoggerTransportClosedError. Code matching onRangeErroror on error message text from these paths needs updating.
- Secret detection now runs inside
@zudojs/messaging v1.2.0
- Behaviour change: an abort during the last or only handler fails the dispatch with
MessageDispatchAbortedError(success: false); it used to reportsuccess: true. send(input, { context: { correlationId, causationId } })puts those identifiers on the message it builds, socreateDerivedMessagecontinues the chain.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/errors
- New
EventListenerLimitExceededError(ErrorCode.EVENT_LISTENER_LIMIT_EXCEEDED), carryingpattern,countandlimit. It is reported as internal and is never exposed to a caller, because registering past a handler limit is a programming fault rather than bad input.
@zudojs/events
EventListenerLimitExceededErrorcan now actually be raised. Nothing in the package ever threw it before, so anycatchbranch testing for it was unreachable. ExceedingmaxHandlersPerPatternstill emits a one-shot warning by default; set the newenforceHandlerLimit: trueon a registry (or emitter/bus options) to refuse the registration instead, which throws the error and leaves the registry exactly as it was. The class is now owned by@zudojs/errorsand re-exported here, so existing imports keep working.- A bus or registry observer (
bus.subscribe,registry.subscribe) that throws is no longer discarded in silence. With noonErrorhook configured, the failure is now reported once per bus or registry on Node's process warning channel as aZudojsEventsWarningwith codeZUDOJS_EVENTS_OBSERVER_ERROR, matching how the handler-leak warning is already reported. A configuredonErrorhook still takes precedence and the warning is not emitted.
@zudojs/messaging
- A handler registered in object form —
{ handle(message, context) }, whichMessageHandlerLikehas always advertised — now actually runs. Previously the dispatcher invoked the handler as a function, so every dispatch to an object handler came back as a failed dispatch withhandler.handler is not a function.NamedMessageHandler.handlernow accepts either form, andthisis bound for class-based handlers. DispatchResult.handlerResultsis now a snapshot taken when the dispatch settles. A handler still running after a timeout can no longer push asuccess: truerecord into the result of a dispatch that already failed withMessageTimeoutError, so audit records and metrics derived fromhandlerResultsare stable once you have awaited the dispatch.- The
Dispatcherinterface now declaresdispose(),getRegistry()andlistMiddleware(), all of whichDefaultDispatcheralready implemented.createDispatcher().dispose()compiles without a cast.
- New
@zudojs/middleware v1.1.0
- New guard-response contract:
createGuardResponse({ status, body?, headers? }),isGuardResponse(), theGuardResponsetype and theGUARD_RESPONSEbrand (Symbol.for("zudojs.middleware.guardResponse")). A structured body getscontent-type: application/jsonby default, and a status outside 100–599 throwsRangeError. It lets a framework-neutral guard refuse a request with a real401/403that@zudojs/httpsends as-is; before, a guard that returned a plain{ status, body, headers }object stopped the handler but the client still saw200. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0.
@zudojs/observability v1.2.0
- Behaviour change (security): redaction is on by default. Without a
redactionoption, log contexts and span attributes are redacted, sopassword,token,authorizationand the rest are no longer exported in the clear. The default rules reuse@zudojs/logger's secret-field matcher. Passredaction: falseto turn it off. shutdown()exports the final metric snapshot once instead of twice.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Tightened three places where caller-controlled input was not bounded, and one where a generated document did not match the contract it described.
@zudojs/rpc—assertValidRequestnow bounds every caller-controlled part of the frame, not justpayload.request.idis capped at the newMAX_RPC_REQUEST_ID_LENGTH(128, overridable withlimits.maxRequestIdLength,0to disable), andmetadatais measured alongsidepayloadagainstlimits.maxPayloadBytes. Previously an unboundedmetadataobject reached middleware and handlers ascontext.metadatahowever large it was, and an unboundedidwas echoed verbatim into both the success and the error response.RPCServer.handleno longer reflects an id that exceeds the limit. Frames that were already inside the limits are unaffected; a frame whosepayloadandmetadatatogether now exceedmaxPayloadBytesis rejected withRPCInvalidRequestErrorwhere it used to be accepted.@zudojs/openapi—addRoutenow detects duplicates on the OpenAPI path template rather than the source path, soGET /users/:idandGET /users/{id}are recognised as the same route and the second is rejected. Both used to register, and generation then silently replaced the first with the second: one operation disappeared from the published document withvalidate()reporting no errors.hasRoute,setRouteandremoveRouteaccept either spelling for the same route.@zudojs/openapi— an object schema that strips unknown keys no longer emitsadditionalProperties: false. That keyword means "reject the payload", whilestripaccepts it and discards the extra key, so a client generated from such a document refused requests the service accepts. Only.strict()emits it now. This also removes a difference betweens.object({…})ands.object({…}).strip(), which validate identically but used to document differently. Regenerate any checked-in spec: objects that are not.strict()lose theiradditionalProperties: false.@zudojs/observability— queue-overflow reports from the batch log and span processors are rate limited. A stalled exporter used to make every subsequentlogger.info()synchronously allocate anErrorand re-enter the configuredonError— usually writing to the sink that was already failing. The first drop is still reported immediately; after that, at most one report per minute, each carrying the running total.@zudojs/observability— a span attribute named__proto__is now recorded instead of silently vanishing, on both span attributes and event attributes. Storing it by plain assignment invoked the prototype setter, which dropped the attribute and replaced the bag's prototype; the injected prototype then let unlimited further attributes past themaxAttributescap. Inherited names such astoStringare counted against the cap too.
@zudojs/openapi v1.5.0
- Transport-neutral
createOpenAPIDocumentFromRoutes(routes, options)/createOpenAPIManagerFromRoutesover the structuralOpenAPIRouteDescriptor, plusOpenAPIManager.setRoutes()androuteWarnings(). Route metadata accepts@zudojs/schemaor raw schemas forparams,query,headers,cookies,bodyand responseschema. Every path template slot is documented even when undeclared, andsecurity: []is no longer dropped. - Behaviour change: a route with no documented responses no longer gets an invented
"200": { description: "OK" }. It gets a spec-validdefaultresponse described as "Undocumented response" (UNDOCUMENTED_RESPONSE_DESCRIPTION) and a warning, sent torouteWarnings(),onSchemaWarningand the newonRouteWarningoption. Declare the responses an operation returns to silence it. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Tightened three places where caller-controlled input was not bounded, and one where a generated document did not match the contract it described.
@zudojs/rpc—assertValidRequestnow bounds every caller-controlled part of the frame, not justpayload.request.idis capped at the newMAX_RPC_REQUEST_ID_LENGTH(128, overridable withlimits.maxRequestIdLength,0to disable), andmetadatais measured alongsidepayloadagainstlimits.maxPayloadBytes. Previously an unboundedmetadataobject reached middleware and handlers ascontext.metadatahowever large it was, and an unboundedidwas echoed verbatim into both the success and the error response.RPCServer.handleno longer reflects an id that exceeds the limit. Frames that were already inside the limits are unaffected; a frame whosepayloadandmetadatatogether now exceedmaxPayloadBytesis rejected withRPCInvalidRequestErrorwhere it used to be accepted.@zudojs/openapi—addRoutenow detects duplicates on the OpenAPI path template rather than the source path, soGET /users/:idandGET /users/{id}are recognised as the same route and the second is rejected. Both used to register, and generation then silently replaced the first with the second: one operation disappeared from the published document withvalidate()reporting no errors.hasRoute,setRouteandremoveRouteaccept either spelling for the same route.@zudojs/openapi— an object schema that strips unknown keys no longer emitsadditionalProperties: false. That keyword means "reject the payload", whilestripaccepts it and discards the extra key, so a client generated from such a document refused requests the service accepts. Only.strict()emits it now. This also removes a difference betweens.object({…})ands.object({…}).strip(), which validate identically but used to document differently. Regenerate any checked-in spec: objects that are not.strict()lose theiradditionalProperties: false.@zudojs/observability— queue-overflow reports from the batch log and span processors are rate limited. A stalled exporter used to make every subsequentlogger.info()synchronously allocate anErrorand re-enter the configuredonError— usually writing to the sink that was already failing. The first drop is still reported immediately; after that, at most one report per minute, each carrying the running total.@zudojs/observability— a span attribute named__proto__is now recorded instead of silently vanishing, on both span attributes and event attributes. Storing it by plain assignment invoked the prototype setter, which dropped the attribute and replaced the bag's prototype; the injected prototype then let unlimited further attributes past themaxAttributescap. Inherited names such astoStringare counted against the cap too.
@zudojs/permissions v1.4.0
- Security, behaviour change: a policy's allow is no longer a grant. A policy is by default an extra condition on top of RBAC/ABAC (
effect: "constrain"): it can deny, but the actor's roles, permissions or rules must still grant the permission. A policy that establishes the right on its own opts in witheffect: "grant";createPermissionEngine({ defaultPolicyEffect: "grant" })restores the old behaviour. If you relied on a policy to grant access, those checks now deny until you addeffect: "grant". NewPolicyEffect,policyGrants,DEFAULT_POLICY_EFFECT. createForbiddenResponse,createUnauthorizedResponseandcreateJsonResponse— and soauthorize()and the require-permission middleware — return@zudojs/middlewareguard responses, which@zudojs/httpnow sends with their real status instead of200.PermissionHttpResponseis an alias ofGuardResponse.- The mirrored
HttpMiddlewaretypes are generic over whatnext()returns, so every guard fits a route'smiddlewarelist withoutas never. NewHttpMiddlewareOutcome. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
Closed four places where a security decision was made from input that could not support it, and two where a revoked grant kept answering from a cache. Every change here refuses more than it did before; none of them accepts anything new.
@zudojs/permissions
createPermissionRegistry()now hassubscribe(listener), the same change notifiercreateRoleRegistry()andcreatePolicyRegistry()already carried, and it fires ondefine, aremovethat removed something, andclear.createPermissionEngine({ expandImplied })accepts the registry itself in place of a closure — passexpandImplied: permissions— and subscribes to it, so revoking an implication drops the decisions that were cached while it stood. Previouslypermissions.remove("post:admin")left everypost:deleteit had implied answeringtruefor the whole cache TTL, whileskipCache: truecorrectly saidfalse. A bare(permission) => permissions.expandImplied(permission)still works, but it cannot announce a change, so an engine given one now caches no decisions rather than serving one made under a revoked implication.- An engine with a
roleResolveror apermissionResolverno longer caches decisions by default. A resolver reads authorization state the engine does not own and cannot see change, and none of it was in the cache key, so a grant withdrawn upstream kept being served until the entry expired. To get caching back, supply the newresolverCacheKey— a function of the actor returning something that changes whenever the resolver's answer for that actor could change (a grants-table version, anupdatedAtstamp). Returningundefinedleaves that actor uncached. Engines without a resolver are unaffected. - The README's request-metadata example imported
requireCurrentTenantfrom@zudojs/tenancy, which does not export it; it now usescreateContextManager({ storage: getDefaultStorage() }).requireCurrentTenant().id, which is where the method actually lives.
@zudojs/security
extractClientIpno longer readsX-Forwarded-Forwhen the chain is shorter than the configuredtrustProxycount. Such a chain did not pass through the proxies whose entries make it trustworthy, and the index clamp landed on the entry the client wrote — so withtrustProxy: 2a request arriving at an inner hop withX-Forwarded-For: 1.2.3.4was rate-limited as1.2.3.4, and rotating that value gave the caller a fresh bucket each time. Short chains now fall through tox-real-ipand thenremoteAddress. Chains at or above the configured length behave exactly as before.createCsrfProtectionandrequiresCsrfProtectionreject amethodslist that is empty, not an array, or contains a blank entry, withConfigurationError.methods: []used to turn CSRF off for every request in silence, which is whatprocess.env.CSRF_METHODS?.split(",").filter(Boolean) ?? []produces when the variable is unset. Omitmethodsfor the defaults.containsTraversalandvalidateRequestTargetstrip RFC 3986 path parameters before segmenting, so/a/..;/bis reported as traversal like every other spelling of it. Tomcat, Jetty and several reverse-proxy pairings resolve it to/a/../b.....//is still not a traversal, and nothing that was already caught has changed.sanitizeObjectrejects amaxDepththat is not an integer of 1 or more withConfigurationError.maxDepth: 0discarded the argument itself and returnedundefinedunder a non-optionalT.
@zudojs/crypto
- A provider's declared
capabilitiesare now consulted before every operation. A provider declaringsigning: falsehadsigncalled anyway; it now throws aCryptoErrornaming the capability and the operation.hash,hmac,encryption,signing,random,keyDerivationandpasswordHashingare all checked, including throughverifyPassword, which raises rather than reporting a missing capability as a wrong password. A provider that declares every capability it implements is unaffected. setDefaultCryptoProviderchecks that all twelve provider methods are functions and that every capability flag is a boolean, so installing a partial object fails at the call that installs it instead of throwing aTypeErrorfrom inside whichever operation reached the missing method first. A rejected provider is not installed.- New exports:
assertProviderCapability,assertCryptoProvider,assertRandomCapability,assertHashCapability,assertHmacCapability,assertPasswordHashingCapabilityandCRYPTO_PROVIDER_METHODS, for anyone writing their own provider or wrapper.
@zudojs/plugins v1.3.0
- An async
PluginEvents.emitthat rejects is contained and reported like a synchronous throw instead of becoming anunhandledRejectionafterstart()resolves. - An
@zudojs/eventsEventBusis accepted bynew PluginManager({ events })andcreatePluginContext(meta, { events }), adapted by the newtoPluginEvents(). NewisPluginEventBus,PluginEventBus,PluginEventSource. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0.
@zudojs/queue v1.4.0
- Behaviour change: process lifetime. Pending work keeps the Node.js process alive, and a started
Workerkeeps it alive untilstop(). An idle, paused or closed queue never does. PasskeepAlive: false(onQueueOptionsandWorkerOptions) for the old unreferenced timers. - Behaviour change: payloads. The default
JsonSerializerpreserves types, so aDatereaches the processor as aDate;BigInt,Map,Set,Uint8ArrayandErrorround-trip too. PasspreserveTypes: falsefor plain JSON. - Behaviour change: ordering. Within a priority, a job is ordered by when it became runnable, so a delayed job no longer jumps ahead of jobs already waiting.
- Work no longer waits for the next poll:
add()and every other event that makes a job runnable wake the poller at once. New optionalQueue.onJobReady(listener), which aWorkersubscribes to onstart().pollIntervalis honoured on every poll. - New 1-based
JobContext.attemptNumber.PassthroughSerializerreally passes payloads through, andqueue.eventsworks without configuration. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/queue
- Security.
zudo:contextis now owned by the queue on every enqueue path. Job metadata handed toqueue.add()can no longer carry a context record of its own: whatever the caller put under that key is dropped before the job is stored, whether or not a context carrier captured anything. Previously, anadd()made with no ambient context kept the caller's record verbatim and the queue replayed it around the middleware and the processor, so an enqueuer could choose the tenant, correlation id or trace the job ran under. Legitimately captured context is unaffected. runJobno longer leaks anabortlistener per job on a consumer's signal. A worker passes one long-lived signal to every job it dispatches, so a long-running worker accumulated one listener — and one retained per-jobAbortController— for every job it had ever processed.- The default in-memory dead letter store is bounded. It retains the most recent 1000 dead-lettered jobs and evicts the oldest beyond that;
createInMemoryDeadLetterStore({ maxEntries })sets a different cap, andNumber.POSITIVE_INFINITYrestores the previous unbounded behaviour. A store the queue created for itself is also cleared byclose(); one you passed in asdeadLetterStoreis left alone, as before. - A throwing queue event listener now reaches the configured logger. The emitter accepts a
loggerof its own (createInMemoryQueueEventEmitter({ logger })), and a queue created withloggerhands it to the emitter it was given, so the failure goes tologger.errorinstead of always falling back toprocess.emitWarning. - The four events that
QueueEventMapdeclared but nothing emitted now fire.worker:started,worker:stoppedandworker:errorare published bycreateWorkeron the queue's emitter, reachable through the new optionalQueue.events.job:cancelledis emitted when a running job is aborted from outside — a draining worker,close(), a consumer's signal — and not for a job that merely timed out.
@zudojs/scheduler
handle.cancel()aborts a running one-shot (after()/at()), as the README says and as a recurring schedule already did. In-flight executions are now tracked by schedule id, so a cancel arriving after the schedule was retired at dispatch time still reaches the run.- Cron day-of-week ranges that span Sunday are parsed correctly.
0-7,1-7andmon-sunall mean every day; previously0-7was accepted and quietly fired once a week, and1-7andmon-sunwere rejected as inverted ranges.fri-sunandsat-sunwork for the same reason. A bare7is still Sunday, and a genuinely inverted range such as5-2is still an error. - A schedule whose job has been unregistered is retired and reported through
onErrorwith aSchedulerJobNotFoundError, instead of re-arming its timer forever while dispatching nothing and still reporting itself as active. handle.resume()on a schedule that is already active is a no-op. It used to recompute the next fire time from now, so a supervisor calling it idempotently could postpone an hourly job indefinitely. Resuming a paused schedule is unchanged.
- Security.
@zudojs/rpc v1.4.0
- Transports ship in the package.
createRPCMemoryTransport(server)connects a client in the same process;createRPCHttpTransport({ url })calls a remote server withfetch;createRPCFetchHandler(server)is a web-standard(Request) => Promise<Response>handler that bounds bodies, answers every failure with an RPC error frame and never sends stack traces. - The client rebuilds typed errors from the wire (
RPCValidationError,RPCProcedureNotFoundError,RPCAuthenticationError,RPCForbiddenError…) and keeps the wirecodeanddetails.mapRPCErrorexposes the server's mapping. error.codeis always the wire code.RPC_TIMEOUT,RPC_CANCELLEDandRPC_UNAVAILABLEused to come back with class codes (ERR_RPC_TIMEOUT…). A@zudojs/errorserror thrown withexpose: truereaches the caller under the matching code (newRPC_NOT_FOUND404 andRPC_CONFLICT409, plusRPC_VALIDATION_ERROR,RPC_UNAUTHENTICATED,RPC_FORBIDDEN,RPC_RATE_LIMITED…) instead ofRPC_INTERNAL_ERROR.- Security: the server refuses (
RPC_INVALID_REQUEST) a frame whosepayloadormetadataholds a__proto__,constructororprototypekey at any depth. A caller that disconnects or aborts cancels the procedure (RPC_CANCELLED) instead of letting it run to its timeout. - The client deadline and
retry()backoff timers keep a script alive until the call settles (Node used to exit with code 13 first);createRPCHttpTransportclamps an over-long timeout to the timer range. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Tightened three places where caller-controlled input was not bounded, and one where a generated document did not match the contract it described.
@zudojs/rpc—assertValidRequestnow bounds every caller-controlled part of the frame, not justpayload.request.idis capped at the newMAX_RPC_REQUEST_ID_LENGTH(128, overridable withlimits.maxRequestIdLength,0to disable), andmetadatais measured alongsidepayloadagainstlimits.maxPayloadBytes. Previously an unboundedmetadataobject reached middleware and handlers ascontext.metadatahowever large it was, and an unboundedidwas echoed verbatim into both the success and the error response.RPCServer.handleno longer reflects an id that exceeds the limit. Frames that were already inside the limits are unaffected; a frame whosepayloadandmetadatatogether now exceedmaxPayloadBytesis rejected withRPCInvalidRequestErrorwhere it used to be accepted.@zudojs/openapi—addRoutenow detects duplicates on the OpenAPI path template rather than the source path, soGET /users/:idandGET /users/{id}are recognised as the same route and the second is rejected. Both used to register, and generation then silently replaced the first with the second: one operation disappeared from the published document withvalidate()reporting no errors.hasRoute,setRouteandremoveRouteaccept either spelling for the same route.@zudojs/openapi— an object schema that strips unknown keys no longer emitsadditionalProperties: false. That keyword means "reject the payload", whilestripaccepts it and discards the extra key, so a client generated from such a document refused requests the service accepts. Only.strict()emits it now. This also removes a difference betweens.object({…})ands.object({…}).strip(), which validate identically but used to document differently. Regenerate any checked-in spec: objects that are not.strict()lose theiradditionalProperties: false.@zudojs/observability— queue-overflow reports from the batch log and span processors are rate limited. A stalled exporter used to make every subsequentlogger.info()synchronously allocate anErrorand re-enter the configuredonError— usually writing to the sink that was already failing. The first drop is still reported immediately; after that, at most one report per minute, each carrying the running total.@zudojs/observability— a span attribute named__proto__is now recorded instead of silently vanishing, on both span attributes and event attributes. Storing it by plain assignment invoked the prototype setter, which dropped the attribute and replaced the bag's prototype; the injected prototype then let unlimited further attributes past themaxAttributescap. Inherited names such astoStringare counted against the cap too.
@zudojs/runtime v1.3.0
- Behaviour change:
start()passes through every state in order (created → initializing → initialized → starting → running), and publishesruntime.initializedandruntime.starting. RuntimeInitializationError,RuntimeRollbackErrorandRuntimeSignalErrorare now thrown or logged where they apply; the first two extendRuntimeStartError.- Behaviour change:
failedis no longer terminal;TERMINAL_STATESis["stopped"]. - New
disposeContainerOnStop: truemakesstop()dispose the container;createTestRuntimedoes so by default. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/lifecycle
priorityis now a real ordering barrier instead of a hint. Components registered at one priority all complete a phase before the next priority starts, soregister(metrics, { priority: 100 })genuinely starts beforeregister(server, { priority: 0 }). Previously the whole dependency level was launched concurrently (up toconcurrency, default 10) and the sorted order was observable only atconcurrency: 1, so whichever hook happened to finish first won. Components sharing a priority still run together, so the default configuration — every component at priority 0 — is unchanged. Shutdown now mirrors startup within a level: the lowest priority stops first, the highest last. The same reversal applies to the exportedreverseTopologicalSort, which now reverses each stage's contents as well as the stage list.shutdown()no longer disposes a component whosestop()is still running. Astop()hook that blows its own componenttimeoutis abandoned rather than cancelled; shutdown only waited for such hooks before the stop phase, so one abandoned during it haddispose()run on top of it whileshutdown()resolved and reported the application DISPOSED. Each shutdown phase now waits for abandoned hooks to settle before the next one begins, still bounded by the globalshutdownTimeout, soawait shutdown(); process.exit(0)can no longer cut a drain short.- Registry and abort failures (
Cannot register components after registry is frozen,Component "x" is already registered, an unregistereddependsOntarget, and a cancelledwithAbort) now throwLifecycleErrorfrom@zudojs/errorsrather than a bareError, so they carry anErrorCodeand answerinstanceof LifecycleError. Messages are unchanged.
@zudojs/container
clearRegistrations()andrestoreSnapshot()now invalidate live scopes. Both already evicted and disposed cached singletons, but scopes were never told, so a scope went on serving the SCOPED instance built from a registration that had just been discarded — for the rest of its life, and without ever disposing it. A test harness that snapshotted, installed a SCOPED fake and then restored kept the fake. Every token that was cached when the registry is cleared or restored is now reported as invalidated, so live scopes drop and dispose their copies and the nextresolve()rebuilds from the current registration.Container "x" has already been disposed,Registrations for container "x" are frozen,Container scopes are disabled, the three disposed-scope guards, an unregistereduseExistingtarget and an unsupported provider now throwContainerError/ContainerLifecycleErrorfrom@zudojs/errorsrather than a bareError. Messages are unchanged.
@zudojs/runtime
LifecycleManagerwithcontinueOnFailure: trueno longer initializes or readies a module whose declared dependency failed. It previously consulted only the failure count, soapiwithdependencies: ["db"]had bothonInitializeandonReadyinvoked — and appeared instart().succeeded— afterdbfailed to come up. Such a module is now skipped, reported ininitialize().failedwith the blocking dependency named, and the skip cascades to its own dependents. Modules independent of the failure still continue, andcontinueOnFailure: false(the default, and whatcreateRuntime()uses) is unaffected.- Runtime option validation and
RuntimeRegistry.register()/require()now throwRuntimeError/RuntimeStateErrorrather than a bareError. Messages are unchanged.
@zudojs/scheduler v1.2.0
- Behaviour change: process lifetime. A started scheduler keeps the Node.js process alive until
stop(); a script that only ran a scheduler used to exit 0 with nothing run.SchedulerOptions.keepAlive: falserestores the old behaviour. stop()is idempotent;Clockis exported from the package root;CronParseErrormessages are no longer garbled;getExecutions()records the real attempt; newJobContext.attemptNumber.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- @zudojs/queue
- Security.
zudo:contextis now owned by the queue on every enqueue path. Job metadata handed toqueue.add()can no longer carry a context record of its own: whatever the caller put under that key is dropped before the job is stored, whether or not a context carrier captured anything. Previously, anadd()made with no ambient context kept the caller's record verbatim and the queue replayed it around the middleware and the processor, so an enqueuer could choose the tenant, correlation id or trace the job ran under. Legitimately captured context is unaffected. runJobno longer leaks anabortlistener per job on a consumer's signal. A worker passes one long-lived signal to every job it dispatches, so a long-running worker accumulated one listener — and one retained per-jobAbortController— for every job it had ever processed.- The default in-memory dead letter store is bounded. It retains the most recent 1000 dead-lettered jobs and evicts the oldest beyond that;
createInMemoryDeadLetterStore({ maxEntries })sets a different cap, andNumber.POSITIVE_INFINITYrestores the previous unbounded behaviour. A store the queue created for itself is also cleared byclose(); one you passed in asdeadLetterStoreis left alone, as before. - A throwing queue event listener now reaches the configured logger. The emitter accepts a
loggerof its own (createInMemoryQueueEventEmitter({ logger })), and a queue created withloggerhands it to the emitter it was given, so the failure goes tologger.errorinstead of always falling back toprocess.emitWarning. - The four events that
QueueEventMapdeclared but nothing emitted now fire.worker:started,worker:stoppedandworker:errorare published bycreateWorkeron the queue's emitter, reachable through the new optionalQueue.events.job:cancelledis emitted when a running job is aborted from outside — a draining worker,close(), a consumer's signal — and not for a job that merely timed out.
@zudojs/scheduler
handle.cancel()aborts a running one-shot (after()/at()), as the README says and as a recurring schedule already did. In-flight executions are now tracked by schedule id, so a cancel arriving after the schedule was retired at dispatch time still reaches the run.- Cron day-of-week ranges that span Sunday are parsed correctly.
0-7,1-7andmon-sunall mean every day; previously0-7was accepted and quietly fired once a week, and1-7andmon-sunwere rejected as inverted ranges.fri-sunandsat-sunwork for the same reason. A bare7is still Sunday, and a genuinely inverted range such as5-2is still an error. - A schedule whose job has been unregistered is retired and reported through
onErrorwith aSchedulerJobNotFoundError, instead of re-arming its timer forever while dispatching nothing and still reporting itself as active. handle.resume()on a schedule that is already active is a no-op. It used to recompute the next fire time from now, so a supervisor calling it idempotently could postpone an hourly job indefinitely. Resuming a paused schedule is unchanged.
- Security.
@zudojs/schema v1.2.0
string().url()still accepts onlyhttp/httpsby default and now takesurl({ protocols: ["postgres", "redis"] })or{ protocols: "any" }.date(),datetime()andtime()validate real values:"2026-02-30","2026-13-45"and"2026-02-30T25:61:00Z"used to pass.- An optional key absent from the input stays absent from the parsed object (it came back as an own key set to
undefined); the inferred type makes it an optional property (newObjectShapeOutput). partial()no longer applies.default()to absent keys, so an update schema no longer resets every defaulted field the caller left out.- Every primitive has
.optional(),.nullable(),.default(),.refine()and.transform()through the newModifiableSchemabase class;schema.boolean().optional()was a type error. SchemaInputof a transform is its input type;TransformSchema<TIn, TOut>extendsSchema<TOut, TIn>. NewisSchemaValidationError(error)guard, and count messages use the singular for one.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Hardens the type and error primitives against untrusted input, and makes a handful of failure paths report the error a caller can actually act on.
BaseErrorno longer overflows the stack when a deeply nested object or array is attached as acause. The redaction walk is now bounded at 32 levels and truncates with"[MaxDepth]", exactly as metadata cloning already did, soJSON.stringify,serializeErrorwithincludeCauseandErrorHandler.toLogObjectstay safe on a parsed request body. Attacker-controlled depth could previously raise aRangeErrorfrom inside the logging path.estimateSerializedSize(value)now defaults to a finite budget (SerializationLimits.MAX_SIZE) instead ofInfinity. Because every occurrence of a shared subtree is charged, an unbounded budget let a 1 KB payload of shared references burn minutes of CPU. Pass an explicitNumber.POSITIVE_INFINITYif you need an exact measurement of input you trust; the returned value is otherwise capped at the budget.assertNoCircularReferencereports running out of depth asSerializationDepthErrorrather than dressing it up asCircularReferenceError, andJSONSerializer.serializewithpreserveTypespasses the caller'smaxDepthinto it. A deep but perfectly acyclic payload used to be rejected as a cycle on that path while the fast path reported a depth error for the same input; the two now agree.hasCircularReferencereturnsfalsefor such a graph instead oftrue.isArrayOfTypereads every index rather than relying onArray.prototype.every, which skips holes. A sparse array such asnew Array(3)no longer satisfies an arbitrary element guard.- A
$typetag arriving from the wire is checked againstSerializationLimits.MAX_TYPE_TAG_LENGTHbefore it is looked up, and is clipped before being quoted into an error message, so an over-long tag can no longer flood a log line. - The envelope trust boundary (
assertValidEnvelope,unwrapEnvelope,deserializeFromEnvelope) throwsInvalidSerializedDataErrorinstead of a bareError, a fullTransformerRegistrythrowsTransformerError, andunwrapSchemaResultthrowsSchemaErrorcarrying the recorded issues. Code that catchesErroris unaffected; code that wants to turn hostile input into a 400 can now tell it apart from an internal bug. Schema.safeParse's documentation no longer claims it never throws: a callback defect or aRangeErrorfrom stack exhaustion is still deliberately allowed to escape rather than being laundered into a validation issue.
@zudojs/security v1.3.0
- Behaviour change (security):
serializeCookie/createSecureCookievalidateDomainas a hostname andPathas free of control characters,;and,; anything else throwsValidationError. - The CSRF checks return
falsefor a non-string token, a request with no method, or a malformed header or cookie bag, instead of throwing aTypeError. - New
findUnsafeKey(value)returns the first__proto__,constructororprototypekey anywhere in decoded data;@zudojs/rpcand@zudojs/apiuse it to refuse prototype-polluting input. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
Closed four places where a security decision was made from input that could not support it, and two where a revoked grant kept answering from a cache. Every change here refuses more than it did before; none of them accepts anything new.
@zudojs/permissions
createPermissionRegistry()now hassubscribe(listener), the same change notifiercreateRoleRegistry()andcreatePolicyRegistry()already carried, and it fires ondefine, aremovethat removed something, andclear.createPermissionEngine({ expandImplied })accepts the registry itself in place of a closure — passexpandImplied: permissions— and subscribes to it, so revoking an implication drops the decisions that were cached while it stood. Previouslypermissions.remove("post:admin")left everypost:deleteit had implied answeringtruefor the whole cache TTL, whileskipCache: truecorrectly saidfalse. A bare(permission) => permissions.expandImplied(permission)still works, but it cannot announce a change, so an engine given one now caches no decisions rather than serving one made under a revoked implication.- An engine with a
roleResolveror apermissionResolverno longer caches decisions by default. A resolver reads authorization state the engine does not own and cannot see change, and none of it was in the cache key, so a grant withdrawn upstream kept being served until the entry expired. To get caching back, supply the newresolverCacheKey— a function of the actor returning something that changes whenever the resolver's answer for that actor could change (a grants-table version, anupdatedAtstamp). Returningundefinedleaves that actor uncached. Engines without a resolver are unaffected. - The README's request-metadata example imported
requireCurrentTenantfrom@zudojs/tenancy, which does not export it; it now usescreateContextManager({ storage: getDefaultStorage() }).requireCurrentTenant().id, which is where the method actually lives.
@zudojs/security
extractClientIpno longer readsX-Forwarded-Forwhen the chain is shorter than the configuredtrustProxycount. Such a chain did not pass through the proxies whose entries make it trustworthy, and the index clamp landed on the entry the client wrote — so withtrustProxy: 2a request arriving at an inner hop withX-Forwarded-For: 1.2.3.4was rate-limited as1.2.3.4, and rotating that value gave the caller a fresh bucket each time. Short chains now fall through tox-real-ipand thenremoteAddress. Chains at or above the configured length behave exactly as before.createCsrfProtectionandrequiresCsrfProtectionreject amethodslist that is empty, not an array, or contains a blank entry, withConfigurationError.methods: []used to turn CSRF off for every request in silence, which is whatprocess.env.CSRF_METHODS?.split(",").filter(Boolean) ?? []produces when the variable is unset. Omitmethodsfor the defaults.containsTraversalandvalidateRequestTargetstrip RFC 3986 path parameters before segmenting, so/a/..;/bis reported as traversal like every other spelling of it. Tomcat, Jetty and several reverse-proxy pairings resolve it to/a/../b.....//is still not a traversal, and nothing that was already caught has changed.sanitizeObjectrejects amaxDepththat is not an integer of 1 or more withConfigurationError.maxDepth: 0discarded the argument itself and returnedundefinedunder a non-optionalT.
@zudojs/crypto
- A provider's declared
capabilitiesare now consulted before every operation. A provider declaringsigning: falsehadsigncalled anyway; it now throws aCryptoErrornaming the capability and the operation.hash,hmac,encryption,signing,random,keyDerivationandpasswordHashingare all checked, including throughverifyPassword, which raises rather than reporting a missing capability as a wrong password. A provider that declares every capability it implements is unaffected. setDefaultCryptoProviderchecks that all twelve provider methods are functions and that every capability flag is a boolean, so installing a partial object fails at the call that installs it instead of throwing aTypeErrorfrom inside whichever operation reached the missing method first. A rejected provider is not installed.- New exports:
assertProviderCapability,assertCryptoProvider,assertRandomCapability,assertHashCapability,assertHmacCapability,assertPasswordHashingCapabilityandCRYPTO_PROVIDER_METHODS, for anyone writing their own provider or wrapper.
@zudojs/serialization v1.2.0
- A custom
transformersregistry keeps the built-in transformers (Date, BigInt, Map, Set, Buffer, Error) behind it instead of silently replacing them;builtins: falseuses only yours. NewcreateBuiltinTransformers(). - A transformer's
serializemay return just the value, which is wrapped as{ $type, $value }for you. - With
preserveTypes, a value no transformer handles that JSON would write as{}throwsSerializeErrornaming the type instead of losing its contents. - Behaviour change: a depth failure from
JSONSerializeris a400SerializationDepthError, via@zudojs/validation's guards. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Hardens the type and error primitives against untrusted input, and makes a handful of failure paths report the error a caller can actually act on.
BaseErrorno longer overflows the stack when a deeply nested object or array is attached as acause. The redaction walk is now bounded at 32 levels and truncates with"[MaxDepth]", exactly as metadata cloning already did, soJSON.stringify,serializeErrorwithincludeCauseandErrorHandler.toLogObjectstay safe on a parsed request body. Attacker-controlled depth could previously raise aRangeErrorfrom inside the logging path.estimateSerializedSize(value)now defaults to a finite budget (SerializationLimits.MAX_SIZE) instead ofInfinity. Because every occurrence of a shared subtree is charged, an unbounded budget let a 1 KB payload of shared references burn minutes of CPU. Pass an explicitNumber.POSITIVE_INFINITYif you need an exact measurement of input you trust; the returned value is otherwise capped at the budget.assertNoCircularReferencereports running out of depth asSerializationDepthErrorrather than dressing it up asCircularReferenceError, andJSONSerializer.serializewithpreserveTypespasses the caller'smaxDepthinto it. A deep but perfectly acyclic payload used to be rejected as a cycle on that path while the fast path reported a depth error for the same input; the two now agree.hasCircularReferencereturnsfalsefor such a graph instead oftrue.isArrayOfTypereads every index rather than relying onArray.prototype.every, which skips holes. A sparse array such asnew Array(3)no longer satisfies an arbitrary element guard.- A
$typetag arriving from the wire is checked againstSerializationLimits.MAX_TYPE_TAG_LENGTHbefore it is looked up, and is clipped before being quoted into an error message, so an over-long tag can no longer flood a log line. - The envelope trust boundary (
assertValidEnvelope,unwrapEnvelope,deserializeFromEnvelope) throwsInvalidSerializedDataErrorinstead of a bareError, a fullTransformerRegistrythrowsTransformerError, andunwrapSchemaResultthrowsSchemaErrorcarrying the recorded issues. Code that catchesErroris unaffected; code that wants to turn hostile input into a 400 can now tell it apart from an internal bug. Schema.safeParse's documentation no longer claims it never throws: a callback defect or aRangeErrorfrom stack exhaustion is still deliberately allowed to escape rather than being laundered into a validation issue.
@zudojs/storage v1.2.0
- Behaviour change:
STORAGE_LOCK_ACQUIRE_TIMEOUTis409(it was504), andSTORAGE_CONNECTION_ACQUIRE_TIMEOUT/STORAGE_CONNECTION_TIMEOUTare503. Error codes are unchanged. - Behaviour change:
BaseRepository.create()andupdate()leave a property whose value isundefinedout of the SQL instead of writingNULL; an explicitnullstill writesNULL. - Behaviour change:
StorageLifecycleManager.drain()andshutdown()visit components one at a time in reverse registration order. - New
writableColumns,filterableColumnsandsortableColumnsoptions, each defaulting tocolumns. - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/types@1.1.1,@zudojs/serialization@1.1.1,@zudojs/constants@1.1.1.
@zudojs/tenancy v1.3.0
- The tenant middleware (
createResolveTenantMiddleware,createRequireTenantMiddleware,createTenantGuardMiddleware) and the helperscreateBadRequest,createUnauthorized,createForbidden,createNotFound,createJsonErrorResponsereturn@zudojs/middlewareguard responses, which@zudojs/httpnow sends with their real status instead of200. NewcreateJsonResponse(status, body). createResolverChain([...])infers its context from the resolvers (newResolverChainContext), and the mirroredHttpMiddlewaretypes fit a route'smiddlewarelist withoutas never.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0,@zudojs/constants@1.1.1.
@zudojs/testing v1.2.0
- New
createHttpTestClient(target), a supertest-style client that drives a real app over HTTP: a base URL, a Node server or listener, a web fetch handler, or an@zudojs/httpserver, adapter, router or pipeline. Fluent.get/.post/…,.set,.query,.send,.auth, a cookie jar, and chained.expect(status),.expectJson(partial),.expectText(). - The recording doubles record every path:
createTestEventBus().busis the double itself, andcreateTestMessageBus()/createTestQueue()record calls made on the underlying bus or queue too. Before, those calls ran but recorded nothing. - Behaviour change:
createTestApplication()is quiet and deterministic by default: a silentcreateSpyLogger(name)and a clock pinned atDEFAULT_TEST_APPLICATION_TIME(2026-01-01T00:00:00.000Z). Passlogger,clockorstartTimeto opt back in. createTestQueue().queueforwardsonJobReady, so a Worker on a test queue wakes as soon as a job is added.InMemoryTestStorage.set()rejects aNaNTTL.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Tooling correctness fixes for the testing, docs, adapters, feature-flag and CLI packages.
createStub()now returns the same no-op function for a given property on every access, sostub.handler === stub.handler. Abus.on("x", stub.handler)/bus.off("x", stub.handler)pair written against a stub now actually removes the listener instead of leaking it between tests.InMemoryTestStorage.set(key, value, 0)now treats a zero TTL as a deadline of "now" — the entry is already expired on the next read. Only an omitted TTL means "never expires". A test that wrote0to mean "already stale" previously got an entry that never expired.generateMarkdownnow HTML-escapes the deprecation blockquote (deprecatedMessage) and the**Owner:**line, as every other text position it writes already did. A document built from untrusted JSON can no longer put raw<script>/<img>tags into generated markdown that a renderer with HTML enabled would execute.AdapterRegistry.healthAll()no longer loses an adapter named__proto__: the per-adapter report is built on a null-prototype object, so the entry is present, the aggregate status reflects it, and nothing writes through toObject.prototype.AdapterRegistry.register()now refuses the names__proto__,constructorandprototypewith anAdapterConfigurationError.AdapterOperationOptions.retryis now implemented rather than merely declared.healthAll({ retry: { attempts, delay } })re-runs a check that reportsunhealthyup toattemptstimes in total, pausingdelayms between tries;timeoutstill bounds each try and an aborted signal stops the retries immediately. Withoutretrythe behaviour is unchanged (one try).valuesEqualnow compares structurally instead of byJSON.stringify: key order no longer matters, a key whose value isundefinedis no longer equal to an absent key, arrays compare element-wise,Dates compare by instant,NaNequalsNaN, and a self-referencing value is compared rather than throwing aTypeErrorout of a function typed to return a boolean.FeatureFlags.snapshot()andgetAll()now reject withFeatureFlagProviderErrorwhen the flags were never loaded, instead of resolving to an empty result that is indistinguishable from "no flags are configured". Once a load has succeeded they keep serving that data even if a later reload fails, and a provider that genuinely holds no flags still resolves empty.evaluate()is unchanged and still reportsreason: "error".- New
providerCooloffMsoption (default 5,000 ms;0restores the old behaviour) leaves a failing flag provider alone for that window instead of re-runninggetAll()andget(key)on every single evaluation during an outage. A successful call closes the window immediately andrefresh()always probes. CLIParser({ stopAtFirstArgument: true })no longer reports the first positional token as the command. The token now appears only inargs; previously it appeared in bothcommands/commandandargs.
@zudojs/transactions v1.2.0
- Behaviour change: committing a rollback-only transaction throws
TransactionRollbackOnlyError(aTransactionRollbackErrorsubclass) instead of the misleading "rollback failed". - Behaviour change:
manager.rollback()on a committed transaction throwsTransactionStateErrorinstead of silently doing nothing. Transaction.signalaborts with aTransactionTimeoutErroron timeout, andrun()stops waiting, rolls back and rejects. NewraceSignaland adapter handle accessorsgetTransactionHandle,currentTransactionHandle,manager.getCurrentHandle().timed_outis emitted once per timeout.- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- No source changes in this release. Republished against
@zudojs/errors@1.2.0.
@zudojs/types v1.2.0
- New
formatCount(count, singular, plural?), which@zudojs/schemaand@zudojs/validationuse so messages read "at least 1 character" rather than "1 characters". - The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Hardens the type and error primitives against untrusted input, and makes a handful of failure paths report the error a caller can actually act on.
BaseErrorno longer overflows the stack when a deeply nested object or array is attached as acause. The redaction walk is now bounded at 32 levels and truncates with"[MaxDepth]", exactly as metadata cloning already did, soJSON.stringify,serializeErrorwithincludeCauseandErrorHandler.toLogObjectstay safe on a parsed request body. Attacker-controlled depth could previously raise aRangeErrorfrom inside the logging path.estimateSerializedSize(value)now defaults to a finite budget (SerializationLimits.MAX_SIZE) instead ofInfinity. Because every occurrence of a shared subtree is charged, an unbounded budget let a 1 KB payload of shared references burn minutes of CPU. Pass an explicitNumber.POSITIVE_INFINITYif you need an exact measurement of input you trust; the returned value is otherwise capped at the budget.assertNoCircularReferencereports running out of depth asSerializationDepthErrorrather than dressing it up asCircularReferenceError, andJSONSerializer.serializewithpreserveTypespasses the caller'smaxDepthinto it. A deep but perfectly acyclic payload used to be rejected as a cycle on that path while the fast path reported a depth error for the same input; the two now agree.hasCircularReferencereturnsfalsefor such a graph instead oftrue.isArrayOfTypereads every index rather than relying onArray.prototype.every, which skips holes. A sparse array such asnew Array(3)no longer satisfies an arbitrary element guard.- A
$typetag arriving from the wire is checked againstSerializationLimits.MAX_TYPE_TAG_LENGTHbefore it is looked up, and is clipped before being quoted into an error message, so an over-long tag can no longer flood a log line. - The envelope trust boundary (
assertValidEnvelope,unwrapEnvelope,deserializeFromEnvelope) throwsInvalidSerializedDataErrorinstead of a bareError, a fullTransformerRegistrythrowsTransformerError, andunwrapSchemaResultthrowsSchemaErrorcarrying the recorded issues. Code that catchesErroris unaffected; code that wants to turn hostile input into a 400 can now tell it apart from an internal bug. Schema.safeParse's documentation no longer claims it never throws: a callback defect or aRangeErrorfrom stack exhaustion is still deliberately allowed to escape rather than being laundered into a validation issue.
@zudojs/validation v1.1.0
- Behaviour change:
assertDepthWithinLimitandassertNoCircularReferencethrowSerializationDepthErrorwithstatusCode: 400andexpose: true, so a request body nested too deep is a client error instead of a hidden500. The message contains only the observed depth and the limit. NewUNTRUSTED_DEPTH_ERRORconstant. - Length and count messages use the singular for one ("at least 1 item").
- The npm
homepagenow links to this package's documentation page on zudojs.oyinlola.site instead of the GitHub README. Development toolchain updated to Vitest 5.0.1 and @types/node 26.6.2; no runtime changes.
- Hardens the type and error primitives against untrusted input, and makes a handful of failure paths report the error a caller can actually act on.
BaseErrorno longer overflows the stack when a deeply nested object or array is attached as acause. The redaction walk is now bounded at 32 levels and truncates with"[MaxDepth]", exactly as metadata cloning already did, soJSON.stringify,serializeErrorwithincludeCauseandErrorHandler.toLogObjectstay safe on a parsed request body. Attacker-controlled depth could previously raise aRangeErrorfrom inside the logging path.estimateSerializedSize(value)now defaults to a finite budget (SerializationLimits.MAX_SIZE) instead ofInfinity. Because every occurrence of a shared subtree is charged, an unbounded budget let a 1 KB payload of shared references burn minutes of CPU. Pass an explicitNumber.POSITIVE_INFINITYif you need an exact measurement of input you trust; the returned value is otherwise capped at the budget.assertNoCircularReferencereports running out of depth asSerializationDepthErrorrather than dressing it up asCircularReferenceError, andJSONSerializer.serializewithpreserveTypespasses the caller'smaxDepthinto it. A deep but perfectly acyclic payload used to be rejected as a cycle on that path while the fast path reported a depth error for the same input; the two now agree.hasCircularReferencereturnsfalsefor such a graph instead oftrue.isArrayOfTypereads every index rather than relying onArray.prototype.every, which skips holes. A sparse array such asnew Array(3)no longer satisfies an arbitrary element guard.- A
$typetag arriving from the wire is checked againstSerializationLimits.MAX_TYPE_TAG_LENGTHbefore it is looked up, and is clipped before being quoted into an error message, so an over-long tag can no longer flood a log line. - The envelope trust boundary (
assertValidEnvelope,unwrapEnvelope,deserializeFromEnvelope) throwsInvalidSerializedDataErrorinstead of a bareError, a fullTransformerRegistrythrowsTransformerError, andunwrapSchemaResultthrowsSchemaErrorcarrying the recorded issues. Code that catchesErroris unaffected; code that wants to turn hostile input into a 400 can now tell it apart from an internal bug. Schema.safeParse's documentation no longer claims it never throws: a callback defect or aRangeErrorfrom stack exhaustion is still deliberately allowed to escape rather than being laundered into a validation issue.
zudojs v1.0.1
- The
zudojspackage is the short way to install the CLI:npm install -g zudojsputs both thezudojsand the shorterzudocommand on your PATH, and runszudojs-cli. This release depends onzudojs-cli@2.1.0.