Repository navigation
Conversation
vansergen
force-pushed
the
build/monorepo
branch
from
October 13, 2026 14:10
de790ec to
93e7fb4
Compare
|
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:
For more information about GitHub Code Scanning, check out the documentation. |
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>
…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>
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist:
npm run ci:alllocally and all tests are passing (Node.js 24.16 and 26.11).Description
Moves
binden,@binden/logger,@binden/body,@binden/compressionand@binden/multipartinto 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.
bindenis moved withgit mv, so its history and tags are kept. The sources oflogger,body,compressionandmultipartare 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.mjsis 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 confirmedE404from the registry counts as "not published" inverify(network or auth errors stop the release before anything is tagged), andpublishruns 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_dispatchwith the version): therecoverjob checks out the tag, publishes the packages that are not published yet (with provenance) and creates the missing GitHub Release.publish --skip-existingrefuses to run from another commit than the tag, and refuses a package already published from another commit (itsgitHead). 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 tomain: a release commit pushed withGITHUB_TOKENwould be rejected by the required status checks, and semantic-release recommends against it. The versions in the repository therefore stay at0.0.0-development(internal ranges^0.0.0-development). The rootCHANGELOG.mdpoints to the GitHub Releases and to the changelogs of the former packages.@semantic-release/git,@semantic-release/changelogand 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/*-configpackages:tsconfig.base.jsonplus a one-linetsconfig.jsonper package;buildtype-checks the tests as well.@binden/eslint-config@7.0.1and@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.
node:testwith tsx. Every workspace runsscripts/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/loggerhad no tests (and was missing from the coverage report, sincebindenloads it fromdist):node:testtests are added.all) and enforces 100% lines/branches/functions/statements, so an untested package or line fails CI.listen(0)), and the files of theBindenResponsetests 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.tsdisables HTTP keep-alive for the test clients (node:httpagent, undici global dispatcher, also used byfetch): some tests depend on the client closing its connection after the response, and a pooled socket would outlive its test.@binden/multipartonly gets type annotations required by the current@types/nodeand@types/busboy(Buffer<ArrayBuffer>,filename as string).Tooling works on Windows:
prebuildremovesdistwithscripts/clean.mjs(norm -rf), npm is run throughscripts/npm.mjs(Node.js withnpm_execpath, sinceexecFile("npm")cannot runnpm.cmd), the tooling tests use no shebangs,chmod,:-joinedPATHor symbolic links (a junction), and.gitattributeschecks out text files with LF.CI: one workflow,
ci.yml:lint-stagedonly (npx --no).linton 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 againstscripts/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, thenrelease.mjs verifyandversion), TypeDoc (treatWarningsAsErrors).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 forpinoin binden), type checked and run as ESM against a real server. Everytypescriptblock of the package READMEs (62 examples) is type checked (not run) against the same installation, with the samestrictoptions. Three combinations: TypeScript 6 with@types/node24 (skipLibCheck: false), TypeScript 6 with@types/node26 (skipLibCheck: truebecause of an upstreamthread-streamissue, see Known issues in the README), and TypeScript 7 with@types/node24.teston 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 onwindows-latestwith Node 24.releaseonmain, afterlint,testandwindows: semantic-release (tag, npm, GitHub Release), without a release commit (see above).recover, by hand only (see Recovery).pagesonmain: 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.1and npm>=11.11.0; the packages are compiled for ES2024..envfiles are ignored again; theinstall:cleanscript (which deleted the lockfile) is removed.Dependencies
Updated to their latest versions: pino 10.4.0 (
@binden/logger),@types/busboy1.5.5 (peer of@binden/multipart), and for the tests fast-json-stringify 7.1.0, sinon 22.1.0 (with@types/sinon22) and undici 8.11.2 (@binden/body); the lockfile is refreshed within the declared ranges. binden no longer importspinoat all (its serializers are built from an allowlist).Deliberately not updated (explained in the README, Dependencies):
<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/node26: the types follow the oldest supported Node.js (24); the tests run on 24 and 26.conventional-changelog-conventionalcommits10: semantic-release 25 usesconventional-changelog-writer8, and preset 10 requires writer 9 (a release dry-run fails with it).npm audit --omit=devreports no vulnerabilities. The 13 advisories of the dev dependencies are in the release tooling and have no fix yet:braces3.0.3 (throughmicromatchof semantic-release) is the latest version, and the others are bundled with npm 11.21.0, the latest npm that@semantic-release/npmsupports.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 orReadable) is written with a callback, so that a failed write rejects (Node.js 24 emitsfinishafter a failed write once the response is ended); aReadablecounts a chunk once it is flushed. It rejects when aReadablefails, closes before its end, or does not have itsContent-Length: nothing sent yet, the error can still be replied; otherwise the response is cut, so that the client knows that it is truncated.Content-Length(a stale one is replaced), 1xx/204/304 have none, 205 hasContent-Length: 0,HEADgets the length of the body it would send. Before, a staleContent-Lengthleft clients waiting or desynchronized keep-alive connections (204, 205, the 416 ofsendFile(), error replies).Readablethat is not sent (toHEAD, 1xx/204/205/304, or an ended response) is destroyed with its errors ignored: a failingdestroy()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, thenRange(GET only, satisfiable ranges merged, 416 otherwise) withIf-Range(a strongETag, or theLast-Modifiedwhen it is a strong validator).Requests and routing (
binden)request.URLis assembled from the protocol of the connection, the authority (of an absolute-form target, otherwiseHost) and the path of the request-target://evil.test/xis a path,https://…targets no longer makesecuretrue, and dot-segments, fragments, backslashes, invalid percent-encoding and userinfo get 400.request.secureisprotocol === "https:".trust_proxy(defaultfalse) decides whetherForwardedis 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.querykeeps 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.HEADhandler is used, RegExp paths keep their flags (minusg/y), a guarded router that refuses the method passes the request on (405 with the union ofAllowwhen nothing handles it),CONNECTroutes are rejected (Node.js never routes them to the middlewares).Errors and logging (
binden)donewithout a reply (an empty response), the context of another request returned by a middleware (500), any thrown value (null, a symbolstatus, a throwing getter → 500), an asyncerror_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.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.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,exposeand a Node.js errorcode.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 strongETagof what it compresses.@binden/body: a newlimitoption (decoded bytes, 413, reading stops; none by default, as decided, the README recommends one), decoding with thecharset, 415 for an unknown coding or charset, andundefinedfrom a customparse(as Ajv parsers return) is a failure (415). A client disconnecting during the upload no longer leaves it pending.@binden/multipart: a newlimitoption 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_limitsrejects 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.mdasks for private reports.Breaking changes (besides the move)
trust_proxy:request.protocol/secure/URLignoreForwardedunlesstrust_proxytrusts the proxies (an application behind a TLS-terminating proxy needsnew Binden({ trust_proxy: 1 })).secureand the protocol ofURLno longer follow the scheme of an absolute-form target.new Cookie()throws aTypeErrorfor an invalid name, value or attribute;Cookie.fromString()skips invalid cookies.ContentEncoding.fromString()/request.content_encodingreturnnullfor an unknown coding; the constructors ofRange,AcceptEncodingandContentTypethrow aTypeErrorfor invalid input, andContentType#toString()includes every parameter.response.status()andnew BindenError()throw aTypeErrorfor a non-integer status (such as"400"); any integer from 100 to 599 is accepted.router.on("CONNECT")/off("CONNECT")throw aTypeError.sendFile()takes the path only (theStatsargument is removed).@binden/body: a customparsethat returnsundefinedmakes it reply 415.@binden/body,@binden/compressionand@binden/multipartare now MIT (likebindenand@binden/logger).First release: 4.0.0
@binden/compression@3.0.0is already on npm, so the first common version must be higher. The annotated tagv3.0.0(already pushed) is a bootstrap marker on the binden 2.0.4 release commit (the same commit asv2.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 tagongit fetch --tags; fix it withgit fetch --tags --force(orgit 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
main:/docs, which this PR removes).binden-js/binden, workflowci.yml) for all 5 packages, or provide anNPM_TOKENthat can publish all of them.logger,body,compressionandmultipart, so they cannot publish out-of-band versions.main: required status checkslint,test (24.14.1),test (24),test (26)andwindows(none are required now), a required review, andenforce_admins. This is compatible with the release now that it does not commit tomain.releaseenvironment with required reviewers for therelease/recoverjobs; private vulnerability reporting (seeSECURITY.md), secret scanning and push protection.GPG_KEY,GPG_KEY_ID,GPG_KEY_PASSPHRASE,GIT_COMMITTER_EMAIL,GIT_COMMITTER_NAME.🤖 Generated with Claude Code