@blamejs/blamejs-shop 0.1.37 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/admin.js +205 -42
  3. package/lib/asset-manifest.json +5 -5
  4. package/lib/storefront.js +428 -51
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/CHANGELOG.md +12 -0
  7. package/lib/vendor/blamejs/README.md +2 -2
  8. package/lib/vendor/blamejs/SECURITY.md +1 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  10. package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
  11. package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
  12. package/lib/vendor/blamejs/lib/archive-read.js +101 -35
  13. package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
  14. package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
  15. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  16. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  17. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  18. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  19. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  20. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  22. package/lib/vendor/blamejs/lib/backup/index.js +2 -3
  23. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  24. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  25. package/lib/vendor/blamejs/lib/cose.js +4 -3
  26. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  27. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  28. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  29. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  30. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  31. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  32. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  33. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  35. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  36. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  38. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  39. package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
  40. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  41. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  42. package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
  43. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  44. package/lib/vendor/blamejs/lib/mail.js +2 -2
  45. package/lib/vendor/blamejs/lib/mdoc.js +14 -14
  46. package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
  47. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  48. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  49. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  50. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  51. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  52. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  53. package/lib/vendor/blamejs/package.json +1 -1
  54. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  55. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  56. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  57. package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
  58. package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
  59. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  62. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
  64. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  66. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  67. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  68. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
  69. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  70. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  71. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  72. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  73. package/package.json +1 -1
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.13.6",
7
- "tag": "v0.13.6",
6
+ "version": "0.13.12",
7
+ "tag": "v0.13.12",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -8,6 +8,18 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.12 (2026-05-27) — **Inbound MX listener now runs the connection-level gate cascade it documented — HELO identity, DNS blocklist, and greylisting.** b.mail.server.mx.create documented helo / rbl / greylist gate options, but the listener never invoked them — an operator who wired them got silent acceptance of mail those gates would have rejected. They are now wired into the live SMTP state machine: the HELO-identity gate evaluates at HELO/EHLO and refuses a spoofed or malformed identity with 550; the DNS-blocklist gate evaluates the connecting IP once per connection and refuses a listed source with 554; the greylisting gate defers a first-seen (ip, sender, recipient) tuple with a 450 tempfail so legitimate senders retry and pass. Each gate is skipped when the operator doesn't wire it. Because these gates do DNS and store lookups, the per-connection command pump was reworked to process commands asynchronously and strictly in arrival order, so pipelined commands (RFC 2920) cannot overtake a gate still resolving and the existing SMTP-smuggling and STARTTLS-stripping defenses are unchanged. The message-authentication gate (SPF/DKIM/DMARC alignment via b.guardEnvelope) needs the inbound SPF + DKIM verification results as inputs; that inbound-auth pipeline lands as a follow-up, and the documentation no longer implies that gate is active today. **Added:** *HELO-identity / RBL / greylist gates wired into `b.mail.server.mx`* — When wired, `opts.helo` (FCrDNS / HELO-shape / self-name checks) refuses a bad HELO identity at HELO/EHLO with 550; `opts.rbl` refuses a connecting IP found on a DNS blocklist with 554 (evaluated once per connection); `opts.greylist` defers a first-seen (ip, sender, recipient) tuple with 450 4.7.1. Their verdicts surface on the `rcpt_to` event (`rblListed`, `greylist`) and the `helo` event (`heloVerdict`), with dedicated `helo_gate_refused` / `rbl_refused` / `greylist_deferred` audit events. A gate the operator doesn't supply is skipped, never synthesized. **Changed:** *MX command pump processes commands asynchronously and in arrival order* — Gate evaluation involves DNS and store lookups, so the per-connection command pump now awaits each command before the next. Pipelined commands are serialized so a gate resolving cannot let a later command answer ahead of an earlier one; reply ordering, the bare-LF SMTP-smuggling refusal, and the STARTTLS-stripping defense are unchanged. No change to the listener's external behaviour when no gates are wired. **Deprecated:** *SPF/DKIM/DMARC-alignment gate documentation corrected to match what is active* — The `envelope` (SPF/DKIM/DMARC alignment) and `dmarc` gate options were documented as wireable but require inbound SPF + DKIM verification results the listener does not yet produce. They are removed from the documented option set until the inbound-authentication pipeline (composing `b.mail.spf` + `b.mail.dmarc` + DKIM verification) lands; run those checks on the delivered message via the agent handoff in the meantime.
12
+
13
+ - v0.13.11 (2026-05-27) — **Test-suite reliability: replaced fixed-delay waits in the rate-limiter and scheduler suites with condition polling.** No runtime behaviour changes. The rate-limiter, scheduler, and websocket-channel test suites waited for asynchronous work to settle by draining a fixed number of event-loop ticks before asserting. Under heavily parallel CI that budget was occasionally too short, so an assertion read state before the async work (a cluster-backend counter update, a scheduler tick-claim) had landed — an intermittent failure unrelated to the code under test. Those waits now poll the observable condition (helpers.waitUntil) and exit as soon as it holds, with a generous upper bound, so they pass quickly on fast machines and reliably under load. A build gate is added so the fixed-tick-drain shape cannot be reintroduced. **Fixed:** *Flaky fixed-budget waits in the rate-limiter / scheduler / sandbox test suites made contention-tolerant* — The rate-limit-cluster and scheduler-exactly-once suites drained a fixed count of event-loop ticks before asserting on asynchronously-updated state; under contended CI the budget could expire before the work settled, producing intermittent failures. They now wait on the actual observable condition (a written response, a settled counter). The sandbox suite's success-path cases gave the worker a 5 s execution budget that cold worker-thread startup under heavily parallel Windows CI could just exceed; those are raised to the framework's 10 s ceiling. Affects test code only — no change to shipped framework behaviour. The unused tick-drain helper in the websocket-channel suite was removed. **Detectors:** *Build gate rejects the fixed-tick-drain wait shape in tests* — A new test-suite lint rule flags the counted microtask/tick-drain idiom (reassigning a promise to its own `.then()` in a loop to wait a fixed number of ticks), the sibling of the existing fixed-`setTimeout`-sleep rule. A single event-loop yield is unaffected; only the drain-as-wait shape is rejected, directing the wait to condition polling instead.
14
+
15
+ - v0.13.10 (2026-05-27) — **Documented-but-inert options wired up, a non-existent CVE reference removed, and a silent iCalendar cap-bypass fixed.** A sweep for places where a documented option or citation did not match what the code does. The most operator-relevant fix: b.calendar.fromIcal documented a safeIcalOpts option that forwards parser caps (byte size, RRULE limits, nesting depth) to b.safeIcal.parse, but the value was never forwarded — so an operator who set tight caps through it got the default profile instead, silently. That is corrected; the nested options now reach the parser. b.archive.read.zip documented an AbortSignal option that was never honored; it now aborts the read at the entry boundary. b.auth.fal documented a bearerOnly alias that had no effect; it now forces the no-proof-of-possession path and refuses the contradictory combination of bearerOnly:true with a holder-of-key binding. Separately, the auth verification paths cited CVE-2026-23993 (13 places) for the "reject an unknown alg before key lookup" guard — that CVE id does not exist (the registry has no record of it); the citation is replaced with the weakness class (CWE-347 / CWE-757) and the real, verifiable neighboring CVEs. The circuit-breaker error-code note that promised a rename "in v0.10" is corrected to the actual plan (v1.0), and the build gate that catches overdue version promises now also catches two-part version numbers. **Changed:** *`b.auth.fal` `bearerOnly` is now a real alias and refuses contradictions* — `bearerOnly: true` now forces the no-proof-of-possession path (equivalent to `hokBinding: null`), as documented. Passing `bearerOnly: true` together with a non-null `hokBinding` is a contradictory assurance request and is now refused at the call rather than silently resolved one way. **Fixed:** *`b.calendar.fromIcal` now forwards `safeIcalOpts` to the parser* — The documented `safeIcalOpts` option (parser caps: max bytes, RRULE COUNT/BYxxx limits, nesting depth) was not being passed to `b.safeIcal.parse` — when supplied under the documented nested key it was silently ignored and the parser ran with its default profile. Both forms now reach the parser: the documented nested `{ safeIcalOpts: { ... } }` and the top-level `{ profile, ... }` that earlier releases accepted, with the nested form winning on conflict. No caller regresses. · *`b.archive.read.zip` honors the documented `signal` (AbortSignal)* — The `signal` option was documented but never read. A large or slow archive read can now be aborted cooperatively — the reader checks the signal at each entry boundary (`inspect`, `entries`, `extractEntries`, `extract`) and rejects with an `archive-read/aborted` error. · *Removed a non-existent CVE reference from the JWT/JWE verification paths* — The "reject an unknown/unsupported `alg` before any key lookup" guard in `b.auth.jwt.verifyExternal`, `b.auth.oauth.verifyIdToken`, `b.auth.oid4vci`, and `b.auth.sd-jwt-vc` cited a CVE id that the registry has no record of. The behaviour is unchanged; the citation is now the weakness class it defends (CWE-347 improper signature verification / CWE-757 algorithm downgrade) alongside the real, verifiable alg-confusion / JWE-bypass CVEs already cited beside it. **Detectors:** *Overdue-version-promise gate now catches two-part version numbers* — The build gate that flags a deferral whose promised landing version has already shipped previously matched only three-part versions (`vN.N.N`); a two-part promise (`vN.N`) slipped past it. It now matches both. The `b.circuitBreaker` `CIRCUIT_OPEN` error-code note that pointed at a passed version is corrected to its actual plan (rename at v1.0, with a deprecation warning a minor ahead).
16
+
17
+ - v0.13.9 (2026-05-26) — **Corrected CVE citations in source threat annotations + a build gate that refuses malformed CVE identifiers.** Several source-comment threat annotations cited CVE identifiers that were rejected by the numbering authority (never assigned to a real issue), attributed to the wrong product, or structurally malformed (a placeholder with a non-numeric sequence). The annotated defenses are unchanged — every cap, refusal, and constant-time comparison behaves exactly as before; only the reference labels were corrected, each to a verifiable CVE or to the underlying weakness class (CWE / RFC) where no single CVE fits. Notable corrections: the S/MIME SHA-1 / MD5 certificate-signature refusal now cites the SHAttered collision and RFC 8551 §2.5 instead of a rejected candidate id; decompression-output caps cite CWE-409 and CVE-2025-0725 instead of a fabricated placeholder; the iCalendar RRULE / nesting / byte caps describe the calendar-bomb recursion-DoS class instead of an unrelated SSRF advisory; and the SAML signature-wrapping (XSW) defense now cites the actively-exploited CVE-2024-45409 (ruby-saml, CVSS 10.0) and CVE-2025-25291 / -25292 that the duplicate-element refusal defeats. A new build-time detector refuses any CVE token whose sequence number is not all-numeric, so a placeholder identifier can never reach a release again. **Fixed:** *Corrected rejected / misattributed / malformed CVE references in source threat annotations* — Threat-annotation comments across the mail, crypto, auth, guard, and safe modules carried CVE identifiers that were rejected by the CVE numbering authority, attributed to the wrong product, or written as non-numeric placeholders. Each was corrected to a verifiable CVE or to the weakness class (CWE / RFC) it defends. No runtime behaviour changed — the defenses these comments describe are unchanged. The S/MIME certificate check's SHA-1 / MD5 refusal message now names the SHAttered collision and RFC 8551 §2.5; the SAML XSW defense now names CVE-2024-45409 and CVE-2025-25291 / -25292. **Detectors:** *`malformed-cve-identifier` — refuses structurally-invalid CVE tokens at build time* — A CVE identifier's sequence number is always numeric (`CVE-<year>-<digits>`). The new detector refuses any CVE token whose post-year segment contains a letter — the placeholder shape that lets a fabricated reference slip past review. It cannot verify that a well-formed id is real or correctly attributed (that stays a review responsibility), but it makes the structurally-invalid class impossible to ship.
18
+
19
+ - v0.13.8 (2026-05-26) — **In-memory archive extraction for read-only / serverless filesystems.** Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead. **Added:** *`extractEntries()` — in-memory archive extraction (ZIP + tar)* — `b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes. **Fixed:** *Removed the inaccurate `b.archive.zip().toGzip()` doc claim* — The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged.
20
+
21
+ - v0.13.7 (2026-05-26) — **Documentation accuracy — several primitives described shipped features as deferred.** A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped. **Fixed:** *Corrected `deferred`/`does-not-ship` docs for features that have shipped* — `b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such. **Detectors:** *Overdue-defer detector in the codebase-pattern gate* — A new check fails the build when a comment promises a feature "lands in" / is "deferred to" / is "not supported in" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions.
22
+
11
23
  - v0.13.6 (2026-05-26) — **`b.ai.frontierModelProtocol` — California SB 53 frontier-AI obligations.** b.ai.frontierModelProtocol assesses a developer's obligations under California's Transparency in Frontier Artificial Intelligence Act — SB 53, Cal. Bus. & Prof. Code §22757.10, effective 2026-01-01 — from a model's training compute and the developer's revenue. It reports whether the model crosses the frontier threshold (more than 10^26 training FLOPs), whether the developer is a large frontier developer (prior-year revenue, with affiliates, above $500M), and the resulting obligations: every frontier developer must report critical safety incidents and publish a transparency report, and a large frontier developer must additionally publish an annual safety framework and disclose its catastrophic-risk assessment. Passing a candidate safety framework reports which required elements (risk identification, mitigation, governance, cybersecurity, standards alignment) are missing. b.ai.frontierModelProtocol.incidentReport validates a critical-incident type against the Act's four categories and computes the notification deadline to the California Office of Emergency Services — 15 days from discovery, or 24 hours when there is an imminent risk of death or serious physical injury. The ca-tfaia compliance posture was already in the catalog. **Added:** *`b.ai.frontierModelProtocol` — SB 53 threshold classification, obligations, and incident reporting* — `b.ai.frontierModelProtocol({ trainingFlops, annualRevenueUsd, framework? })` returns `isFrontierModel`, `isLargeFrontierDeveloper`, the `obligations` list, and (when a framework is supplied) its `frameworkGaps`. `b.ai.frontierModelProtocol.incidentReport({ type, discoveredAt, imminentRiskToLife? })` builds a critical-safety-incident report with the California OES recipient and a `dueAt` / `deadlineHours` computed from the 15-day (or 24-hour imminent-risk) statutory window; `type` must be one of the four categories in `INCIDENT_TYPES`. Throws `FrontierProtocolError` on malformed input.
12
24
 
13
25
  - v0.13.5 (2026-05-26) — **`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit.** b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog. **Added:** *`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios* — `b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `"selection"` (group entries `{ selected, total }`) or `"scoring"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input.
@@ -169,7 +169,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
169
169
  - **Mail (outbound)** — multipart + attachments + DKIM + calendar invites; bounce intake (`b.mail`, `b.mailBounce`)
170
170
  - **Mail (outbound delivery)** — turnkey MX-lookup → MTA-STS-fetch → DANE-TLSA → REQUIRETLS handshake → SMTP wire layer → RFC 3464 DSN-on-permanent-failure → deferred-retry scheduling, all wired once (`b.mail.send.deliver`)
171
171
  - **Mail (inbound auth)** — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`)
172
- - **Mail server listeners** — RFC 5321 MX inbound (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
172
+ - **Mail server listeners** — RFC 5321 MX inbound with connection-level gate cascade (HELO identity / DNS blocklist / greylisting) (`b.mail.server.mx`), RFC 6409 submission with SASL + identity-binding (`b.mail.server.submission`), RFC 9051 IMAP4rev2 with CONDSTORE / QRESYNC / NOTIFY / METADATA / CATENATE (`b.mail.server.imap`), RFC 8620 + RFC 8621 JMAP Core + Mail over HTTP/SSE/WebSocket (`b.mail.server.jmap`), POP3 (`b.mail.server.pop3`), ManageSieve (`b.mail.server.managesieve`)
173
173
  - **JMAP EmailSubmission reference** — composes `b.mail.send.deliver` to land the RFC 8621 §7.5 surface end-to-end (`b.mail.server.jmap.emailSubmissionSetHandler`)
174
174
  - **Mail crypto** — PQC-first S/MIME via CMS (`b.mail.crypto.cms`) + OpenPGP encrypt/decrypt + WKD key discovery with IDN-homograph defense (`b.mail.crypto.pgp`)
175
175
  - **Mail-stack agent** — multi-threaded worker pool + queue dispatch + sealed mail-store backed by SQLite FTS5 (`b.mail.agent`, `b.mailStore`)
@@ -228,7 +228,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
228
228
  - **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`)
229
229
  - **CSV** — RFC 4180 with Excel formula-injection prevention (`b.csv`)
230
230
  - **IDs + slugs** — RFC 9562 UUID v4 + v7 (`b.uuid`); URL-safe slugs (`b.slug`)
231
- - **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
231
+ - **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); in-memory extraction with no disk write for read-only / serverless filesystems (`b.archive.read.zip(...).extractEntries()` / `.tar`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
232
232
  - **Pagination + forms** — HMAC-signed cursor pagination (`b.pagination`); HTML form rendering + validation + CSRF (`b.forms`)
233
233
 
234
234
  ### Production
@@ -332,6 +332,7 @@ This is the minimum-viable security posture for a production deployment. The fra
332
332
  - [ ] For routes that accept YAML (config uploads, CI/CD pipelines, infra-as-code, document-import flows — ANY operator-supplied YAML the server parses): `b.guardYaml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound YAML bodies that don't go through those primitives, wire `b.guardYaml.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses deserialization-tag RCE (defends CVE-2026-24009 Docling/PyYAML, CVE-2022-1471 SnakeYAML, CVE-2017-18342 PyYAML class), billion-laughs alias recursion (CVE-2026-27807 MarkUs class), Norway-problem implicit booleans, multi-document streams, leading-zero octals, duplicate keys, merge-key anchor-chains, bidi/null/control chars. Unlike JSON, YAML's threat surface includes language-specific deserialization triggers — `!!python/object/new:...` / `!!java.util.HashMap` / `!!ruby/object` etc. — which the source-level scan catches before any downstream parser (PyYAML / SnakeYAML / js-yaml) sees them
333
333
  - [ ] For routes that accept JSON bodies (REST APIs / webhook receivers / config uploads — ANY operator-supplied JSON the server parses): `b.guardJson.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.12. For inbound JSON request bodies that don't go through those primitives, wire `b.guardJson.parse(body, { profile: "strict" })` before passing the parsed structure to operator handlers — strict refuses prototype pollution at source level (catches `__proto__` / `constructor` / `prototype` keys before any parser sees the input — defends CVE-2025-55182 React Server Functions RCE class), duplicate keys (RFC 8259 SHOULD-unique smuggling), NaN/Infinity, comments, JSON5 syntax, BOM, bidi/null/control chars, numeric precision-loss, depth + breadth + array-length + string-length caps. Pair with `topLevelKeyAllowlist: [...]` for routes with a known shape so unauthorized keys refuse before validation
334
334
  - [ ] For routes that accept email (inbound webhooks from mail providers, .eml uploads, mailbox imports, message-archival flows, customer-support-ticket-by-email — ANY operator-supplied RFC 822/5322 message the server processes): `b.guardEmail.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.17. For inbound message bytes that don't go through those primitives, wire `b.guardEmail.validateMessage(bytes, { profile: "strict" })` BEFORE the parser sees the message — strict refuses SMTP smuggling (bare CR / bare LF outside CRLF pairs combined with embedded SMTP verbs `MAIL FROM`/`RCPT TO`/`DATA`/`EHLO`/`HELO`/`RSET`/`QUIT` — defends CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim / CVE-2026-32178 .NET System.Net.Mail class), CRLF header injection in single-line headers (defends From/Bcc/body smuggling), IDN homograph mixed-script domains in address-bearing headers (Cyrillic / Greek / Armenian / Cherokee codepoints overlapping Latin — operator opts in to legitimate non-Latin via `allowedScripts: ["latin", "cyrillic"]`), Punycode `xn--` labels, display-name spoofing (`"support@apple.com" <attacker@evil>` — display contains @-address that doesn't match envelope domain), IP-literal addresses (`user@[1.2.3.4]` — bypasses DNS/DMARC alignment), RFC 5322 comment syntax in addresses, multiple @ characters, RFC 5321 length caps (local-part 64 / domain 255 / address 320), RFC 5322 line cap (998), BOM injection, bidi/null/control chars in addresses + headers. For per-address validation outside a full message context (form-submitted email, signup, MX-host validation), wire `b.guardEmail.validateAddress(addr, { profile: "strict" })`. Pair with operator's DMARC / SPF / DKIM verifier for envelope-alignment checks — guardEmail is the source-level gate, not the authentication-result interpreter
335
+ - [ ] For an inbound `b.mail.server.mx` listener, wire the connection-level gate cascade so the anti-abuse controls actually run: `helo: b.mail.helo` (FCrDNS / HELO-shape / self-name spoofing → 550), `rbl: b.mail.rbl.create({ providers: [...] })` (connecting-IP DNS blocklist → 554, evaluated once per connection), and `greylist: b.mail.greylist.create({ store })` (first-seen (ip, sender, recipient) → 450 tempfail). A gate you don't wire is skipped, not synthesized — so an unwired RBL is no protection, not a default-allow surprise. SPF/DKIM/DMARC alignment is performed on the delivered message via the agent handoff until the inbound-authentication pipeline lands
335
336
  - [ ] For routes that accept markdown (rich-text editors, comment systems, README rendering, documentation submission, GitHub-style wikis, mail-rendered markdown, document-import flows — ANY operator-supplied markdown the server renders): `b.guardMarkdown.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.16. For inbound markdown bodies that don't go through those primitives, wire `b.guardMarkdown.validate(body, { profile: "strict" })` BEFORE passing the source to any markdown renderer (marked / markdown-it / commonmark / remark / parsedown — all of them) — strict refuses dangerous URL schemes in inline links + images + autolinks + reference-link definitions (defends CVE-2025-9540 Markup Markdown class, CVE-2025-24981 MDC class, NuGetGallery GHSA-gwjh-c548-f787, Joplin GHSA-hff8-hjwv-j9q7), whitespace-tolerant dangerous-tag matching (`<script\n>` / `<script\t>` — defends CVE-2026-30838 CommonMark DisallowedRawHtml bypass class), HTML-entity scheme bypass (`&#x6A;avascript:` / `&#106;avascript:` decoded BEFORE scheme matching), reference-link smuggling (`[label]: javascript:...`), front-matter YAML/TOML blocks, HTML comments, code-fence language injection (language tag containing `<>"' `` blocks attribute breakout), catastrophic emphasis runs (CVE-2025-6493 CodeMirror Markdown class, CVE-2025-7969 markdown-it class), inline DOCTYPE, bidi/null/control chars, total-bytes + line + link + image + autolink + ref-def + list-depth + blockquote-depth caps. **Layer with `b.guardHtml`**: source-level guardMarkdown then render then output-level guardHtml together close the residual bypass surface that either alone misses (markdown engines surprise; sanitizers also surprise — defense in depth)
336
337
  - [ ] For routes that accept XML (SOAP endpoints, sitemap submissions, RSS / Atom feeds, OAI-PMH harvesters, SAML / WS-Federation receivers, document-import flows — ANY operator-supplied XML the server parses): `b.guardXml.gate({ profile: "strict" })` is wired by default into `b.fileUpload` + `b.staticServe` as of v0.7.15. For inbound XML bodies that don't go through those primitives, wire `b.guardXml.validate(body, { profile: "strict" })` before passing the document to any XML parser — strict refuses DOCTYPE declarations unconditionally (XXE + billion-laughs vector — defends CVE-2026-24400 AssertJ class, CVE-2024-8176 libexpat recursive-entity stack-overflow class), `<!ENTITY>` declarations including parameter entities (out-of-band exfiltration vector), external entity references (SYSTEM / PUBLIC with file:// / http:// / ftp:// schemes — local file read + SSRF), `<xi:include>` remote inclusion (CVE-2024-25062 libxml2 use-after-free class), `xsi:schemaLocation` operator-controlled schema fetch, processing instructions (`<?xml-stylesheet ?>` CSS-injection vector), CDATA sections (often used to hide payloads from naive scanners), XML signature wrapping (xmldsig surface), bidi/null/control chars in element text + attribute values, and applies depth + element + per-attribute-value caps. DOCTYPE remains refused at every profile level (strict / balanced / permissive) because billion-laughs is universal. Operators integrating with legacy SOAP that requires DTDs must instead route through a separately-firewalled XML processor with explicit allowlist — the gate has no knob to relax DOCTYPE
337
338
  - [ ] **For ZIP-shaped uploads specifically**, reach for `b.safeArchive.extract({ source, destination, guardProfile: "strict" })` — the one-liner composes `b.archive.read.zip`'s random-access reader (LFH/CD skew defense + CD-walk validation), `b.guardArchive.zipBombPolicy` defaults (per-entry + per-archive + ratio caps), `b.guardArchive.entryTypePolicy` defaults (symlink / hardlink / device / fifo / socket entries refused), and `b.guardFilename.verifyExtractionPath`'s dual-check (string-normalize + `fs.realpath`-agreement; refuses pre-resolve names exceeding PATH_MAX=4096 to defend the CVE-2025-4517 TOCTOU class). The fs-coupled realpath check is the depth above `b.guardArchive.checkExtractionPath`'s portable string-only gate; operators with their own extract loop call both. Default refuses ZIP encrypted entries (v0.12.10/11 add the encryption read paths). For tar / gzip / 7z / rar / zstd, the read-side primitives land in v0.12.8 / v0.12.9; until then use the legacy guard-only path below
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.13.6",
4
- "createdAt": "2026-05-27T02:34:13.690Z",
3
+ "frameworkVersion": "0.13.12",
4
+ "createdAt": "2026-05-27T10:09:34.345Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -187,9 +187,8 @@ function chatbot(session, opts) {
187
187
  * machine-readable manner. This primitive returns the disclosure
