Skip to content

build!: binden monorepo with npm workspaces and lockstep releases - #246

Open
vansergen wants to merge 675 commits into
mainfrom
build/monorepo
Open

vansergen wants to merge 675 commits into
mainfrom
build/monorepo

Conversation

@vansergen

@vansergen vansergen commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Checklist:

  • I have run npm run ci:all locally and all tests are passing (Node.js 24.16 and 26.11).
  • If this is a significant change, an issue has already been created where the problem / solution was discussed: N/A

Description

Moves binden, @binden/logger, @binden/body, @binden/compression and @binden/multipart into this repository as npm workspaces (packages/*). All packages are released together with one version by semantic-release. Besides the infrastructure and CI, it updates the dependencies, fixes the library bugs found while reviewing this PR (see Library fixes and Breaking changes), and relicenses every package under MIT.

What changes

  • History. binden is moved with git mv, so its history and tags are kept. The sources of logger, body, compression and multipart are copied without their history: it stays in their repositories, which will be archived. The import commit lists the upstream SHAs. The unreleased changes of @binden/logger (pino 10, MIT license) are separate commits, so they show up in the release notes.

  • Lockstep releases. scripts/release.mjs is called from @semantic-release/exec:

    • verify: fails if any package is already published at the next version;
    • version: sets the version in every workspace, rewrites internal dependency ranges to ^<version> and refreshes the lockfile (in the checkout of the release job only);
    • publish: publishes the packages in dependency order (logger → binden → body → compression → multipart).

    npm run release:check (run in CI) checks that all workspaces stay in lockstep. Only a confirmed E404 from the registry counts as "not published" in verify (network or auth errors stop the release before anything is tagged), and publish runs the lockstep check first.

  • Recovery. semantic-release pushes the tag before the packages are published. A release that fails after its tag is recovered by running the CI workflow by hand (workflow_dispatch with the version): the recover job checks out the tag, publishes the packages that are not published yet (with provenance) and creates the missing GitHub Release. publish --skip-existing refuses to run from another commit than the tag, and refuses a package already published from another commit (its gitHead). Tested with a fake npm (failure after each workspace, recovery, wrong commit).

  • No release commit. A release creates the v<version> tag, publishes the packages and creates a GitHub Release whose notes are the changelog. Nothing is committed back to main: a release commit pushed with GITHUB_TOKEN would be rejected by the required status checks, and semantic-release recommends against it. The versions in the repository therefore stay at 0.0.0-development (internal ranges ^0.0.0-development). The root CHANGELOG.md points to the GitHub Releases and to the changelogs of the former packages. @semantic-release/git, @semantic-release/changelog and the GPG setup of the release job are removed.

  • Configs live in the repo. They are written in place instead of coming from the @binden/*-config packages:

    • TypeScript: tsconfig.base.json plus a one-line tsconfig.json per package; build type-checks the tests as well.
    • ESLint, commitlint, semantic-release, c8, TypeDoc.
    • ESLint rules are the ones from @binden/eslint-config@7.0.1 and @binden/eslint-config-ts@6.0.2. Entries that change nothing were dropped; effective rules were verified to be identical. Rule violations in code carried over from the old repositories are switched off per file, with a comment.
  • Tests.

    • mocha and ts-node are replaced by node:test with tsx. Every workspace runs scripts/test.mjs, which passes extra options to Node.js before the test files (npm test -w packages/body -- --test-name-pattern=JSON, npm test -- --test-reporter=dot).
    • @binden/logger had no tests (and was missing from the coverage report, since binden loads it from dist): node:test tests are added.
    • c8 includes every package source (all) and enforces 100% lines/branches/functions/statements, so an untested package or line fails CI.
    • Process safety tests run fixtures in a process of their own (a crash fails the test, not the runner), and a keep-alive suite sends every request of a test on one connection (a framing error shows as a hang or a wrong body).
    • Every new test was checked by reverting its fix (mutation), on Node 24 and 26 where they differ.
    • Test servers listen on ephemeral ports (listen(0)), and the files of the BindenResponse tests live in a new temporary directory per test, so a local service on a fixed port or a second run cannot break the tests. packages/*/test/setup.ts disables HTTP keep-alive for the test clients (node:http agent, undici global dispatcher, also used by fetch): some tests depend on the client closing its connection after the response, and a pooled socket would outlive its test.
    • @binden/multipart only gets type annotations required by the current @types/node and @types/busboy (Buffer<ArrayBuffer>, filename as string).
  • Tooling works on Windows: prebuild removes dist with scripts/clean.mjs (no rm -rf), npm is run through scripts/npm.mjs (Node.js with npm_execpath, since execFile("npm") cannot run npm.cmd), the tooling tests use no shebangs, chmod, :-joined PATH or symbolic links (a junction), and .gitattributes checks out text files with LF.

  • CI: one workflow, ci.yml:

    • Actions pinned by commit (Dependabot updates them); the pre-commit hook runs the installed lint-staged only (npx --no).
    • lint on Node 24: lockstep check, Prettier, commitlint for the pushed range, ESLint, tooling tests (scripts/*.test.mjs: release script with a fake npm, npm launcher, test runner, pack check), the packed files of every package against scripts/pack-files.json, consumer check, a release rehearsal (semantic-release in dry-run mode with the real configuration in a clone with a local mirror, the next version and its notes, then release.mjs verify and version), TypeDoc (treatWarningsAsErrors).
    • Consumer check (scripts/consumer-check.mjs): the packed tarballs are installed into a fresh project with --install-strategy=nested (so an undeclared dependency fails, as it did for pino in binden), type checked and run as ESM against a real server. Every typescript block of the package READMEs (62 examples) is type checked (not run) against the same installation, with the same strict options. Three combinations: TypeScript 6 with @types/node 24 (skipLibCheck: false), TypeScript 6 with @types/node 26 (skipLibCheck: true because of an upstream thread-stream issue, see Known issues in the README), and TypeScript 7 with @types/node 24.
    • test on Node 24.14.1 (the oldest supported version), 24 and 26 with coverage; Coveralls upload from Node 24.
    • windows (new): npm ci, the tests and the tooling tests on windows-latest with Node 24.
    • release on main, after lint, test and windows: semantic-release (tag, npm, GitHub Release), without a release commit (see above).
    • recover, by hand only (see Recovery).
    • pages on main: deploys the TypeDoc site to GitHub Pages after the checks, without waiting for the release (a failed release no longer skips the docs).
  • Engines. The packages (and the tooling) require Node.js >=24.14.1 and npm >=11.11.0; the packages are compiled for ES2024.

  • .env files are ignored again; the install:clean script (which deleted the lockfile) is removed.

Dependencies

Updated to their latest versions: pino 10.4.0 (@binden/logger), @types/busboy 1.5.5 (peer of @binden/multipart), and for the tests fast-json-stringify 7.1.0, sinon 22.1.0 (with @types/sinon 22) and undici 8.11.2 (@binden/body); the lockfile is refreshed within the declared ranges. binden no longer imports pino at all (its serializers are built from an allowlist).

Deliberately not updated (explained in the README, Dependencies):

  • TypeScript 7: typescript-eslint (<6.1.0) and TypeDoc (≤6.0.x) need the JavaScript API that the native compiler does not have yet. The repository builds with TypeScript 6; the consumer check type checks the packages and the README examples with TypeScript 7.
  • @types/node 26: the types follow the oldest supported Node.js (24); the tests run on 24 and 26.
  • conventional-changelog-conventionalcommits 10: semantic-release 25 uses conventional-changelog-writer 8, and preset 10 requires writer 9 (a release dry-run fails with it).

npm audit --omit=dev reports no vulnerabilities. The 13 advisories of the dev dependencies are in the release tooling and have no fix yet: braces 3.0.3 (through micromatch of semantic-release) is the latest version, and the others are bundled with npm 11.21.0, the latest npm that @semantic-release/npm supports.

Library fixes

Inherited from the former repositories (or found by the reviews of this PR), each with a regression test checked by reverting the fix:

Responses (binden)

  • send() settles on what reaches the connection: a body (string, buffer or Readable) is written with a callback, so that a failed write rejects (Node.js 24 emits finish after a failed write once the response is ended); a Readable counts a chunk once it is flushed. It rejects when a Readable fails, closes before its end, or does not have its Content-Length: nothing sent yet, the error can still be replied; otherwise the response is cut, so that the client knows that it is truncated.
  • Every response is framed by its final status: a body gets its exact Content-Length (a stale one is replaced), 1xx/204/304 have none, 205 has Content-Length: 0, HEAD gets the length of the body it would send. Before, a stale Content-Length left clients waiting or desynchronized keep-alive connections (204, 205, the 416 of sendFile(), error replies).
  • A Readable that is not sent (to HEAD, 1xx/204/205/304, or an ended response) is destroyed with its errors ignored: a failing destroy() crashed the process, and an ended response kept the file open.
  • set() sets the own properties only (an inherited, or polluted, property was sent as a header).
  • sendFile() reads the metadata and the content from one file handle (no more than its size is sent; a FIFO is not waited for), and evaluates the preconditions in RFC 9110 order: If-Match/If-Unmodified-Since → 412, If-None-Match (weak comparison) → 304 or 412, If-Modified-Since → 304, then Range (GET only, satisfiable ranges merged, 416 otherwise) with If-Range (a strong ETag, or the Last-Modified when it is a strong validator).

Requests and routing (binden)

  • request.URL is assembled from the protocol of the connection, the authority (of an absolute-form target, otherwise Host) and the path of the request-target: //evil.test/x is a path, https://… targets no longer make secure true, and dot-segments, fragments, backslashes, invalid percent-encoding and userinfo get 400. request.secure is protocol === "https:".
  • trust_proxy (default false) decides whether Forwarded is trusted at all; the trusted element is taken by its position among the raw elements (an invalid element of a proxy no longer lets the one of the client take its place), with fail-closed parsing.
  • request.query keeps its prototype (__proto__ keys), cookies are validated (no attribute injection) and an empty cookie value is kept, and the header parsers follow RFC 9110: Authorization, Content-Type (every parameter kept and quoted), Content-Encoding (unknown codings → 415 in @binden/body), Range, Content-Range, Accept-Encoding (qvalues), If-Modified-Since (HTTP-date only); their constructors reject what the parsers reject.
  • Routing: an explicit HEAD handler is used, RegExp paths keep their flags (minus g/y), a guarded router that refuses the method passes the request on (405 with the union of Allow when nothing handles it), CONNECT routes are rejected (Node.js never routes them to the middlewares).

Errors and logging (binden)

  • A request gets a reply in these cases, which used to leave it pending: done without a reply (an empty response), the context of another request returned by a middleware (500), any thrown value (null, a symbol status, a throwing getter → 500), an async error_handler (awaited; nothing started → the default reply; a reply that is not ended once it settles is cut by closing the connection). A handler that never settles keeps its request: there is no deadline.
  • The default error reply goes through send(): with the queued cookies, the framing above, and without the representation headers of the failed body. Status codes are any integer from 100 to 599.
  • Logging cannot crash the process: a logger whose child() throws is used itself, logger calls are guarded, and the serializers never throw (a cyclic chain of prototypes is bounded). The default logs are an allowlist: a request is its method, its path (without query, fragment and userinfo; the path itself is logged), and a few headers; errors are their built-in type, status, expose and a Node.js error code.

Middlewares

  • @binden/compression: completes like an uncompressed response (end() callback, writableEnded, backpressure, errors), never compresses partial responses or responses without a body, negotiates by effective quality, and weakens a strong ETag of what it compresses.
  • @binden/body: a new limit option (decoded bytes, 413, reading stops; none by default, as decided, the README recommends one), decoding with the charset, 415 for an unknown coding or charset, and undefined from a custom parse (as Ajv parsers return) is a failure (415). A client disconnecting during the upload no longer leaves it pending.
  • @binden/multipart: a new limit option for the whole request (413, reading stops; none by default, the README recommends limits for the files, which are held in memory), a cut-off file part no longer crashes the process, throw_limits rejects truncated fields, and an aborted upload settles.

READMEs: the examples compile (type checked with TypeScript 6 and 7), the Router, the request-target and the contracts above are documented, and SECURITY.md asks for private reports.

Breaking changes (besides the move)

  • trust_proxy: request.protocol/secure/URL ignore Forwarded unless trust_proxy trusts the proxies (an application behind a TLS-terminating proxy needs new Binden({ trust_proxy: 1 })).
  • Request-target: targets with dot-segments, fragments, backslashes, invalid percent-encoding or userinfo get 400; secure and the protocol of URL no longer follow the scheme of an absolute-form target.
  • Cookies: new Cookie() throws a TypeError for an invalid name, value or attribute; Cookie.fromString() skips invalid cookies.
  • Headers: ContentEncoding.fromString()/request.content_encoding return null for an unknown coding; the constructors of Range, AcceptEncoding and ContentType throw a TypeError for invalid input, and ContentType#toString() includes every parameter.
  • Status codes: response.status() and new BindenError() throw a TypeError for a non-integer status (such as "400"); any integer from 100 to 599 is accepted.
  • Routing: router.on("CONNECT")/off("CONNECT") throw a TypeError.
  • sendFile() takes the path only (the Stats argument is removed).
  • @binden/body: a custom parse that returns undefined makes it reply 415.
  • Logs: the default serializers log an allowlist (see above).
  • Licenses: @binden/body, @binden/compression and @binden/multipart are now MIT (like binden and @binden/logger).

First release: 4.0.0

@binden/compression@3.0.0 is already on npm, so the first common version must be higher. The annotated tag v3.0.0 (already pushed) is a bootstrap marker on the binden 2.0.4 release commit (the same commit as v2.0.4). Counting from that tag, a semantic-release dry-run (now also run by CI, see release rehearsal) gives 4.0.0, with the breaking changes above in its notes. The release notes contain only the unreleased changes (ct_text → text/plain, MIT license for binden and logger, pino 10), the custom logger feature, the library fixes above and the migration.

The tag was moved once (from an earlier baseline commit) and will stay where it is now. A clone that fetched the old tag reports would clobber existing tag on git fetch --tags; fix it with git fetch --tags --force (or git tag -d v3.0.0 && git fetch --tags).

Old per-package changelogs stay in packages/*/CHANGELOG.md; the notes of the new releases are the GitHub Releases.

Before merging

  • Settings → Pages → Source: GitHub Actions (done: Pages no longer serves main:/docs, which this PR removes).
  • npm: set up Trusted Publishing (repository binden-js/binden, workflow ci.yml) for all 5 packages, or provide an NPM_TOKEN that can publish all of them.
  • Disable releases in the old repositories logger, body, compression and multipart, so they cannot publish out-of-band versions.
  • Branch protection of main: required status checks lint, test (24.14.1), test (24), test (26) and windows (none are required now), a required review, and enforce_admins. This is compatible with the release now that it does not commit to main.
  • Optional hardening: a release environment with required reviewers for the release/recover jobs; private vulnerability reporting (see SECURITY.md), secret scanning and push protection.
  • After merging: delete the secrets that the release job no longer uses: GPG_KEY, GPG_KEY_ID, GPG_KEY_PASSPHRASE, GIT_COMMITTER_EMAIL, GIT_COMMITTER_NAME.
  • Merge with a merge commit only, without squash or rebase, so the separate commits (and their release notes) are kept.

🤖 Generated with Claude Code

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread scripts/pack-rules.test.mjs Fixed
vansergen and others added 26 commits October 16, 2026 18:32
State that a send fails into a reply only before the headers are committed, that a source
destroyed before closes a committed response, that a close with errored rejects with that
error, that binden's own failures settle at once, that the listeners of a Readable are removed
by its own removeListener(), and that the request of an echo gets a guard once its send settles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in binden

A sendFile() or the send of a stream can settle once a failed pipe or a failed error handler has
recorded its cancellation (see Exchange.seal()) but before [kCancel] runs, e.g. when the file that
it sends fails in between. Its settlement deleted its pending entry, and with it what [kCancel]
would restore. So the reply that replaced it kept the status and the headers of the file (a 206,
its ETag and its Content-Type), and the representation chosen for the file was never forgotten,
since the send failed as its revoked work, which may no longer forget it.

The rollback of each send (the status and the headers from before a sendFile(), the framing and
the representation of a stream) is now kept apart from its settlement. A send that settles once
its cancellation is recorded keeps it, and [kCancel] runs every rollback, once and in the order of
the sends, before it cancels those still in progress and before the reply that replaces them. The
reason that they reject with and the release of the file or of the stream are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with in binden

A send of a stream-like source whose chunk is not bytes (e.g. an object of a readable-stream 3 or
4 Readable in object mode) fails with the ERR_INVALID_ARG_TYPE that its adapter refuses the chunk
with, and the adapter destroys the source with that error. The source then emits it, and its guard,
which listens once the listeners of the send are removed, logged it again at debug as a late error
of a released source, although that error is the one that has decided the send.

The adapter now records that error as decided before it destroys the source with it, as the send
of a Readable does for an error of its own, so that the guard reports only a later error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server that listens on port 0 without a host binds `::`, and macOS may give
it a port that a foreign process already listens on at 127.0.0.1 (e.g. the
gvproxy of podman): a client of localhost or 127.0.0.1 then reaches that
process, and a test fails with a foreign reply (an SSH banner was seen). A
listen on 127.0.0.1 never gets such a port.

Every test server, fixture and the consumer check now listens on 127.0.0.1,
and its clients connect to 127.0.0.1 (the helpers keep their signatures; the
binden helpers export the address as HOST). The examples keep their listen
without a host: their harness gives each one a free port of 127.0.0.1. The
only test that needs an IPv6 socket (an IPv4 peer as `::ffff:127.0.0.1`) keeps
its listen without a host, and says why. The allowed host of the options test
is the address that its client now sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line

A poller that `within()` waits for kept polling once `within()` had expired:
when its condition never came (e.g. a request that the Linux loopback never
pauses), the test failed, and the referenced timer of the poller kept the
process of its file, and so the run, alive until the CI job timed out.

The binden and compression helpers get `pollUntil(check, ms)`, a poller that
rejects by itself once `ms` (10 s by default) has passed, judged after a turn
of the pending I/O, whoever waits for it. Every poller without a deadline of
its own uses it: the tests of binden and compression, and the fixtures of
binden that their tests run in a child process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tests of the packages

After a stall of the process (a loaded machine, a GC), the timers run before
the I/O that has completed meanwhile is read. A read that ended after some
milliseconds of silence (the exchange of the committed failures, the idle
reads of the cut and strict tests, the window of the native pipes and of
the pipe failures fixture) then judged a connection silent, or open, with its
reply already waiting; and a 2 s `within()` reported a settled reply as
"Not settled".

These reads now wait for what they expect (the replies, or the close) and
judge silence only after a turn of the pending I/O (`ioTurn()`, and
`pollUntil()` once its deadline has passed). The waits that only guard
against a hang (`within()`, `waitFor()`, the client timeouts, the pollers)
get 10 s, which a loaded machine does not reach; the assertions of a timing
keep their own bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loopback in binden

The test of a request paused by the backpressure of its echo, whose reply
another writer ends, sent a body of 4 MiB and waited for the pause once 2 MiB
had been echoed. The loopback of Linux takes about 5 MiB of a client that does
not read (its receive buffer may grow to 14 MiB): the response never backed
up, the request was never paused, and the test failed on every Linux job of
GitHub CI (its poller then kept the job alive until its timeout).

Its body is now chunked, and its client writes it until the send has paused
the request (at most 256 MiB, each wait bounded), then ends it once the
reply has been ended. Verified on Linux (node:26.11.1 and node:24.14.1, 20
runs each) and macOS; it still fails when the request is left paused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es the connection in binden

The migration guide listed the cases where `send()` destroys a Node.js
Readable that binden 2.0.4 only piped, but not two in which a shared stream
is now lost to the other responses that 2.0.4 delivered it to whole: another
writer ends the response, and binden fails the send itself (the stream is
destroyed with that error, which the other sends reject with).

The sections of @binden/body and @binden/multipart now state that a body
refused before it is read in full closes the connection after the reply,
whoever replies (also an error handler that echoes the request), where 2.0.4
with @binden/body 2.0.1 and @binden/multipart 2.0.3 kept it and answered the
next request. Each claim was checked against 2.0.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o fixtures in binden

The retention fixture described its bound listeners as those of the
`context.bind()` of OpenTelemetry, which does not patch `once()`: current
OpenTelemetry patches it too. The fixture binds as a wrapper of the listener
methods may, and says so. The echo errors fixture is run by
`release.test.ts`, not by `process.test.ts`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ils in binden

A send whose write was not delivered, or whose response failed (the
rejection of its `done`), destroyed its source before it settled: a source
whose destroy emits at once still reached the listeners of the send, which
could then reject with that error instead. The send now settles first (it
rejects, and removes its listeners), then releases its source, as every
other settlement of a send does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the first resume() of a paused source was protected. A source that is
not paused (whose on("data") of Node.js resumes it inside the executor of
the send), an own on() or a newListener listener that throws, and a resume()
at a drain threw out of the send: an uncaught exception, or a send that
rejected while its `done` later rejected unhandled, its listeners and its
pending entry left (.started stayed true), and the source still sent. A
thrown null resolved the send, and a thrown primitive made decided() throw,
so that the source was never destroyed.

The send now observes its `done` before any code of the source runs. Every
own on() (each listener recorded first) and the first resume run in one try,
and the resume at a drain is protected, as the work that has started the
send. A throw fails the send as binden fails it itself, with what was thrown
as it is, the failure being a flag rather than an error that is not null.
decided() records objects only, and a guard that cannot be installed is
logged. The comment on once() no longer names OpenTelemetry, which patches
it now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inden

Once another writer had ended the response (the application, or a
compression replying 406), or the send itself with the whole body, a later
failure of the source destroyed the response: its complete reply was cut
(2 of 4 MiB delivered), and the pipelined responses after it were lost.

The send is now stopped then: its source is released, the response is left
as it is, and the send settles as the response does (it resolves at its
finish, and rejects on a premature close). The error of the source is logged
once at debug, as a late error. An end of the source short of the length
that the send has read no longer cuts such a reply either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inden

A Readable that has errored without being destroyed (as without
autoDestroy) never emits again: its send waited forever, or until the
deadline. It is now treated as one that has been destroyed: the send rejects
at once with its errored, and closes a response whose headers are committed.
A destroyed source that has an errored rejects with that error as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binden

A send that failed once its head was committed destroyed the response as its
work, so that a response still waiting for its socket (of a pipelined
request) was not marked to be cut: once it got its socket, Node.js 24.16 and
26 sent the committed head (Content-Length: 3), and the client read the next
response as its body. The failure of a send, and the send of a source
destroyed before, now destroy the response with the rights of the framework
(while their work may still mutate it), so that its socket is cut once it
comes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he framework in binden

The cancellation of the pending sends settled them inside kFramework, where
the removal of the listeners of a source runs its own removeListener() and
its removeListener listeners: code of the abandoned work, which could then
replace the 503 of the deadline, or the 500 of a failed error handler (a 299
of its own). The guard and the removals of a cancelled send now run from the
release list, outside kFramework, as the release of its source does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n binden

The guard of the request of a response (an echo) logged its late error, e.g.
its abort, as "A stream-like source has failed once released", although the
request is neither stream-like nor released. It is logged as "A released
stream has failed" now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in @binden/compression

The client gets the empty 406 before the send of its request has settled, so the next request
could run first and share its counter of reads: each request now counts the reads of its own
source, records its outcome by path, and the next request waits for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…latform in binden

Windows takes the whole 16 MiB write while its send buffer has room, so its response may have
finished before the source errors: the test no longer requires an unfinished response, which
macOS and Linux still have there, so a release at the finish still fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sage in binden

A late error of a released Node.js Readable was logged as "A stream-like source has failed once
released", which names a kind of stream that it is not, while the echoed request logged "A
released stream has failed" since round 19. Every released stream (a Node.js Readable, a
stream-like source, and the request of the response) now logs "A released stream has failed".

The message is kept in one internal constant, releasedFailure, exported with the other internals
of seal.js, so that the tests and fixtures of every package wait for it through the constant; one
test pins its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m in binden

Once every released stream logs the same neutral message, the separate report of the echoed
request (IOwnership.echoed, added in round 19 only for its message) does exactly what
IOwnership.released does. It is removed: the request of the response is reported by released,
and the response no longer chooses a report by its source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e tests of the packages

ioTurn() waited one setImmediate turn before a poller judged an expired deadline, or converse()
judged silence. Once a stall of the process has passed (a loaded machine), the timer runs first,
the end of a socket is read in the poll phase that follows, and its close is emitted only in the
close callbacks, after the check phase of that turn: a connection that the server had closed was
judged open (R3's converse-stall.mjs and poll-close-stall.mts).

ioTurn() now waits two turns, so that the close callbacks run in between. pollUntil() and the
converse() of cut.test.ts use it, and with them pipe-failures.ts, pipes.test.ts and
native-pipes.test.ts; @binden/compression gets the same ioTurn() for its pollUntil() and the
talk() of strict.test.ts, and the eventually() of @binden/multipart waits the same two turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e in binden

The framework makes its own replies through the methods of the response, also while it cancels a
send (the rollbacks of its headers) and in the 503 of a deadline: a method that a work has
replaced on the response instance (e.g. response.setHeader) runs there with the rights of the
framework, as R1's s3-cancel-rights.mjs and s4-instance-patch-reply.mjs show (299 HIJACK).

Routing every operation of the framework through the prototype would not remove the trust in the
class of the response, so the boundary is documented instead (approved by Codex): the Lifecycle
section says that such a patch is part of the framework's own replies, and that a work that may be
abandoned must not patch the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nden

MIGRATION-4.md did not tell two changes of send() against binden 2.0.4 (R3's mig-r19.mjs and
ended.mjs, run against 2.0.4 and this branch):
- a Readable that has been destroyed, or that has errored without being destroyed (errored, e.g.
  without autoDestroy), rejects at once with its errored, where 2.0.4 never settled;
- a source that fails once the response has ended no longer rejects: the reply and the responses
  pipelined after it are sent, and the error is logged at debug. 2.0.4 rejected when another
  writer had ended the response, and its default error handler then cut the reply. A send of the
  whole body had resolved at the end of its stream in 2.0.4, which ignored a later error.

The README told that an error which decides a send is not logged again: that holds for an
object only, which can be recorded as decided; a thrown primitive (e.g. a string) is logged once
at debug when the source emits it (R3's throws.mjs fresh string).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lose in the packages

When a test times out, node:test does not run the rest of its function: a client that awaits a
close or a data event without a deadline never reaches the finally that closes its server and its
socket, and the process of the file stays alive (R3's close-wait.test.mjs). The waits of
response.test.ts, pipes.test.ts and options.test.ts now go through within(), so that they reject
by themselves and their cleanup runs.

The setInterval of observers.test.ts, and the one of the streaming tests of
@binden/compression, were cleared only in a finally that runs once the send settles: a send
that never settles kept the interval, and the process, alive. They are cleared once the
response closes as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed times in binden

Three tests failed on a loaded machine (R3.19-F5):
- the queued-replies fixture destroyed its clients after a fixed 120 ms, whether or not their
  /small had come, so that fewer cleanups ran (R3's queued-replies-count.mts: 6 to 99 of 100
  under CPU hogs on Linux). The clients now go away once the deadline of every /small has passed
  (its signal is aborted), and the fixture waits up to 5 s for every cleanup;
- the two sendFile() 304 tests registered the listener of the second request once the first
  send had settled, while the client sends the second request as soon as it has the first
  response (R3's sendfile-race.mjs HEAD: 79 of 100 would come before it). The listener is
  registered before the requests are sent;
- headers/linear.test.ts bounded the wall time of a parse (127 ms seen under load). It bounds the
  CPU time of the parsing thread now (process.threadCpuUsage()), with the same 100 ms: under 24
  CPU hogs a parse took up to 750 ms of wall time but at most 26 ms of thread CPU time, where
  process.cpuUsage() also counts the background threads of V8 (up to 49 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second sendFile() started once the first had set the headers of its file snapshotted them, and
the cancellation restored the snapshots in the order of their start: the second reinstated the
ETag, the type and the 206 of the first, which the reply that replaced both then sent. The
rollbacks now run latest first, so the response is as it was before the earliest cancelled send.
The framing of the bodies that are not sent (Content-Length, Transfer-Encoding, Content-Range)
is removed after them, also a framing that was there before the earliest send (e.g. set by the
application before a sendFile()), and the representation chosen for them is forgotten once, in
place of a removal and a forget for each stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vansergen and others added 8 commits October 23, 2026 03:59
…eps in binden

The restore of a value send whose head Node.js refuses (H30-2) also forgets the representation
chosen for its body, and leaves a response whose head is committed as it is, which no test showed.
A negotiator now counts its choice and its forgetting for a refused send, and a send whose write
throws once the head is committed (as Node.js corks the socket) rejects with that error and keeps
the headers that have been sent.

The restore no longer checks that the work may mutate the response: a send of a work that may not
is ignored before anything else, and nothing revokes the work within the synchronous commit, so
that the check could never fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est of compression

The test of a compressed body cut at its deadline failed under load (R30 R3 P2-6a): exchange()
added its listeners of the connection only after the pause of the client, and the client read
after a timer of 400 ms that raced the deadline of 100 ms. On a loaded machine the deadline could
pass before the compressor had given its first chunk: the server closed the connection without
sending anything, the paused client ended and closed during the pause, and the listener added
afterwards never heard it (a hang of 10 s, then a failure).

exchange() now adds its listeners before it pauses (an explicitly paused socket stays paused), and
may pause until a promise settles. The test resumes the client once the server's signal has been
aborted by the deadline, whenever a loaded machine runs it, and accepts both valid cuts: nothing
sent at all, or a 200 head with a body that is not complete.

Under 16 CPU hogs on a machine already loaded (load average 35 to 47 on 8 cores), 24 runs of the
test in rounds of 8 in parallel all passed; an instrumented copy of the new order got nothing in 5
of 32 runs and an incomplete body in the others, never a hang, where the order of the old helper
hung in 2 of 24 runs under the same load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s misses in binden

The JSDoc of the scan named a parenthesis that an earlier element of a sequence does not close and
any parentheses in parentheses as its limits, but it misses a parenthesis in a string either way
(`")"` as well), finds one level of parentheses (`[(1)]`, `f(1)`), and also misses a nested
sequence, a `satisfies` and a type assertion of the last element (R30-R3 P2-7). It names them now:
any syntax of TypeScript around the last element, a parenthesis in a string, a nested sequence,
and parentheses nested in parentheses of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n binden and logger

MIGRATION said that a result of a middleware derived from its context, or a proxy of it, failed
the request in binden 2.0.4, but a proxy whose traps forward to the context (as @binden/compression
3.0.0 returned) replaced the context there, so that the next middlewares got it, and binden 4
ignores it silently (R30-R3 P2-3). It says so now, and how to migrate. With its pino 9,
@binden/logger 2.0.3 also used a `logMethod` set on the hooks once the logger was made for every
child made afterwards, e.g. each `context.log` of binden 2.0.4, which neither MIGRATION nor the
footer of 21727d9 said (R30-C2, P2-4).

The example of a sendFile() of 2.0.4 that left the headers of its file to the reply that followed
is now a file that stat() reads but that cannot be opened (of mode 000): a head that Node.js
refused never settled there (P2-8). The README says how Node.js frames the reply that follows a
sendFile() that has restored the response: with chunks, or by closing the connection when the
application had set a Transfer-Encoding before.

BREAKING CHANGE: A middleware whose result is a proxy of its context whose traps forward to the
context (e.g. what @binden/compression 3.0.0 returned) no longer replaces the context: binden
2.0.4 gave that proxy to the next middlewares, binden ignores it as any value that is not a context
(told by its private brand). Change the context or its response itself, or return a Context of the
same request and response. In @binden/logger, a `logMethod` set on the hooks given to new Logger()
once it is made is no longer used for the children made afterwards (e.g. each `context.log`), as
it was with the pino 9 of 2.0.3: set the hooks before the logger is created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of the kept values in binden

The reader of `Context#setTimeout()` waits until the deadline that it sets (1 ms) has passed, which
on a loaded machine may take longer than the 100 ms that a reader of a call without I/O has: it
has the bound of an I/O now, so that a slow deadline is never a finding. The header of the
fixture also states the rule of the values that a call keeps where it examines a hostile value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… multipart and logger

f89a691 gets the logger of the context inside the logging guard of the parsers: what a `log`
getter throws (e.g. of a subclass of Context) drops the records of the parse, and the parse goes
on (C30-1). The READMEs of @binden/body and @binden/multipart say so, and MIGRATION adds what a
logger of the context that throws did to 2.0.1 and 2.0.3, checked against both: @binden/body
rejected with an error of its methods, of its child() or of its getter, @binden/multipart with one
of its getter, and of its methods for a request that it skipped. The README of binden says that a
logger of binden is never nested over another (8502e51), and the README of @binden/logger that
a target, a step and a transport given as functions are copied too, with the properties by which
pino refuses a stream or a formatter of the level (2ce54fc).

BREAKING CHANGE: in @binden/body and @binden/multipart, a `log` getter of the context that throws
(e.g. of a subclass of Context) no longer rejects run(): the records of the parse are dropped,
where 2.0.1 and 2.0.3 rejected with its error. In @binden/logger, a target, a step of a pipeline or
a transport given as a function has its properties read once, as those of an object are, where
2.0.3 gave it to pino, which read the `target` of a target 3 times, of a step 2 times, and the
`targets` of a transport 3 times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd 31 in binden

36115d9 stores the integer that Node.js commits on every implicit commit of the status of the
response (also of an end() without a chunk, of flushHeaders() and of a write() to HEAD), and
refuses a status out of range before the encoding of a chunk (C30-3, H30-1, H30-3); 80a2d5e makes
a value send whose head Node.js refuses leave the response as it was before it (H30-2). The README
says both, checked against plain node:http and through @binden/compression: what `statusCode`
reads back, the paths of the implicit commits, and how the reply that follows a refused value send
is framed (by its own length, chunked once a Content-Length set before is removed, or by the close
of the connection once a Transfer-Encoding set before is). Its note on the reply that follows an
unwind of sendFile() now covers a failed send of a stream too.

The status of round 31 is not observable against binden 2.0.4, which did as plain node:http, but
the refused value send is: 2.0.4 kept the Content-Type that send() had set (checked on its dist),
which MIGRATION says now.

BREAKING CHANGE: a value send (send(), text(), html(), json(), form()) whose head Node.js refuses
(e.g. a status out of range or an invalid statusMessage) leaves the response as it was before it,
without the Content-Type that it set: binden 2.0.4 kept that Content-Type (`plain/text`) for the
reply that followed. Set the type of that reply yourself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The findings of the fixture of the public surface that this round had to fix are fixed (the
`zstd.params` of @binden/compression, now copied as `brotli.params`): the test asserts again that
the fixture finds nothing, without the list of the findings being fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he path, in binden

The test of a value send whose head is refused called a function of an object found by a name
taken from the path of the request, which CodeQL reports as an unvalidated dynamic method call.
The refusals of the test are now looked up in a Map, and a name that is not one of them fails the
test, so that no member of an object (e.g. of its prototype) is ever called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vansergen and others added 19 commits October 23, 2026 14:23
…them again, in binden

The Map of 833f475 kept a function that a name from the path chose, which CodeQL still reports as
an unvalidated dynamic method call, and the loop over the refusals read Object.keys() of that Map,
which has none: the test probed no refusal at all. A switch on the name now makes each refusal (no
function is looked up by a name from the request), and the test probes the three refusals of an
explicit list again (a refusal that does nothing fails it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in binden

The fixture of the public surface told what a call keeps only of the functions, plain objects and
arrays given to it, so that about forty positions whose value is an instance, a `Cookie`, a
`Headers`, a `Map`, a view or a `URL` were never checked (R31-R3 P2-4): `cookies.add()` keeps a
revoked proxy or a derived `Cookie` until the head is committed, and the default error reply of a
commit that fails on it then leaves the client with an empty, closed connection.

What a call keeps is told now of every value that the application owns: a fresh value of the
fixture, whatever its class, or a stand-in of one that every valid call shares (a copy of a value
of the language or of Node.js too). A value that the environment of the call holds (one that
survives a valid call that is never made, e.g. a context of an exchange, which its server keeps),
or that is shared without a stand-in, is not told: each such position where a hostile value is
accepted is named by its entry point and position (`held`), with why, and the reader or the
alternative check that uses it instead. What the call returns itself (the caller's), and the
valid value that `late` gives the call, are not kept by it. A shared holder's stand-in is copied
from the one that has the stand-ins of its nested positions (R31-R3 P2-5).

The cookie that `cookies.add()` keeps is bound to the commit of the head by binden's default error
reply, which a failing middleware of the exchange gets: its client must get the 500, with the
valid cookie given, and the `toString()` of a cookie is read once, by the commit, not by the call.
That reader, and the positions that the next commits bind, are listed in `integrating` until their
fixes and bindings are integrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pino refuses a stream given as `transport` by three checks in turn: whether it is a SonicBoom (by
its prototype chain), then a truthy `writable`, then a truthy `_writableState`. It reads `targets`
and spreads the own properties only afterwards. copyTransport() spread the transport first and read
`writable`, `_writableState` and `targets` unconditionally, so a getter that pino never evaluates
ran, and its error replaced pino's refusal (C31-1).

The copy now makes the same checks in the same order, before the spread, each read once and reused
by the spread, and it reads nothing more of a refused transport. The SonicBoom class of pino is
resolved from pino itself, once. Where it cannot be (e.g. in a bundle), the copy keeps the
prototype, by which pino tells one itself, as before. An accepted copy has no prototype otherwise,
through which pino would walk the prototype chain of the transport a second time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gger

pino refuses a `transport` given with a destination that is a path ("only one of option.transport
or stream can be specified") before it reads anything of the transport. The copy of the options
still read it whole first, so a getter that pino never evaluates ran, and its error replaced the
refusal, as with the refusal of a stream (C31-1). The copy now leaves such a transport as it is
for pino to refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in compression

Node.js ignores the chunk of a write() or an end() once the response is destroyed or has ended,
and binden alone does not check its encoding then (see #writes() of BindenResponse). The
compression checked it first: it converted and stored the status (e.g. 204.5 as 204, or an
object converted once), then threw for an unknown encoding, or for a status out of range
(ERR_HTTP_INVALID_STATUS_CODE for 1000). Such a chunk is now ignored as without a compression,
also once the application has ended the response through its compressor or its replacement: the
status is left as it is, and nothing throws (H31-1). A replaced response (406) that the
application has not ended still checks the encoding, as one that has not ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it is in compression

A response destroyed before the decision is left as it is, and once the application has ended it
so, Node.js and binden alone ignore the chunk of another end(), even one that write() would refuse
(e.g. end(1)): Node.js reads the end of the response first. The compression refused it with
ERR_INVALID_ARG_TYPE, as for a response that has not ended, which the README promises only for a
response replaced with 406 or ended through its compressor. Its end() now goes to binden as for a
response sent as it is, so that the destroyed and ended rows of an end() equal plain Node.js
(H31-1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in binden

A cookie added by the application whose serialisation throws as the head is committed (e.g. a
revoked proxy, an object derived from a Cookie, or a toString() that throws) fails that commit,
and the send rejects with its error. The default error reply then committed the same cookies,
failed again, and left the client with an empty, closed connection ("The error reply has not
been sent"), also for the empty 500 that replaces an error_handler that fails (R31-R3 P2-4).

Once binden replies an error, each cookie is now serialised once, in its own guard: one that
throws, or whose value Node.js refuses in a header (e.g. with a line break), is dropped from the
cookies of the response and logged ("A cookie has not been sent with the error reply", warn),
also one that has not failed before, and the error is replied with the others, framed by its
Content-Length, so that a pipelined request is answered. The send of the application is not
guarded and still rejects. The fixture of the public surface no longer integrates the findings
of cookies.add().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut output, in compression

The test of a compressed body that the deadline cuts resolved its client on any abort, and took an
empty reply for a cut before the first chunk: a regression that closes the connection before the
first chunk (R31-R2 H31-2), or that holds the compressed chunks back until the compressor ends
(R31-R3 P2-3), passed it. It now records why the request was aborted, and requires the deadline
in both branches (BindenAbortError#reason). The empty reply is accepted only when the compressor
had given no output by the deadline, counted at the output of the compressor itself, before
anything could hold its writes back; otherwise the partial body is required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ir readers in binden

The values that the rule of ownership now tells, and that the fixtures had not bound, are bound to
readers that use them, or named where the environment holds them.

Node.js keeps what the application gives it, and binden reads it later. The socket of a request
(`request.socket`) is bound to `ip`, and the request of a response (`response.req`) to
`writeContinue()` and `writeProcessing()`, which read its HTTP version. The error of `destroy()`
(`response.errored`) is bound too, and so is the logger of pino of `Logger.fromPino()`, to the
records it writes.

The server of `callback()` is now one that only the application holds, so that what the call keeps
of it is told: the listener keeps it, and never reads it as it handles a request (`unread`).

The positions whose value the server of an exchange or a handle holds are named in `held`:
- The serializers of a request, a response or a context give a record, which pino reads once the
  call has returned without running a trap of the value (`unread`).
- The parsers, `multipartInfo()`, `skipCompression()` and `shutdown()` never read their value once
  they have settled, also as the exchange replies (`settled`, a new check: no trap of `late-use`
  runs as the readers run, once the call has settled).
- The writers of a compression read their context as the response replies.

A reader finds the exchange of a hostile value by the valid value it stands for, and fails its work
so that binden replies by its default error reply. The stand-ins stay those of a function, a plain
object or an array: no fixture shares another kind of value.

The reads of the HTTP version of a request that fails as it is read throw an error that is not
formatted from `writeContinue()` and `writeProcessing()`; they stay in `integrating` with the
cookies of the error reply until their fix is integrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd writeProcessing() in binden

The informational writes read the HTTP version of the request of the response, which may be the
application's own (e.g. a proxy whose getters throw once the response is made). writeContinue()
and writeProcessing() read it outside of any formatting, so that its error escaped unformatted,
while writeEarlyHints() and writeInformation() format it. The version is now read inside the
formatting, so that each of them throws the error of the application as it is, formatted. The
fixture of the public surface no longer integrates those findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f new Binden() in binden

The reason that the fixture of the public surface gave for keeping the options of `new Binden()`
named what they hold, not what keeps them (R31-R3 N4). The constructor reads them once, but its
closure keeps the object itself: the classes of the requests and of the responses of the
application are made there and share its scope, and the options are collected with them, not
before. What a request uses later is what the options hold, its functions and its logger, which
the reader of a request still uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reated errors misses in binden

The JSDoc of the scan named a parenthesis in a string as one that it misses before the last
element of a sequence, but it misses one in a template or in a regular expression as well (R31-R3
P2-6): `(0, ")", TypeError)`, a template with `)` and `/\)/` before `TypeError` are all missed.
It says "a parenthesis in a literal" now, with an example of each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es in binden, body and logger

The README said that the reply that follows an unwind of sendFile() is chunked, or framed by the
close of the connection once a Transfer-Encoding set before is removed, and listed the framing of
the reply that follows a refused value send as if every reply had it. That holds only for a reply
of write() and end(), which sets no length of its own: send() and the default error reply set
their own Content-Length, so that a pipelined request is answered (R31-R3 P2-1). Both passages
say so now, checked over every pre-state and reply, also through @binden/compression.

MIGRATION said that a refused value send of 2.0.4 kept `plain/text`, which only send() and text()
had set: json() kept `application/json` (P2-2). It also left out that the send removes a
Content-Length or a Transfer-Encoding that the application had set before it, where 2.0.4 kept
them, checked on its published dist: a stale `Content-Length: 5` sent with the 3 bytes of the
reply that followed, or a reply kept chunked.

The README said that no logger of binden keeps the ones made before it, but its constructor
flattens only a logger of binden itself (R31-C1): a copy, a derived logger or a proxy of one is
wrapped as any logger of the application and keeps what it references, so that a child() that
gives a copy of the last `context.log` chains them, and a chain deeper than the stack drops its
records (the call never fails). The README says so now.

MIGRATION gave each `context.log` of binden 2.0.4 as the children that a `logMethod` set later
reached with @binden/logger 2.0.3, but binden 2.0.4 took no logger: its `context.log` was a child
of its own module logger (R31-C2). The example is a child that the application makes later now.
For a logger of the context whose methods throw, MIGRATION and the footer of 329a725 said that
@binden/body 2.0.1 rejected with its error, but 2.0.1 logged through a child of it, only where it
skipped a request, met an unsupported coding, or failed to parse a body, and the record of a
decoding failure came from a listener of zlib, so that its error was uncaught (R31-C3). Both are
checked against binden 2.0.4 with @binden/body 2.0.1.

BREAKING CHANGE: a value send (send(), text(), html(), json(), form()) whose head Node.js refuses
also removes a Content-Length or a Transfer-Encoding that the application had set before it, so
that the reply that follows frames its own body: send() and the default error reply by their own
Content-Length, a reply of write() and end() chunked, by its own length or by the close of the
connection. binden 2.0.4 kept them (a stale `Content-Length: 5` sent with 3 bytes, or a chunked
reply), and the Content-Type that it kept was the one of the send (e.g. `plain/text` of send()
and text(), `application/json` of json()): set the type and the framing of that reply yourself.
With @binden/logger 2.0.3, a `logMethod` set on the hooks once the logger was made was used for
each child that the application made afterwards (e.g. one per request). With @binden/body 2.0.1, a
logger of the context whose methods throw rejected run() with their error only where it logged (a
skipped request, an unsupported coding, a body that does not parse), and the error of a decoding
failure was uncaught, the request never answered: run() resolves or rejects with its own errors
now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d errors in binden

The scan took every `new (` for the construction of a class expression, also the constructor type
of TypeScript `abstract new (...args: never[]) => unknown`, which creates nothing, so that a fix of
@binden/logger had to write its type another way. A `new (` whose parameters are followed by `=>`
is a constructor type, and is skipped now. One whose parameters have parentheses of their own
(e.g. `new (callback: () => void) => unknown`) is still found, and named among the limits of the
scan, which errs towards a finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said that the copy of the options reads the `writable`, `_writableState`, `targets` and
prototype of a transport, by which pino refuses a stream, as if all of them were read. It now reads
them in the order of pino (85cec16, C31-1): whether it is a SonicBoom, then `writable`, then
`_writableState`, and nothing more of a transport that pino refuses, then `targets` and its own
properties. The README says so, and that a transport given with a destination that is a path is not
read (dc0525e), and that the copy keeps the prototype of the transport where the class of
SonicBoom cannot be resolved from pino (e.g. in a bundle). Checked against copyTransport().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hunks in binden and compression

0507f08 serialises each cookie of an error reply of binden once, in its own guard, and drops and
logs at `warn` one that fails; 45ad85b formats what reading the request throws through the
informational writes; cf297ba and 664268e make @binden/compression ignore a chunk of a destroyed
or ended response, as Node.js does. The READMEs say so: the default error handler and the empty 500
of a failed error handler, a cookie that fails the commit of the application, the HTTP version
that the informational writes read, and the responses whose chunks Node.js ignores (their status
neither converted nor stored). Checked against the code, plain node:http, R31-R2's gone31 and the
cookies of a request that fails, also pipelined.

MIGRATION says how the cookies of the default error reply are serialised. Against binden 2.0.4 it
does not say that a failing cookie left the client without a reply: 2.0.4 sent its error reply by
an end(), which added no cookie, so that it never read them and replied the 500 (checked on its
published dist). That the error reply now carries the cookies is in MIGRATION already, so there is
no change of 2.0.4 to add to a footer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The findings of the fixture of the public surface that this round had to fix are fixed: the
cookies that the error reply of binden drops (0507f08) and the HTTP version of a request that
the informational writes read inside the formatting (45ad85b). The test asserts again that the
fixture finds nothing, without the list of the findings being fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markdown-it 14.3.2, which typedoc uses to build the documentation, has a quadratic-time denial of
service in its parsing of inline HTML (GHSA-vfgh-jcmw-g37q), fixed in 14.3.3. The update is within
the range that typedoc requires and changes only the lockfile: no package of binden depends on it,
and the documentation builds as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The undici that the tests of body, compression and multipart use as their client (8.11.2), and the
ones that the release tools use (6.29.0 and 7.30.0), have published fixes of denials of service in
their latest patches (8.12.3, 6.29.1 and 7.30.1). The update is within the ranges that require them
and changes only the lockfile: no package of binden depends on undici.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants