{"_id":"@chad3814/nntp","_rev":"6-3b4e1168c8f841bcc1b57898280c79e0","name":"@chad3814/nntp","dist-tags":{"latest":"2.0.0"},"versions":{"0.0.1":{"name":"@chad3814/nntp","version":"0.0.1","author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/nntp@0.0.1","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/nzb-utils#readme","bugs":{"url":"https://github.com/chad3814/nzb-utils/issues"},"dist":{"shasum":"e61336e77948c13935f91052df25c30cb10e18b2","tarball":"https://registry.npmjs.org/@chad3814/nntp/-/nntp-0.0.1.tgz","fileCount":2,"integrity":"sha512-acLSiojlakhbZx3RUMYpSGjl1HbSH520G1+xbUJHRzUuYB140ofHcSarBgeUD268Qi4LUBarL+63G3O8q/m4cg==","signatures":[{"sig":"MEQCICIHgYVWGs5I1hivxzccf+WfhFE5tESe0sD9JxY71hJrAiBKrx8SVS7xqDvlzV8DJTTaB6Km1P/SCZGqcoJBXIhJ7w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":736},"_npmUser":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"deprecated":"Placeholder reserving the name; contains no code. Use 1.0.0 or later.","repository":{"url":"git+https://github.com/chad3814/nzb-utils.git","type":"git","directory":"packages/nntp"},"_npmVersion":"10.9.7","description":"Placeholder reserving the @chad3814/nntp name. Not a release.","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nntp_0.0.1_1786246415860_0.3545727271297725","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@chad3814/nntp","version":"1.0.0","keywords":["nntp","usenet","news","rfc3977"],"author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/nntp@1.0.0","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/nzb-utils#readme","bugs":{"url":"https://github.com/chad3814/nzb-utils/issues"},"dist":{"shasum":"267f4597b39fb35a9e703187f3525b9ba5748455","tarball":"https://registry.npmjs.org/@chad3814/nntp/-/nntp-1.0.0.tgz","fileCount":49,"integrity":"sha512-WetDOveBoeG40GWCLQwH0sZH3fpGgJ6JLAXYCqbnu+QBSwzCEvn6XATp53GWhxe3QmIfULi8u576KDeEbrotGg==","signatures":[{"sig":"MEUCIA/v9RAA7pDSnNlyCAhXRWF951u0Cpxi4sJT3x1oh91pAiEA0GSdWXgkvI0QNsMZcPMLnrZ+8ZFuiyaAbNlzxsr+gU0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fnntp@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":190999},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"8ae48327c0338bdc401e27dca856a7ab069bcb60","scripts":{"build":"tsc -b","prepack":"node ../../scripts/assert-built.mjs"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"trustedPublisher":{"id":"github","oidcConfigId":"oidc:b32fd5b7-fcaf-494d-b28a-23be0b006d08"}},"repository":{"url":"git+https://github.com/chad3814/nzb-utils.git","type":"git","directory":"packages/nntp"},"_npmVersion":"12.0.2","description":"Strictly-typed NNTP client (RFC 3977) with TLS, AUTHINFO, and dot-unstuffing","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"@chad3814/secret-provider":"^1.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nntp_1.0.0_1786247634689_0.6967447627277188","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@chad3814/nntp","version":"1.1.0","keywords":["nntp","usenet","news","rfc3977"],"author":{"name":"Chad Walker"},"license":"MIT","_id":"@chad3814/nntp@1.1.0","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"homepage":"https://github.com/chad3814/nzb-utils#readme","bugs":{"url":"https://github.com/chad3814/nzb-utils/issues"},"dist":{"shasum":"17b88739f48833b79571138beb6238d91e20afa8","tarball":"https://registry.npmjs.org/@chad3814/nntp/-/nntp-1.1.0.tgz","fileCount":48,"integrity":"sha512-bFQ4z9UbeWkGRCwF0XFt+yQDDLhLBYGMvXItw0pXHVeHnrUGuKYVUQ5zoHpCdsxKEIRFMa05AltJC8sWE3eggw==","signatures":[{"sig":"MEYCIQCMm66seNTvy+ObMyU0FvcTetReLp7ynas0jAJJoUrUJwIhAPGrStEV6jSm8mHx22gxzdYXbuGlqCJ+Iw1ZbgOs62rQ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fnntp@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":133592},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"daa0e12d8f773ffd28ef6cdf33ce7fa1f8d2826b","scripts":{"build":"tsc -b","prepack":"node ../../scripts/assert-built.mjs"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"},"trustedPublisher":{"id":"github","oidcConfigId":"oidc:b32fd5b7-fcaf-494d-b28a-23be0b006d08"}},"repository":{"url":"git+https://github.com/chad3814/nzb-utils.git","type":"git","directory":"packages/nntp"},"_npmVersion":"12.0.2","description":"Strictly-typed NNTP client (RFC 3977) with TLS, AUTHINFO, and dot-unstuffing","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"@chad3814/secret-provider":"^1.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nntp_1.1.0_1786370565368_0.14543081514323664","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"_id":"@chad3814/nntp@2.0.0","bugs":{"url":"https://github.com/chad3814/nzb-utils/issues"},"dist":{"shasum":"3e49b568969c0feb39fdc62ad41ad33ab068486c","tarball":"https://registry.npmjs.org/@chad3814/nntp/-/nntp-2.0.0.tgz","integrity":"sha512-WLXghMdpyJOSP68KaTrbpXm9CWVNB8wTgJ43HLqQDjjb2UhVmth0derqgxri09bKwQdORjY52+A8iDxWr4/J5w==","fileCount":63,"unpackedSize":197334,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@chad3814%2fnntp@2.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCLZdjvixWTv6Kxan6kxNMqn4DNlLeY9PUoVdOG9drTyAIhAK9PlOlESp3bpjzYyaN78C2zcCv0iSWSa2ygOCCHCONi"}]},"main":"./dist/index.js","name":"@chad3814/nntp","type":"module","types":"./dist/index.d.ts","author":{"name":"Chad Walker"},"engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"209ac209913dbcbc80926f8380e183258a7fc3ed","license":"MIT","scripts":{"build":"tsc -b","prepack":"node ../../scripts/assert-built.mjs"},"version":"2.0.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b32fd5b7-fcaf-494d-b28a-23be0b006d08"},"approver":{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}},"homepage":"https://github.com/chad3814/nzb-utils#readme","keywords":["nntp","usenet","news","rfc3977"],"repository":{"url":"git+https://github.com/chad3814/nzb-utils.git","type":"git","directory":"packages/nntp"},"_npmVersion":"12.0.2","description":"Strictly-typed NNTP client (RFC 3977) with TLS, AUTHINFO, and dot-unstuffing","directories":{},"maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@chad3814/secret-provider":"^1.2.0"},"publishConfig":{"access":"public"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nntp_2.0.0_1786486058987_0.42820259994482335"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-09T03:33:35.738Z","modified":"2026-08-11T22:07:39.473Z","0.0.1":"2026-08-09T03:33:35.988Z","1.0.0":"2026-08-09T03:53:54.767Z","1.1.0":"2026-08-10T14:02:45.497Z","2.0.0":"2026-08-11T22:07:39.110Z"},"bugs":{"url":"https://github.com/chad3814/nzb-utils/issues"},"author":{"name":"Chad Walker"},"license":"MIT","homepage":"https://github.com/chad3814/nzb-utils#readme","keywords":["nntp","usenet","news","rfc3977"],"repository":{"url":"git+https://github.com/chad3814/nzb-utils.git","type":"git","directory":"packages/nntp"},"description":"Strictly-typed NNTP client (RFC 3977) with TLS, AUTHINFO, and dot-unstuffing","maintainers":[{"name":"chad3814","email":"chad@chad-cat-lore-eddie.com"}],"readme":"# @chad3814/nntp\n\nStrictly-typed NNTP client (RFC 3977, `AUTHINFO` from RFC 4643) with TLS,\nconnection pooling, and dot-unstuffing.\n\n**Status: 2.0.0.** One runtime dependency,\n[`@chad3814/secret-provider`](https://github.com/chad3814/secret-provider).\n\n```ts\nimport { NntpPool } from '@chad3814/nntp';\nimport { chain, fromEnv, fromFile } from '@chad3814/secret-provider';\n\nconst pool = new NntpPool({\n  endpoint: { host: 'news.example.com', port: 563, security: 'implicit' },\n  credentials: {\n    user: fromEnv('NNTP_USERNAME'),\n    pass: chain(fromEnv('NNTP_PASSWORD'), fromFile('/run/secret/nntp_password')),\n  },\n  credentialTtlMs: 15 * 60_000, // if the source issues short-lived credentials\n  connections: 8,\n});\n\nconst { body } = await pool.body('abc123@news.example.com'); // no angle brackets\n```\n\n## Credentials\n\nThis is the only package in the repo that accepts a credential. `NntpCredentials`\nis taken by `NntpClient.authenticate()` and `NntpPool`'s constructor, used to\nbuild one command, and never assigned to a readable field, logged, serialized, or\nincluded in an error. `@chad3814/nzb` takes an injected `ArticleSource` instead\nand has no credential-shaped parameter at all.\n\nThere is one deliberate qualification to that, about what the pool holds in\nmemory rather than what it exposes; it is spelled out below.\n\n### Literals or providers\n\nEach field is an `NntpSecret` — a literal, or a `Provider<string>` from\n[`@chad3814/secret-provider`](https://github.com/chad3814/secret-provider):\n\n```ts\ntype NntpSecret = string | Provider<string>;\n```\n\nSo `chain`, `fromEnv`, `fromFile`, `fromStatic` and anything else of that shape\ngo straight in. Providers are the better form for a secret: nothing is fetched by\nconstructing a pool, the value can come straight from a vault or a subprocess\nwithout passing through a config file, and a rotated credential is picked up\nwithout rebuilding anything.\n\nThis follows the pattern that library's README lays out for consumers, including\nthe parts that are easy to get wrong:\n\n- **Normalised and memoized once, here.** A literal becomes `fromStatic`, and\n  both fields are memoized at the pool's boundary. A pool of eight connections\n  makes **one** trip to the underlying source, not eight, and a caller who never\n  thought about memoization does not get a subprocess spawn per connection.\n- **Resolved at use, never in the constructor.** Construction stays synchronous\n  and no credential is fetched until a connection is actually opened.\n- **Expiry is supported.** `credentialTtlMs` re-resolves a credential once it is\n  that old, for sources that issue them with a lifetime. The clock starts when\n  the value _arrives_, so a vault taking three seconds to answer does not burn\n  three seconds of a five-second lifetime. Without it, the value is cached for\n  the life of the pool.\n- **The password is resolved only if the server asks for one.** Some servers\n  answer `281` to `AUTHINFO USER` alone, and there is no reason to fetch a secret\n  that will not be sent.\n- **`ProviderError` propagates untouched.** Wrapping it would destroy\n  `tryNextLink` and the aggregated list of every source a chain tried — which is\n  the part that makes a misconfiguration diagnosable, and, since that list names\n  the variables and paths, what identifies which credential failed. Only this\n  package's own validation raises `NntpCredentialError`.\n\n**What that costs, stated plainly:** a memoized provider holds the resolved\ncredential in its closure, so the pool does retain the secret in memory — until\n`credentialTtlMs` elapses, or for the pool's lifetime if it is not set. That is a\ndeliberate trade against a vault round-trip per connection, not an oversight. If\nyou would rather nothing were retained, pass credentials to\n`NntpClient.authenticate()` directly and manage connections yourself; that path\nresolves per call and caches nothing.\n\n### Line breaks are rejected\n\n`AUTHINFO PASS ${secret}` is built by interpolation, so a credential containing\nCR or LF would terminate the line early and append whatever follows as a second\nNNTP command. Both literals and resolved values are rejected if they contain one.\nThis matters more with providers than it did without: a value read from a file,\nan environment variable or a subprocess is exactly where a stray newline comes\nfrom.\n\n### What cannot be asserted\n\nErrors are built from the status code and the server's own text, never from the\ncommand line — otherwise `AUTHINFO PASS <secret>` lands in logs and stack traces.\n`redact()` covers the timeout path, where the label would otherwise be the raw\ncommand.\n\nOne honest limit, worth stating because it is easy to assume otherwise: a\n`#private` field is invisible to `JSON.stringify`, `Reflect.ownKeys` **and**\n`util.inspect({ showHidden: true })` alike, so \"the client does not retain the\npassword\" cannot be asserted at runtime. It is enforced against the source\ninstead, by a test that fails on any `this.x = credentials` assignment in the\npackage — and, since providers made it possible to stash the _resolved_ value\nunder another name, on any `this.x = resolveSecret(...)` too.\n\nThat rule covers `NntpClient`, which retains nothing. It does **not** claim the\nsame of `NntpPool`, which by design holds memoized providers whose closures\nretain the resolved value; see above.\n\n## What this does that the reference stack does not\n\n- **Dot-unstuffing happens here.** NNTP transmits a body line beginning with `.`\n  as `..`. yEnc decoders do not undo it — `@thaunknown/yencode` calls its\n  decoder with `stripDots = false` — so a transport that skips it corrupts the\n  article silently, with no checksum failure to point at it.\n\n  How often that fires depends entirely on the encoder. yEnc's spec _recommends_\n  escaping `.` at the start of a line, and an encoder that follows the\n  recommendation never produces a line NNTP would stuff. Measured against a real\n  post: **0 stuffed lines in 66,563**, across two 4 MiB articles. So this is a\n  correctness requirement, not a common event — earlier drafts of these docs\n  claimed \"roughly one article in a few hundred\", which that measurement does not\n  support for encoders that escape leading dots. It still has to be right,\n  because the encoders that skip the recommendation exist and nothing downstream\n  would catch them.\n\n- **Empty bodies do not hang.** A body that is only a terminator has no\n  preceding line, so the usual scan for `\\r\\n.\\r\\n` never matches and the read\n  blocks until the socket times out.\n- **A split terminator is found.** `\\r\\n.` and `\\r\\n` can arrive in separate TCP\n  segments. The scanner keeps a lookback window, because the client polls after\n  every `data` event rather than once at the end.\n- **Connections open on demand.** The reference pool opens all 24 in its\n  constructor: 24 TLS handshakes and 24 logins to fetch a 172 KB preview.\n- **Failures stay attributable.** The reference pool catches every\n  per-connection error bare and reports one generic \"failed to establish any\n  NNTP connections\", making a wrong password indistinguishable from a provider\n  connection cap. Here the originating error propagates, and `pool.failures`\n  keeps the per-attempt history.\n- **A failed connection is discarded, not re-enqueued.** The reference pool\n  returns connections in a `finally` with no health check and then hands the\n  same dead socket out repeatedly.\n- **A caller waiting for a connection can always be woken, or told why not.**\n  Parked callers are resolved _or rejected_, and parking happens only after\n  checking for a connection that has already gone idle. Both matter at\n  saturation: see below.\n- **Every command has a deadline.** The reference implementation has no timeouts\n  anywhere.\n- **Message-IDs are wrapped.** NZBs store them bare and the protocol requires\n  angle brackets; forgetting is a `430` on every article.\n- **Payloads stay `Buffer`.** Usenet is 8-bit clean. Only status lines become\n  strings, as `latin1`.\n\n## At a provider's connection cap\n\n`502 Too many connections` is not an authentication failure, and treating it as\none sends people to rotate a working password. It raises `NntpCapacityError`,\nand the pool responds by shrinking `limit` to what the account actually gives\nand running the work on the connections it has, rather than failing requests\nthat are perfectly fetchable.\n\nMeasured against a real 100-connection account, asking for 200 at once:\n\n```\n200 concurrent requests all settled in 11.9 s; 99 refused, limit shrank 200 -> 101\n```\n\nTwo things worth knowing from that run:\n\n- **The shrunk limit is approximate, and self-correcting.** It is set to the\n  number of connections open at the moment of a refusal, which counts opens\n  still in flight, so it can land a little above the true cap. The next refusal\n  brings it down again.\n- **Saturation is where pool liveness actually gets tested.** Every open starts\n  before any completes, so the connections that succeed finish their work and go\n  _idle_ while the refusals are still arriving. An earlier version parked the\n  refused callers without looking at the idle list, and nothing was left running\n  to wake them: 200 concurrent requests hung with no error and no work in\n  flight. Parking now checks for an idle connection first, and a parked caller\n  is failed outright when the pool provably cannot serve it — no live\n  connections, none openable, or destroyed. At 40 requests against a cap of 10\n  the interleaving does not occur, which is why it took a live account to find.\n\n`scripts/smoke.ts` reproduces this against a real provider under\n`NNTP_PROBE_CAP=1`. It is opt-in because it deliberately saturates the account.\n\n## More than one server\n\nAn article one provider has dropped is often still on another. `NntpMultiPool`\ntakes an ordered list and reaches a later server only when an earlier one cannot\nsupply the article — filling gaps, not aggregating bandwidth.\n\n```ts\nconst pool = new NntpMultiPool({\n  servers: [\n    { name: 'primary', endpoint, credentials, connections: 20 },\n    { name: 'block', endpoint: other, credentials: blockCreds, connections: 8 },\n  ],\n});\n\nconst { body, server } = await pool.body('abc123@news.example.com');\n```\n\nServers are tried **sequentially**, and the reason is money: a second provider\nis usually a metered block account, and asking everyone at once would spend its\nbytes on every article the primary already had. For the same reason, taking\noverflow from a server that is at its connection cap is opt-in per server via\n`spillover`, and off by default — a metered account should pay for gaps, not\nfor overflow the primary would have covered a moment later. `spillover` gates\nonly that path; a genuine gap (a `430`) still falls through to a non-spillover\nserver.\n\n| Outcome                                 | What happens                                                                                                                                                                                                                                           |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `430`                                   | A gap. Advance to the next server. Never counts against the server's health.                                                                                                                                                                           |\n| Timeout or connection loss              | Counted. Three in a row with no success between takes the server out of rotation for the life of the pool.                                                                                                                                             |\n| At the connection cap, nothing openable | Advance only to servers with `spillover: true`. When more than one server is saturated, the error surfaced is the **earliest** one's — the primary's cap is the actionable account, and a downstream server's cap is just where the walk gave up next. |\n| Auth refused on the primary             | Fatal, and sticky. Failing over would run a whole download on a backup because of a typo.                                                                                                                                                              |\n| Auth refused on any other server        | That server is marked down immediately, on the first strike — a wrong password will still be wrong next time.                                                                                                                                          |\n\nIf every server answers `430`, the article is gone and a `430`\n`NntpProtocolError` is thrown, so callers that skip-and-report (`nzb get`) keep\nworking. Any other mixture throws `NntpUnavailableError`, whose `attempts`\nnames each server and its reason.\n\n`statAll(messageId)` reports per server, with three states rather than two:\n`present`, `absent` (the server said 430) and `unknown` (it could not be\nasked). `absent` and `unknown` are different facts, and only unanimous `absent`\njustifies giving up on a file.\n\nA third-party credential provider's error can end up here too, by the same\nroute as `NntpUnavailableError`'s messages: `NntpServerStatus.downReason` and\neach failed attempt carry the provider's `error.message` unwrapped, per\n`resolveSecret`'s policy in `auth.ts` of letting a provider's rejection\npropagate rather than wrapping it. That is existing, deliberate behavior, not\nnew — providers already own their own error hygiene. It is just newly visible\nhere, across more than one server, and worth stating plainly for the\n`@chad3814/secret-provider-*` vault packages that are coming: a provider must\nnot put the secret in the error it throws.\n\n## Layout\n\n| Module                  | Role                                                    |\n| ----------------------- | ------------------------------------------------------- |\n| `response-buffer.ts`    | Wire framing: lines, dot-terminated blocks, unstuffing  |\n| `auth.ts`               | Resolving a credential and spending it on `AUTHINFO`    |\n| `client.ts`             | One connection: commands, responses, timeouts           |\n| `pool.ts`               | Lazy pool of authenticated connections                  |\n| `multi-pool.ts`         | An ordered list of pools, walked until one answers      |\n| `multi-pool-failure.ts` | Classifying one candidate's failure; the down threshold |\n| `multi-pool-models.ts`  | Per-server options, status, and the `statAll` verdict   |\n| `socket.ts`             | TCP / implicit TLS / `STARTTLS` upgrade                 |\n| `wire.ts`               | Message-ID wrapping and redaction                       |\n\n`NntpMultiPool` composes one `NntpPool` per server rather than teaching one pool\nabout several endpoints, because the learned connection cap, the credential and the\nup/down state are all per-server. It manages no sockets of its own. Failure\nclassification is split into `multi-pool-failure.ts` so that `rule(entry, error, walk,\nisPrimary)` decides what a failure means from nothing but those four arguments, never\n`this` — it mutates the entry and walk it is handed, but never touches a pool or a\ncredential — and returns a ruling for the class to apply, which is what keeps the\nauthority to fail an entire walk in one place.\n\n`ResponseBuffer` and `auth.ts` are both socket-free on purpose. Framing is where\nthe subtle bugs live, and authentication is where the sensitive ones are, so each\nis a function of its inputs and is tested without a network: `runAuthInfo` takes\na callback that sends a line and returns a parsed response, which is enough to\ntest which codes mean what, when the password is fetched, and what never reaches\nan error.\n\n## Testing\n\n131 unit tests. The client and pool run against a real TCP server (`test/fake-server.ts`)\nrather than a mocked socket — the bugs worth catching are framing bugs, and a\nmock that hands over whole responses cannot produce a split terminator. The\nfake server can deliver a reply as several writes specifically to force awkward\nchunk boundaries.\n\nMutation-tested: dropping angle brackets, skipping dot-unstuffing, removing the\nsplit-terminator lookback, reading a body after a `430`, leaking the password\ninto an auth error, stashing it in a private field, ignoring the connection cap,\nand never reusing a connection each fail at least one test. So do the credential\nmutations: removing the line-break check, checking only CR and not LF, accepting\nan empty or non-string value, resolving the password before the server asks for\nit, dropping the pool's memoization, ignoring `credentialTtlMs`, starting the\nexpiry clock before the resolution instead of after, and re-wrapping\n`ProviderError`.\n","readmeFilename":"README.md"}