188
188
  * payload (visible label + structured metadata) the operator wires
189
189
  * into the encoder / response pipeline. C2PA manifest emission is
190
- * deferred to v0.12.21 `b.contentCredentials` this primitive
191
- * supplies the label markup and the metadata schema that the C2PA
192
- * adapter consumes when it lands.
190
+ * handled by `b.contentCredentials`; this primitive supplies the label
191
+ * markup and the metadata schema that the C2PA adapter consumes.
193
192
  *
194
193
  * @opts
195
194
  * contentType: "image" | "audio" | "video" | "text", // required
@@ -47,9 +47,11 @@ function _isGzipMagic(buf) {
47
47
  * `toAdapter(adapter)` / `digest()` — so gzip slots into the same
48
48
  * downstream sinks (object-store + filesystem + http adapters).
49
49
  *
50
- * Composes `b.archive.tar().toGzip(adapter)` / `b.archive.zip().
51
- * toGzip(adapter)` indirectly: those convenience methods call this
52
- * primitive after materializing their archive bytes.
50
+ * `b.archive.tar().toGzip(adapter)` composes this primitive after
51
+ * materializing the tar bytes (the canonical `.tar.gz`). There is no
52
+ * `zip().toGzip()` a ZIP is already DEFLATE-compressed per entry, so
53
+ * gzip-wrapping it would compress already-compressed data for no gain;
54
+ * gzip the uncompressed tar stream instead.
53
55
  *
54
56
  * @opts
55
57
  * level: number, // 0-9, default 6 (zlib default).
@@ -503,6 +503,18 @@ function zip(adapter, opts) {
503
503
  var entryTypePolicy = _normalizeEntryTypePolicy(opts.entryTypePolicy);
504
504
  var cdCache = null;
505
505
 
506
+ // Cooperative cancellation — operators pass an AbortSignal to bound a
507
+ // large/slow archive read. Checked between entries (the natural yield
508
+ // point); a long single-entry decompress is already bounded by the
509
+ // bomb policy.
510
+ function _throwIfAborted() {
511
+ if (opts.signal && opts.signal.aborted) {
512
+ throw new ArchiveReadError("archive-read/aborted",
513
+ "archive read aborted via opts.signal" +
514
+ (opts.signal.reason !== undefined ? ": " + String(opts.signal.reason) : ""));
515
+ }
516
+ }
517
+
506
518
  async function _loadCD() {
507
519
  if (cdCache) return cdCache;
508
520
  var eocd = await _locateEocd(adapter);
@@ -512,6 +524,7 @@ function zip(adapter, opts) {
512
524
  }
513
525
 
514
526
  async function inspect() {
527
+ _throwIfAborted();
515
528
  var loaded = await _loadCD();
516
529
  _enforceBombPolicy(loaded.entries, bombPolicy);
517
530
  _emitAudit(opts, "archive.read.inspect", "success", {
@@ -539,28 +552,22 @@ function zip(adapter, opts) {
539
552
  }
540
553
 
541
554
  async function* entries() {
555
+ _throwIfAborted();
542
556
  var loaded = await _loadCD();
543
557
  _enforceBombPolicy(loaded.entries, bombPolicy);
544
558
  for (var i = 0; i < loaded.entries.length; i += 1) {
559
+ _throwIfAborted();
545
560
  yield loaded.entries[i];
546
561
  }
547
562
  }
548
563
 
549
- async function extract(extractOpts) {
550
- extractOpts = extractOpts || {};
551
- if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
552
- throw new ArchiveReadError("archive-read/no-destination",
553
- "extract: opts.destination must be a non-empty string (target directory)");
554
- }
555
- var destination = nodePath.resolve(extractOpts.destination);
556
- if (!nodeFs.existsSync(destination)) {
557
- nodeFs.mkdirSync(destination, { recursive: true });
558
- }
559
- var loaded = await _loadCD();
560
- _enforceBombPolicy(loaded.entries, bombPolicy);
561
- // Compose b.guardArchive on the metadata pass — operators with a
562
- // posture set declared via opts.guardProfile get the cascade.
563
- var guardEntries = loaded.entries.map(function (e) {
564
+ // Run the b.guardArchive metadata cascade and refuse the whole archive on
565
+ // any critical issue. Shared by disk `extract` and in-memory `extractEntries`
566
+ // so both apply the identical posture-aware refusal. `auditAction` names the
567
+ // audit row for the refusal path.
568
+ function _assertGuardMetadata(loadedEntries, auditAction) {
569
+ if (opts.guardProfile === false) return;
570
+ var guardEntries = loadedEntries.map(function (e) {
564
571
  return {
565
572
  name: e.name,
566
573
  size: e.uncompressedSize,
@@ -573,29 +580,87 @@ function zip(adapter, opts) {
573
580
  attrs: { externalAttrs: e.externalAttrs },
574
581
  };
575
582
  });
576
- if (opts.guardProfile !== false) {
577
- var profile = opts.guardProfile || "balanced";
578
- var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
579
- if (guardResult && Array.isArray(guardResult.issues) && guardResult.issues.length > 0) {
580
- // Refuse the whole archive when any entry trips a critical
581
- // guard issue; collect every issue for the audit trail.
582
- var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
583
- if (critical.length > 0) {
584
- _emitAudit(opts, "archive.read.extract.refused", "refused", {
585
- entries: loaded.entries.length,
586
- issues: critical.map(function (i) { return i.ruleId; }),
587
- });
588
- throw new ArchiveReadError("archive-read/guard-refused",
589
- "extract refused — " + critical.length + " critical guard issue(s): " +
590
- critical.map(function (i) { return i.ruleId + " (" + i.snippet + ")"; }).join("; "));
591
- }
583
+ var profile = opts.guardProfile || "balanced";
584
+ var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
585
+ if (!guardResult || !Array.isArray(guardResult.issues) || guardResult.issues.length === 0) return;
586
+ var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
587
+ if (critical.length === 0) return;
588
+ _emitAudit(opts, auditAction, "refused", {
589
+ entries: loadedEntries.length,
590
+ issues: critical.map(function (i) { return i.ruleId; }),
591
+ });
592
+ throw new ArchiveReadError("archive-read/guard-refused",
593
+ "extract refused — " + critical.length + " critical guard issue(s): " +
594
+ critical.map(function (i) { return i.ruleId + " (" + i.snippet + ")"; }).join("; "));
595
+ }
596
+
597
+ // In-memory extraction: yields each file entry's decompressed bytes WITHOUT
598
+ // writing to disk — for read-only / serverless filesystems. Applies the same
599
+ // bomb-policy, guard cascade, entry-type policy, and per-entry filename
600
+ // safety as `extract`; directory entries are skipped (no bytes). The realpath
601
+ // agreement check is disk-specific and intentionally omitted (no extraction
602
+ // root); the caller owns where, if anywhere, the bytes land.
603
+ async function* extractEntries(extractOpts) {
604
+ extractOpts = extractOpts || {};
605
+ var loaded = await _loadCD();
606
+ _enforceBombPolicy(loaded.entries, bombPolicy);
607
+ _assertGuardMetadata(loaded.entries, "archive.read.extractEntries.refused");
608
+ var totalDecompressed = 0;
609
+ var yielded = 0;
610
+ for (var i = 0; i < loaded.entries.length; i += 1) {
611
+ _throwIfAborted();
612
+ var entry = loaded.entries[i];
613
+ if (entry.isEncrypted && !extractOpts.allowEncrypted) {
614
+ throw new ArchiveReadError("archive-read/encrypted-entry",
615
+ "entry " + JSON.stringify(entry.name) + " is encrypted — not decrypted on the in-memory path");
616
+ }
617
+ var typeRefusal = _enforceEntryTypePolicy(entry, entryTypePolicy);
618
+ if (typeRefusal) {
619
+ throw new ArchiveReadError("archive-read/entry-type-refused",
620
+ "entry " + JSON.stringify(entry.name) + " is a " + typeRefusal +
621
+ " — refused by entryTypePolicy");
592
622
  }
623
+ if (_isDirectoryEntry(entry.name, entry.externalAttrs)) continue;
624
+ // Archive-level name threats (Zip-Slip traversal, etc.) are refused for
625
+ // the whole archive by the guardArchive cascade above. The caller owns
626
+ // final placement of the returned bytes; we deliberately do NOT apply the
627
+ // disk-write filename policy (shell-exec extensions / reserved names) here
628
+ // — nothing is written, and over-filtering would drop legitimate names.
629
+ var lfhResult = await _verifyLfhMatchesCd(adapter, entry);
630
+ var body = await _decompressEntry(adapter, entry, lfhResult.dataStart, bombPolicy);
631
+ totalDecompressed += body.length;
632
+ if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
633
+ throw new ArchiveReadError("archive-read/total-too-large",
634
+ "cumulative uncompressed=" + totalDecompressed +
635
+ " exceeds maxTotalDecompressedBytes during extractEntries");
636
+ }
637
+ yielded += 1;
638
+ yield { name: entry.name, bytes: body, size: body.length };
639
+ }
640
+ _emitAudit(opts, "archive.read.extractEntries.completed", "success", { entries: yielded });
641
+ }
642
+
643
+ async function extract(extractOpts) {
644
+ extractOpts = extractOpts || {};
645
+ if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
646
+ throw new ArchiveReadError("archive-read/no-destination",
647
+ "extract: opts.destination must be a non-empty string (target directory)");
593
648
  }
649
+ var destination = nodePath.resolve(extractOpts.destination);
650
+ if (!nodeFs.existsSync(destination)) {
651
+ nodeFs.mkdirSync(destination, { recursive: true });
652
+ }
653
+ var loaded = await _loadCD();
654
+ _enforceBombPolicy(loaded.entries, bombPolicy);
655
+ // Compose b.guardArchive on the metadata pass — operators with a
656
+ // posture set declared via opts.guardProfile get the cascade.
657
+ _assertGuardMetadata(loaded.entries, "archive.read.extract.refused");
594
658
  var written = [];
595
659
  var bytesExtracted = 0;
596
660
  var totalDecompressed = 0;
597
661
  try {
598
662
  for (var i = 0; i < loaded.entries.length; i += 1) {
663
+ _throwIfAborted();
599
664
  var entry = loaded.entries[i];
600
665
  // Skip directory + dangerous-by-default entry types unless the
601
666
  // entry-type policy opts in.
@@ -693,10 +758,11 @@ function zip(adapter, opts) {
693
758
  }
694
759
 
695
760
  return {
696
- kind: "zip-random-access",
697
- inspect: inspect,
698
- entries: entries,
699
- extract: extract,
761
+ kind: "zip-random-access",
762
+ inspect: inspect,
763
+ entries: entries,
764
+ extract: extract,
765
+ extractEntries: extractEntries,
700
766
  };
701
767
  }
702
768
 
@@ -276,6 +276,87 @@ function tar(adapter, opts) {
276
276
  });
277
277
  }
278
278
 
279
+ // Shared b.guardArchive metadata cascade — disk `extract` + in-memory
280
+ // `extractEntries` refuse the whole archive identically on a critical issue.
281
+ function _assertGuardMetadata(entries, auditAction) {
282
+ if (opts.guardProfile === false) return;
283
+ var profile = opts.guardProfile || "balanced";
284
+ var guardEntries = entries.map(function (e) {
285
+ return {
286
+ name: e.name,
287
+ size: e.size,
288
+ compressedSize: e.size,
289
+ isSymlink: e.typeflag === TF_SYMLINK,
290
+ isHardlink: e.typeflag === TF_HARDLINK,
291
+ linkTarget: e.linkname,
292
+ isDirectory: e.typeflag === TF_DIRECTORY,
293
+ isEncrypted: false,
294
+ attrs: { mode: e.mode },
295
+ };
296
+ });
297
+ var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
298
+ if (!guardResult || !Array.isArray(guardResult.issues)) return;
299
+ var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
300
+ if (critical.length === 0) return;
301
+ _emitAudit(opts, auditAction, "refused", {
302
+ entries: entries.length,
303
+ issues: critical.map(function (i) { return i.ruleId; }),
304
+ });
305
+ throw new TarError("archive-tar/guard-refused",
306
+ "extract refused — " + critical.length + " critical guard issue(s)");
307
+ }
308
+
309
+ // In-memory extraction: yields each regular file entry's bytes without
310
+ // writing to disk (read-only / serverless filesystems). Same guard cascade,
311
+ // type-policy refusals, filename safety, and bomb cap as `extract`; directory
312
+ // and (validated) link entries carry no content and are not yielded.
313
+ async function* extractEntries(extractOpts) {
314
+ extractOpts = extractOpts || {};
315
+ var allowDangerous = extractOpts.allowDangerous || {};
316
+ var walked = await _walk();
317
+ var entries = walked.entries;
318
+ var bytes = walked.bytes;
319
+ _assertGuardMetadata(entries, "archive.read.tar.extractEntries.refused");
320
+ var totalDecompressed = 0;
321
+ var yielded = 0;
322
+ for (var i = 0; i < entries.length; i += 1) {
323
+ var entry = entries[i];
324
+ var type = _classifyTypeflag(entry.typeflag);
325
+ if (type === "device" || type === "fifo" || type === "socket") {
326
+ throw new TarError("archive-tar/entry-type-refused",
327
+ "entry " + JSON.stringify(entry.name) + " is a " + type + " — refused unconditionally");
328
+ }
329
+ if (type === "symlink" && !(allowDangerous.symlinks || entryTypePolicy.symlinks)) {
330
+ throw new TarError("archive-tar/entry-type-refused",
331
+ "entry " + JSON.stringify(entry.name) + " is a symlink — refused by entryTypePolicy");
332
+ }
333
+ if (type === "hardlink" && !(allowDangerous.hardlinks || entryTypePolicy.hardlinks)) {
334
+ throw new TarError("archive-tar/entry-type-refused",
335
+ "entry " + JSON.stringify(entry.name) + " is a hardlink — refused by entryTypePolicy");
336
+ }
337
+ // Directories + (now-permitted) links carry no content bytes.
338
+ if (type === "directory" || type === "symlink" || type === "hardlink") continue;
339
+ // Archive-level name threats are refused by the guardArchive cascade
340
+ // above; the caller owns placement of the returned bytes, so the
341
+ // disk-write filename policy is intentionally not applied here.
342
+ // COPY the slice — bytes.subarray/slice shares the full collected-archive
343
+ // backing store, so a caller retaining one entry would pin the whole
344
+ // archive in memory, defeating the serverless memory goal. Buffer.from
345
+ // gives the entry its own backing store (matching the ZIP path, whose
346
+ // _decompressEntry already returns a fresh buffer).
347
+ var body = Buffer.from(bytes.subarray(entry._bodyStart, entry._bodyStart + entry.size));
348
+ totalDecompressed += body.length;
349
+ if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
350
+ throw new TarError("archive-tar/total-too-large",
351
+ "cumulative uncompressed=" + totalDecompressed +
352
+ " exceeds maxTotalDecompressedBytes during extractEntries");
353
+ }
354
+ yielded += 1;
355
+ yield { name: entry.name, bytes: body, size: body.length };
356
+ }
357
+ _emitAudit(opts, "archive.read.tar.extractEntries.completed", "success", { entries: yielded });
358
+ }
359
+
279
360
  async function extract(extractOpts) {
280
361
  extractOpts = extractOpts || {};
281
362
  if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
@@ -290,34 +371,7 @@ function tar(adapter, opts) {
290
371
  var walked = await _walk();
291
372
  var entries = walked.entries;
292
373
  var bytes = walked.bytes;
293
- if (opts.guardProfile !== false) {
294
- var profile = opts.guardProfile || "balanced";
295
- var guardEntries = entries.map(function (e) {
296
- return {
297
- name: e.name,
298
- size: e.size,
299
- compressedSize: e.size,
300
- isSymlink: e.typeflag === TF_SYMLINK,
301
- isHardlink: e.typeflag === TF_HARDLINK,
302
- linkTarget: e.linkname,
303
- isDirectory: e.typeflag === TF_DIRECTORY,
304
- isEncrypted: false,
305
- attrs: { mode: e.mode },
306
- };
307
- });
308
- var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
309
- if (guardResult && Array.isArray(guardResult.issues)) {
310
- var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
311
- if (critical.length > 0) {
312
- _emitAudit(opts, "archive.read.tar.extract.refused", "refused", {
313
- entries: entries.length,
314
- issues: critical.map(function (i) { return i.ruleId; }),
315
- });
316
- throw new TarError("archive-tar/guard-refused",
317
- "extract refused — " + critical.length + " critical guard issue(s)");
318
- }
319
- }
320
- }
374
+ _assertGuardMetadata(entries, "archive.read.tar.extract.refused");
321
375
  var written = [];
322
376
  var bytesExtracted = 0;
323
377
  var totalDecompressed = 0;
@@ -405,9 +459,10 @@ function tar(adapter, opts) {
405
459
  }
406
460
 
407
461
  return {
408
- kind: "tar-reader",
409
- inspect: inspect,
410
- extract: extract,
462
+ kind: "tar-reader",
463
+ inspect: inspect,
464
+ extract: extract,
465
+ extractEntries: extractEntries,
411
466
  };
412
467
  }
413
468
 
@@ -56,11 +56,10 @@
56
56
  * - CVE-2024-12905 / CVE-2025-48387 (tar-fs path traversal).
57
57
  * - CVE-2025-4138 / 4330 (Python tarfile data filter bypass).
58
58
  *
59
- * Out of scope (v1):
59
+ * Compression is via `b.archive.gz` composition (tar.gz). Out of scope
60
+ * (v1):
60
61
  * - Sparse-file emission (read reconstructs them; write doesn't
61
62
  * produce sparse).
62
- * - Compression (tar.gz lands v0.12.9 via `b.archive.gz`
63
- * composition).
64
63
  * - BSD-tar extensions beyond pax.
65
64
  *
66
65
  * @card
@@ -155,6 +155,18 @@ function fromAssertion(opts) {
155
155
  "fal.fromAssertion: channel must be 'front' or 'back'");
156
156
  }
157
157
  var hokBinding = opts.hokBinding;
158
+ // `bearerOnly: true` is the explicit alias for "no proof-of-possession
159
+ // binding" (hokBinding === null). It contradicts a non-null hokBinding;
160
+ // refuse the contradiction at the entry point rather than silently
161
+ // picking one — an operator who sets both has a config bug.
162
+ if (opts.bearerOnly === true) {
163
+ if (hokBinding !== undefined && hokBinding !== null) {
164
+ throw new AuthError("auth/bad-fal-opts",
165
+ "fal.fromAssertion: bearerOnly:true conflicts with hokBinding '" + hokBinding +
166
+ "' (bearerOnly forces no proof-of-possession binding)");
167
+ }
168
+ hokBinding = null;
169
+ }
158
170
  if (hokBinding !== undefined && hokBinding !== null) {
159
171
  if (hokBinding !== "mtls" && hokBinding !== "dpop" && hokBinding !== "saml-hok") {
160
172
  throw new AuthError("auth/bad-fal-opts",
@@ -28,8 +28,8 @@
28
28
  *
29
29
  * Defenses against the well-known JWT pitfalls:
30
30
  *
31
- * - alg confusion (CVE-2024-54150 / CVE-2025-30144 / CVE-2026-22817
32
- * Hono class) — `algorithms` is REQUIRED with no default; `none`,
31
+ * - alg confusion (CVE-2024-54150 / CVE-2026-22817 Hono class) —
32
+ * `algorithms` is REQUIRED with no default; `none`,
33
33
  * `HS256` cannot be accepted unless the operator explicitly listed
34
34
  * them, and even then the verifier refuses HS* algs in
35
35
  * verifyExternal because HMAC + a public-key JWKS is the canonical
@@ -175,8 +175,8 @@ function _assertAlgKtyMatch(alg, jwk) {
175
175
  else if (alg === "ML-DSA-65" || alg === "ML-DSA-87") { expectedKty = "AKP"; }
176
176
  else {
177
177
  // Unknown alg — caller's alg allowlist should have rejected first;
178
- // refuse here defensively (CVE-2026-23993 class — unknown-alg paths
179
- // that skip downstream verification).
178
+ // refuse here defensively (CWE-347 alg-confusion class — unknown-alg
179
+ // paths that skip downstream verification; cf. CVE-2026-22817).
180
180
  throw new AuthError("auth-jwt-external/unsupported-alg",
181
181
  "_assertAlgKtyMatch: alg '" + alg + "' has no defined key-type binding");
182
182
  }
@@ -336,7 +336,7 @@ async function verifyExternal(token, opts) {
336
336
 
337
337
  // Decode header + payload.
338
338
  var parts = token.split(".");
339
- // CVE-2026-29000 / CVE-2026-23993 / CVE-2026-22817 / CVE-2026-34950 —
339
+ // CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950 —
340
340
  // JWE-bypass + alg-confusion. A 5-segment compact serialization is a
341
341
  // JWE (RFC 7516); accepting it on a JWS verifier is the canonical
342
342
  // confused-deputy shape. verifyExternal is JWS-only; refuse JWE
@@ -350,7 +350,7 @@ async function verifyExternal(token, opts) {
350
350
  }); } catch (_e) { /* audit best-effort */ }
351
351
  throw new AuthError("auth-jwt-external/jwe-refused",
352
352
  "5-segment JWE token refused — verifyExternal only handles JWS " +
353
- "(JWE bypass class — CVE-2026-29000 / CVE-2026-23993 / CVE-2026-22817 / CVE-2026-34950)");
353
+ "(JWE bypass class — CVE-2026-29000 / CVE-2026-22817 / CVE-2026-34950)");
354
354
  }
355
355
  if (parts.length !== 3) {
356
356
  throw new AuthError("auth-jwt-external/malformed-jwt",
@@ -371,8 +371,9 @@ async function verifyExternal(token, opts) {
371
371
  throw new AuthError("auth-jwt-external/unknown-crit",
372
372
  "token declares 'crit' header — verifyExternal does not support critical extensions");
373
373
  }
374
- // CVE-2026-23993 refuse alg values outside the accepted list BEFORE
375
- // any key lookup. The early refusal closes the class where an
374
+ // Alg-allowlist gate (CWE-347 improper-sig-verification / CWE-757
375
+ // algorithm-downgrade) refuse alg values outside the accepted list
376
+ // BEFORE any key lookup. The early refusal closes the class where an
376
377
  // unknown / unsupported alg slips through to a downstream code path
377
378
  // that interprets it permissively. The per-listed algorithm check
378
379
  // above in the opts-validation loop refuses the OPERATOR'S allowlist
@@ -381,11 +382,11 @@ async function verifyExternal(token, opts) {
381
382
  if (opts.algorithms.indexOf(header.alg) === -1) {
382
383
  throw new AuthError("auth-jwt-external/alg-not-allowed",
383
384
  "token alg='" + header.alg + "' not in allowed list [" + opts.algorithms.join(", ") +
384
- "] (CVE-2026-23993 — refused before key lookup)");
385
+ "] (alg-allowlist gate — refused before key lookup)");
385
386
  }
386
387
  if (SUPPORTED_CLASSICAL_ALGS.indexOf(header.alg) === -1) {
387
388
  throw new AuthError("auth-jwt-external/unsupported-alg",
388
- "token alg='" + header.alg + "' is not in the verifier's supported set (CVE-2026-23993)");
389
+ "token alg='" + header.alg + "' is not in the verifier's supported set (alg-allowlist gate)");
389
390
  }
390
391
 
391
392
  // Resolve key.
@@ -480,7 +481,10 @@ async function verifyExternal(token, opts) {
480
481
  // weak iss validation. Constant-time compare defeats prefix-timing
481
482
  // narrowing; emit a DISTINCT audit event (separate from sig-verify-
482
483
  // fail) so detection signals lights up on the cross-realm shape
483
- // independently of generic verification failures.
484
+ // independently of generic verification failures. The `typeof ... !==
485
+ // "string"` guard also rejects an array-valued iss (CVE-2025-30144,
486
+ // fast-jwt — an iss array `["attacker", "valid"]` passed an any-match
487
+ // check); only a single string iss is accepted.
484
488
  if (typeof payload.iss !== "string" ||
485
489
  !_issuerMatches(payload.iss, opts.issuer)) {
486
490
  try { audit().safeEmit({
@@ -234,8 +234,8 @@ async function verify(token, opts) {
234
234
  // SECURITY: when the resolver uses header.kid as a filename / map
235
235
  // key / cache index, it MUST sanitize the kid first. Path-traversal
236
236
  // (`../etc/passwd`), null-byte (`key\0..`), control chars, and
237
- // similar shapes turn a kid lookup into an arbitrary-file-read
238
- // primitive (CVE-2018-0114 java-jwt class). Use
237
+ // similar shapes turn a kid lookup into an arbitrary-file-read or
238
+ // SQLi primitive (the PortSwigger JWT "kid" injection / LFI class). Use
239
239
  // `b.guardJwt.kidSafe(header.kid)` — throws on traversal indicators
240
240
  // and control bytes, returns the validated kid on success.
241
241
  var key;