@blamejs/blamejs-shop 0.5.2 → 0.5.3

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - v0.5.3 (2026-06-27) — **Vendored framework refreshed to 0.15.37 — non-finite numeric options can no longer silently disable a security check.** Updates the vendored blamejs framework to 0.15.37. The upstream release closes a class of fail-open configuration bugs: several numeric options validated a value only as `typeof === number && >= 0`, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently turned off the very protection it tunes — an `Infinity` CWT / OCSP-staple / ARC clock-skew made the expiry and freshness comparisons unsatisfiable (so an expired token, a replayed pre-revocation OCSP `good` response, or an expired ARC seal would be accepted), and an `Infinity` WebSocket message/frame cap, inbox/flag-cache size cap, or audit-chain fan-out cap disabled the bound. These options now route through the framework's finite-bounds validator, refusing a non-finite value at the entry point (or falling back to the safe default), while options where unbounded is deliberate (reconnect-indefinitely, retain-indefinitely) still accept it. This storefront configures these options with finite values, so there is no runtime or behavior change for it; the hardened framework rides along and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.37 (non-finite option values rejected)* — Refreshes the vendored blamejs framework to 0.15.37. The upstream fix makes a clock-skew tolerance or resource cap of `Infinity` / `NaN` a rejected value rather than one that silently disables the check it configures — covering the CWT / OCSP-staple / ARC time checks, the WebSocket-client inbound size and handshake caps, and the inbox / flag-cache / audit-chain size and count caps, with deliberately-unbounded options (reconnect/retain indefinitely) unaffected. The shipped storefront passes finite values to these options, so its runtime, public API, and wire format are unchanged; the hardened framework simply rides along.
12
+
11
13
  - v0.5.2 (2026-06-27) — **Vendored framework refreshed to 0.15.36.** Updates the vendored blamejs framework to 0.15.36. The upstream release is internal test-suite tooling only: it re-verifies the guard-suite class that allows a bare comma/semicolon split on token-only RFC header grammars (RRULE, RFC 9421 signature components, TLS-RPT rua, SCIM attribute paths), renames its marker to a descriptive token, and prunes five inert marker comments. No runtime code, public API, or wire format changed upstream, so there is no runtime or behavior change for this storefront and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.36* — Refreshes the vendored blamejs framework to 0.15.36, an upstream release scoped to test-suite tooling — a guard-suite class re-verified and its marker renamed, plus five inert marker comments removed. The shipped framework runtime, public API, and wire format are unchanged, so no operator action is required.
12
14
 
13
15
  - v0.5.1 (2026-06-27) — **Vendored framework refreshed to 0.15.35 — Prometheus label-injection fix in the metrics shadow registry.** Updates the vendored blamejs framework to 0.15.35. The upstream release fixes a label-injection in the framework's metrics shadow registry: it built a metric's cardinality key by joining label pairs with commas and re-split that string when rendering the Prometheus exposition, so a comma (or `=`) inside a label VALUE forged additional label pairs in the scrape output — which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary — and two distinct label sets could collide into one cardinality bucket. The key now uses canonical-JSON of the label set and the render emits the structured labels instead of re-splitting, so a value's commas/equals stay inside the value and a label named `constructor` / `prototype` / `__proto__` is preserved. This storefront does not compose the metrics shadow registry, so there is no runtime change for it; the fix ships in the vendored framework regardless and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.35 (Prometheus label-injection fix)* — Refreshes the vendored blamejs framework to 0.15.35. The upstream fix stops a comma or `=` in a metric label value from forging extra Prometheus label pairs in the shadow registry's scrape output — the cardinality key is now canonical-JSON of the label set and the render emits the structured labels rather than splitting a joined string, so distinct label sets stay distinct and reserved-name labels survive. The shipped storefront does not expose the metrics shadow registry, so its runtime, public API, and wire format are unchanged; the hardened framework simply rides along.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.2",
2
+ "version": "0.5.3",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -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.15.36",
7
- "tag": "v0.15.36",
6
+ "version": "0.15.37",
7
+ "tag": "v0.15.37",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -859,6 +859,7 @@
859
859
  "release-notes/v0.15.34.json": "lib/vendor/blamejs/release-notes/v0.15.34.json",
860
860
  "release-notes/v0.15.35.json": "lib/vendor/blamejs/release-notes/v0.15.35.json",
861
861
  "release-notes/v0.15.36.json": "lib/vendor/blamejs/release-notes/v0.15.36.json",
862
+ "release-notes/v0.15.37.json": "lib/vendor/blamejs/release-notes/v0.15.37.json",
862
863
  "release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
863
864
  "release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
864
865
  "release-notes/v0.15.6.json": "lib/vendor/blamejs/release-notes/v0.15.6.json",
@@ -1567,7 +1568,7 @@
1567
1568
  ".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
1568
1569
  ".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
1569
1570
  "ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
1570
- "CHANGELOG.md": "sha256:7b4cf6d91c65bf01fda536a83f26198e722bf97cef6c88f3cfd58436ebc2e43a",
1571
+ "CHANGELOG.md": "sha256:e14e4572e0a057f9633365e55eb176592f766006a4392009a25c9060d9b17cc0",
1571
1572
  "CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
1572
1573
  "CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
1573
1574
  "GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
@@ -1577,7 +1578,7 @@
1577
1578
  "NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
1578
1579
  "README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
1579
1580
  "SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
1580
- "api-snapshot.json": "sha256:204692f40f115ba8cc211fd485feb2a11bfae877b9c013aba085c0729cf267bc",
1581
+ "api-snapshot.json": "sha256:8665f75a6befdc47255199601827a9642aa9f1662f271c35325068482edb11ad",
1581
1582
  "assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
1582
1583
  "assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
1583
1584
  "bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
@@ -1785,7 +1786,7 @@
1785
1786
  "lib/agent-event-bus.js": "sha256:c8b21e11f0b6596ddfc36cd87a7ee26185e41ff59e3bb65c0fb4b02e525ee1e4",
1786
1787
  "lib/agent-idempotency.js": "sha256:dc3f9f82d129a844812a042e6730f167df471fc0792d60b0dc2bb174604d896e",
1787
1788
  "lib/agent-orchestrator.js": "sha256:517f672eefbfc430cabf06f3162ab5fd2212996d17366909d9eff7aa514363b2",
1788
- "lib/agent-posture-chain.js": "sha256:2e09b38742194b5823d174315c6ae2dd89857d9e662286f37c46c14627243b1a",
1789
+ "lib/agent-posture-chain.js": "sha256:450f0ebe438b4e99d7017a498ae1f705b54c472145111d145348be2ea78490d7",
1789
1790
  "lib/agent-saga.js": "sha256:1a239892fe534d16e5567b731730bebc073057fe7d3bae63e0c49d989648c501",
1790
1791
  "lib/agent-snapshot.js": "sha256:eac77c6c771b31ced5ce01937ee0cab301bc97a5b96d9db949471296029da769",
1791
1792
  "lib/agent-stream.js": "sha256:8e519cba5505ad8e89bfe827483c16fe577ad39742e17f8f01d30b430a4bbf0b",
@@ -1823,7 +1824,7 @@
1823
1824
  "lib/asyncapi-traits.js": "sha256:82ec58b77f0a65ef1b5e8b67ee9555e9cb984de769c3393fe5c6d4d0640546f0",
1824
1825
  "lib/asyncapi.js": "sha256:cd48847ed363023e248cf0fbe275b545b0002b85e93ed91315990ef8a6abe483",
1825
1826
  "lib/atomic-file.js": "sha256:b93b008c121f785d149d4450681467b18785781c3a82b9b001bb2a7d636c3ad1",
1826
- "lib/audit-chain.js": "sha256:1c9895a2bbf6ce3b4c546b1b3c95ac9d5ed55d82a2b58c0de4e3ecc1947c3c31",
1827
+ "lib/audit-chain.js": "sha256:c8c4bfb7abf0a4f2288b56404f30c1d8311c22f3c71f6c9c3a606a142744f3ea",
1827
1828
  "lib/audit-daily-review.js": "sha256:61cebe47d6e5eeeedcab45515473acfaab623217810a7ab3629b3aeca5028519",
1828
1829
  "lib/audit-emit.js": "sha256:1171699027269e154fd247141a549e65bcdb566e52ed00b993c7dfb9a293ba0f",
1829
1830
  "lib/audit-sign.js": "sha256:b8ddef053fca28c1e6cf265f73ecd254290a9c2e0759250342e926280205342f",
@@ -1920,7 +1921,7 @@
1920
1921
  "lib/crypto.js": "sha256:0884094cdfc52e577c10b51622b940e9acc1ce2ef8dd8996d790e51e80b2f9b5",
1921
1922
  "lib/csp.js": "sha256:b7a4d24f721767edecb7bc0c7a28c7824b7dda596c2edb3be387304684a8698a",
1922
1923
  "lib/csv.js": "sha256:eca5d01409e72f5f73c5a954f3019826ed5da06f72103a5117016aeb0654f17c",
1923
- "lib/cwt.js": "sha256:1c39f6ea999ecdb2cb119933b497af9cbfd512aeaa73261076f12018f0393e80",
1924
+ "lib/cwt.js": "sha256:b0156f9098c3eb336221949c838b25f2eb9d4f765af6e293df225493f64c12a4",
1924
1925
  "lib/daemon.js": "sha256:9441d845c9a2d4b2808cdd6bf18915d89338a3dd75fa304c4b38c047b9f47a54",
1925
1926
  "lib/dark-patterns.js": "sha256:e54cc6fb591748b937627560ccdfd84da0e84974da5189e15b911e00b3c7e51b",
1926
1927
  "lib/data-act.js": "sha256:9d8f276393bef93e90ddf471f6c424cf95728a9c67fc8416fc5e3ccc99f690ee",
@@ -1947,14 +1948,14 @@
1947
1948
  "lib/error-page.js": "sha256:4ed2e8a118b35d3dc76a072b233713ae1fdaedb54334df68ad081e19f765cb88",
1948
1949
  "lib/events.js": "sha256:b5f0034fda33285f571b9b247db50bff08f632762d9bf69b83784b435eeea75c",
1949
1950
  "lib/external-db-migrate.js": "sha256:990014016e71a818672b3f2c32eaa2ff6815553feb52efafb88ec765df9a570a",
1950
- "lib/external-db.js": "sha256:d39513c1ef842397374218d8416c62ff0ec36e73ab0e9c507298dcfb9d0bda63",
1951
+ "lib/external-db.js": "sha256:309a6bf0156f609dd3f143eaae1f41eec5a22a49f9d83eaf7c8f6f28d82b2d76",
1951
1952
  "lib/fapi2.js": "sha256:6ae4679931675a629fa3f9e9efba71172fe69251dec2dd5d8c995cfb9dcad8f7",
1952
1953
  "lib/fda-21cfr11.js": "sha256:9256bcea7d526a7280d5d3b1beed5be1daf2402ded2d5a18d602466d01a7884c",
1953
1954
  "lib/fdx.js": "sha256:67a29b140b6d07a767da31d6b55d1db0c2219cdaa780531fdec0fec63a5c551c",
1954
1955
  "lib/fedcm.js": "sha256:4b0f1e3d8e762353a28b2d42e00678c120d6e8ad574ecb9d94d4aa8dead0f7f7",
1955
1956
  "lib/file-type.js": "sha256:158d798f8204cc96038b4fda2a215bb4ba79232476e31195f15d4fee8f790a00",
1956
1957
  "lib/file-upload.js": "sha256:436b763fd928f286fb7faba8e2e9373cd37dcf677cb19e12afee61f00413e420",
1957
- "lib/flag-cache.js": "sha256:7bf41923bb42df59a41df37f7baa5b8b67172ba9ca71eeab47841dd0e4c9b64f",
1958
+ "lib/flag-cache.js": "sha256:4333ce5105e2a2970b7277fa77f6664d1c6d2e3c6e7c3fa6094c228d305df4dc",
1958
1959
  "lib/flag-evaluation-context.js": "sha256:fa8846a9a97a9d773dc08dd0da664d1d6d3fead4e12dc9982b33fa45b59d47a8",
1959
1960
  "lib/flag-providers.js": "sha256:d85ff62c2d599634c3d2dc9d4e4a8cfcecec8178ce63a2800438434cc365f84a",
1960
1961
  "lib/flag-targeting.js": "sha256:c2b5096e04dcaa98435c66cfa6b1aae02d0da708ca7e96f810627ef38e669309",
@@ -2040,7 +2041,7 @@
2040
2041
  "lib/iab-mspa.js": "sha256:64d45d2f8b8373efe4daa0584f72b1a50940c9118aeb4f0925c38d3fdb3d64f3",
2041
2042
  "lib/iab-tcf.js": "sha256:c21467eced77bce0bc28ca08a7ef5ea5f7a99b4f3f5fad36c1cb70b773e0f7c3",
2042
2043
  "lib/importmap-integrity.js": "sha256:addabb8fb49c9ce94e768d8f6d32ac05dc096b2c036eff3ad3528ba65b84292b",
2043
- "lib/inbox.js": "sha256:dd603479a72420288f5aa65d4b50a147f02e135d8e2899a3ac7760cb2af56261",
2044
+ "lib/inbox.js": "sha256:40e662386d56d4f2b3fbad02f1fed8a33f75e88596e12f5f88a7acfff94de588",
2044
2045
  "lib/incident-report.js": "sha256:03729af0dcaba8aadd709c7c3036776c6155b012bd7b80ffc2a13a42609d2a0f",
2045
2046
  "lib/ip-utils.js": "sha256:cc7a40523a25f3732f8f9ce3b52d725a981ca831a973888abd059ef8e5dfa459",
2046
2047
  "lib/jobs.js": "sha256:f8e273bdd25b172cff189a8b5b56748828216fdb5d5f445634477c7b2686e7c2",
@@ -2068,9 +2069,9 @@
2068
2069
  "lib/log.js": "sha256:8268967e7f45bf59b131ba7e651b48a15656850f5585efe98001af4406c7c495",
2069
2070
  "lib/lro.js": "sha256:da9baf47f27c422c32d51495b2896c887ec3ac283875712efcd7528fd396868b",
2070
2071
  "lib/mail-agent.js": "sha256:4ca93408c03873796d285d002067ffa54fa70859ad5437215d6001d90c0f76ea",
2071
- "lib/mail-arc-sign.js": "sha256:872f31e16321f8629e3af3cac31d3deb460804748b25a7fbb427164a2ad88f84",
2072
+ "lib/mail-arc-sign.js": "sha256:68e71b313e7f44924c60ad668a101897db4c9966222793eed69bc1c868788d59",
2072
2073
  "lib/mail-arf.js": "sha256:47d02905b58c6c0733114e58594034adedd73799de39b93420459dbb04d22050",
2073
- "lib/mail-auth.js": "sha256:da409e8f2b3bae1308f1d008b6df27f468075a83ceaeae8ee7cda0ab9c4a1027",
2074
+ "lib/mail-auth.js": "sha256:2e7e72ca6dcc5598cb5a27ffb20b2fa43869d5a27aaf95fcf41b170dbafdea8a",
2074
2075
  "lib/mail-bimi.js": "sha256:b22afc5e8ad04503fee7e41f3b34c05c9f0759579436bfbe5f7706180c466155",
2075
2076
  "lib/mail-bounce.js": "sha256:461e885dd2ea0664e9a4af39ae5c36f3a5d9c7dc914dad4c40641b705952fd04",
2076
2077
  "lib/mail-crypto-pgp.js": "sha256:2e94f51d2cb13b92c612e84f73cf96e83eb17843d57b9dc9aef688ce55a1f9b4",
@@ -2110,7 +2111,7 @@
2110
2111
  "lib/mcp.js": "sha256:c65af74fdb3cf30436699aba5051c4d6e8cb972887cbaaecc46250d557599953",
2111
2112
  "lib/mdoc.js": "sha256:bc31d38d6fdfac8db8f040dfa4d33ab40405f56340bb4ec978f3ba98e93f6433",
2112
2113
  "lib/metrics.js": "sha256:55d445b738b2cfc8ceb1bfa7a1e69b306497e8065ea86d61e4f4087873acce1b",
2113
- "lib/middleware/age-gate.js": "sha256:dc8c61a4d41f4a781220c9c97eb475fb2867f5ee444eb2756badcbc5b8411795",
2114
+ "lib/middleware/age-gate.js": "sha256:4d4967beef100e2e58f1565de770e31fb8dd50dab2f04fa8251972fea5607826",
2114
2115
  "lib/middleware/ai-act-disclosure.js": "sha256:6fc62940d85813019b31c586fc86853570329a939565a6947f81052d50977c42",
2115
2116
  "lib/middleware/api-encrypt.js": "sha256:f90210bfc4fe04b50f6d72e14676a140c5759e255ed359567066a1e80ab5fb63",
2116
2117
  "lib/middleware/assetlinks.js": "sha256:bb36f8d53991668b4ae1557d0cab3ec8b479a16f9133c43f9adc07963c8b4750",
@@ -2182,7 +2183,7 @@
2182
2183
  "lib/network-nts.js": "sha256:90ce47584ad58c171e8029eb1d5df8ba4ba130c4038d1fe260109685c34c8f75",
2183
2184
  "lib/network-proxy.js": "sha256:9342c534863f905000d2432b67c26dd7a5ea02be25aaf9ebe05976079e61d4c9",
2184
2185
  "lib/network-smtp-policy.js": "sha256:4d10a339d5c471cdff217c5514038eb2e30f4ecb97a38cb4405165ee0f9dcf7e",
2185
- "lib/network-tls.js": "sha256:554b64c77389e1c961da8d061c63c806be994960466fb26f2b2268dbec0a369b",
2186
+ "lib/network-tls.js": "sha256:bfd58cf11c307151c98044d8b7ac43f7fefc8f8d36fee61789e88e7435aafbe0",
2186
2187
  "lib/network-tsig.js": "sha256:41dc85a6e816cce1c58c36891dfc6bafc612b68978936107a09a5a553563e0d7",
2187
2188
  "lib/network.js": "sha256:40cb01627bf45741cac22ef79f5985f69323e47732d54cf3ce28c06882830c25",
2188
2189
  "lib/nis2-report.js": "sha256:fa106a4f95dd9301d7da966c5792197bdbebac477a4f1d2fd2ac2481fbd818fd",
@@ -2341,14 +2342,14 @@
2341
2342
  "lib/wiki-concepts.js": "sha256:54789790b950d926cbd0657dd51b32435c4988012b520819615df93673680de5",
2342
2343
  "lib/worker-pool.js": "sha256:fc35b3c9f83a4c72be0981400f5294b566472bfe187df0500775e37dd71afa4c",
2343
2344
  "lib/worm.js": "sha256:f17e24f8ca39295f396f40bd0c4ad1c946f5e41ff646ef1b33e8d50dc8b9435e",
2344
- "lib/ws-client.js": "sha256:5a854921c96500d504c731d302a7d338d93400b41bbe12056aa1c38494ac7497",
2345
+ "lib/ws-client.js": "sha256:3ac479017e1199f1b69300b81e473f8303f6782161b73e95b17e47cf494ee066",
2345
2346
  "lib/x509-chain.js": "sha256:fdc0ecd264d9de0e039f348e819675fdf3838d61097a97a0e3e119def338722a",
2346
2347
  "lib/xml-c14n.js": "sha256:01a27d20df99ebd6eaefbff0aad933a2231049f65a54ae994236694d4146ce5d",
2347
2348
  "oss-fuzz/projects/blamejs/Dockerfile": "sha256:6c48ec4d81f4b1836a6b43701a2497cf737002413dd0da6a10892be1498b0766",
2348
2349
  "oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
2349
2350
  "oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
2350
2351
  "oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
2351
- "package.json": "sha256:585a646ad04f1afaa7faf4d273a16f5d8c6d530071d20db7b83f045ddaeaff73",
2352
+ "package.json": "sha256:ffb1cef6e0b19b328852007de2c92144fdb0a08f4627a4b3bf048eeb3857bee4",
2352
2353
  "release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
2353
2354
  "release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
2354
2355
  "release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
@@ -2387,6 +2388,7 @@
2387
2388
  "release-notes/v0.15.34.json": "sha256:2a1b36d897d8e9f2cece051c098030f08d9c6ee6bef136800bd19453ad65f188",
2388
2389
  "release-notes/v0.15.35.json": "sha256:e317b2252734aace7a0f734e3ab40c3a0c4ac208d678c0e9834be34eb424d15b",
2389
2390
  "release-notes/v0.15.36.json": "sha256:392c15724efc02fad5825aaf93b3f5dcef90a0cb9909d61f86e777f3061045ec",
2391
+ "release-notes/v0.15.37.json": "sha256:b696debed024bed09c59eaeb467d6dde93d7f30ad33f46af8a37d07534df09d8",
2390
2392
  "release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
2391
2393
  "release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
2392
2394
  "release-notes/v0.15.6.json": "sha256:0e3b9e5e43b70b61dd258c3003d1b8729cd3c26c62a34dedcca81bbec5d31077",
@@ -2633,7 +2635,7 @@
2633
2635
  "test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
2634
2636
  "test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
2635
2637
  "test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
2636
- "test/layer-0-primitives/codebase-patterns.test.js": "sha256:4180fd47eef2c81248ec83433e31f4975eefb0441bb3f3366a25a5a98ce5d0b9",
2638
+ "test/layer-0-primitives/codebase-patterns.test.js": "sha256:ae47993e4fb7be4bb52b4c71a178cd47928b853dec9935c6445d8966da04ed4f",
2637
2639
  "test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
2638
2640
  "test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
2639
2641
  "test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
@@ -2676,7 +2678,7 @@
2676
2678
  "test/layer-0-primitives/csp-report.test.js": "sha256:8bb2e54a31fd4317a6a39a4dc3358147a0102dbfa58b71d85a5743008289a2ff",
2677
2679
  "test/layer-0-primitives/csrf-protect.test.js": "sha256:802a8de2fee57c01a03b523cd6701780c181838006356c30a01452b30381225b",
2678
2680
  "test/layer-0-primitives/csv.test.js": "sha256:6a9ed5a3fc6fd315261ad0bf7788047de5323fe09c36bb52d6e599a761b18085",
2679
- "test/layer-0-primitives/cwt.test.js": "sha256:1e642b69cbd6a5b33b7755df3a17d5cecfb96e071560e4b384437ce05ab25e4d",
2681
+ "test/layer-0-primitives/cwt.test.js": "sha256:74b06a3278eacbaed102ae9e966fac8d43e3fc609c288fde4f535566df78a5ed",
2680
2682
  "test/layer-0-primitives/daemon.test.js": "sha256:a118c7a6d738115ea4590ef1d639e4c06f139732bcaf8545d649452dfdb3979e",
2681
2683
  "test/layer-0-primitives/daily-byte-quota.test.js": "sha256:c4683ef74d205ba8ebabf3c9f0ce60d0e86824cf0c8a0f23c1e6b8c5b5b70f23",
2682
2684
  "test/layer-0-primitives/dane.test.js": "sha256:59d01acff1a81f0d691ecb28920f5c22e30ef416bf84a52c853c1a00e64cf783",
@@ -2737,7 +2739,7 @@
2737
2739
  "test/layer-0-primitives/fido-mds3.test.js": "sha256:91893e57258f91386ea3baedf8727dac08b5c75725a74b74ecdfbc5d99bb4adc",
2738
2740
  "test/layer-0-primitives/file-type.test.js": "sha256:ba6bb725857cbf7b165f4391e3aa126c9fc4286c5afb92d1964ff1d0dfd2b57c",
2739
2741
  "test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js": "sha256:ad13f7f84a33899b4f4e66b74c896f606e070bfe5df769b0acd109cfb7ab38c7",
2740
- "test/layer-0-primitives/flag.test.js": "sha256:8b58826342b8c28d90e11e4cfe1a7e266c2296615a90558221cad12e9f6ecb4e",
2742
+ "test/layer-0-primitives/flag.test.js": "sha256:37f17c037220ef60bb0afb79cbddd359a9b5784ed6ce8efd3dc04063fbc09176",
2741
2743
  "test/layer-0-primitives/forensic-snapshot.test.js": "sha256:629a8af1ccd0396e2b30f8e46d76fe19eab9f33c156c0db78e3e76649e21030c",
2742
2744
  "test/layer-0-primitives/fsm.test.js": "sha256:7037831083426478ed4efd7d1c17d4b96c3609816a41819b5c9da71d2b6187e8",
2743
2745
  "test/layer-0-primitives/gate-contract-content-gate.test.js": "sha256:d9f8b94231efb0e02a94b7934bf58a3f8e343829ce11346b7302e69e13efd551",
@@ -2798,7 +2800,7 @@
2798
2800
  "test/layer-0-primitives/iab-tcf.test.js": "sha256:34e5f9b2740c0b96e3f84d003fbbf55a4d366f3d08f6baedca0109573eead9b6",
2799
2801
  "test/layer-0-primitives/idempotency-key.test.js": "sha256:0214b68cbb3f50a0c7cbc7fcb92b1edf86a714dd6040c3a1a2be34b31eb9c631",
2800
2802
  "test/layer-0-primitives/importmap-integrity.test.js": "sha256:babd21e2356c7d1d7916ae89fc228105290bf8c99371f0cde68512a62bea45dd",
2801
- "test/layer-0-primitives/inbox.test.js": "sha256:d0865782d10cb9a72bbf39223c5499d5b1ec50967173e2dfe3cd1bf88b279d8c",
2803
+ "test/layer-0-primitives/inbox.test.js": "sha256:b8c16ba0cd5f2453ef9cc242dfc1af85e88f8d65a361c02ec2209225a0e67b99",
2802
2804
  "test/layer-0-primitives/incident-report.test.js": "sha256:57bc8d037e6a3f84fcefe0a95205e5ac0f83c6dbb8b1f935c3c5184d2456c857",
2803
2805
  "test/layer-0-primitives/ip-utils.test.js": "sha256:0ca865e21be6a8815a5c0dd772497b3e0ab4cedd67e7dbabf9518c82af789132",
2804
2806
  "test/layer-0-primitives/jose-jwe-experimental.test.js": "sha256:fabd155817296fbac7cea4ccaaf5e1740471fe97b431fe46e13e6a30d13784ef",
@@ -2822,7 +2824,7 @@
2822
2824
  "test/layer-0-primitives/mail-agent.test.js": "sha256:51725784bab6849cb02f1e94e0ba97c2b8e7e101780a26ae7ec19f9d894640ef",
2823
2825
  "test/layer-0-primitives/mail-arf.test.js": "sha256:2279aebee547903a8c1415c9fc1c9c194757b3917e869bfddb43dd0375e68a93",
2824
2826
  "test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js": "sha256:22066c238cdb9c4f84377f56f2d69e387bcb4a601ef384cc874fc55249fbfaa6",
2825
- "test/layer-0-primitives/mail-auth.test.js": "sha256:4b967c9921ad3e9e84f87eca1a6077d7d089750c4e919ec3ea09e4a11cd06884",
2827
+ "test/layer-0-primitives/mail-auth.test.js": "sha256:36f434e2ddb65b3f4cae76be48a917971254959d2312c1f87a6dfd5144d54560",
2826
2828
  "test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js": "sha256:d14d66d801b5767372ce7014617841f8710f7ad95107984da058ade794fd4d8f",
2827
2829
  "test/layer-0-primitives/mail-bimi.test.js": "sha256:3e12e211e1f75f07d1ba43ada69c9adcd205902194f983d8ae24293b1acd1cd5",
2828
2830
  "test/layer-0-primitives/mail-bounce.test.js": "sha256:d4ef09adc71e1c9e336a9fc5b059e6b264d9abcd0c032023bfa7b15a9173a4b7",
@@ -3025,7 +3027,7 @@
3025
3027
  "test/layer-0-primitives/time.test.js": "sha256:59f6dcd5753b9bb97508044c30cfca948c4707dd8d107dab5eab53f0dcd91e84",
3026
3028
  "test/layer-0-primitives/tls-exporter.test.js": "sha256:3224b12f18619fa805e933c8eef18356e42f8554d75d7c4e23ebd8b5b993ca6b",
3027
3029
  "test/layer-0-primitives/tls-ocsp-ct.test.js": "sha256:7c98ddde33095683a3be527fee9e868501ca99eac74cc40fe1c56ca6018f1660",
3028
- "test/layer-0-primitives/tls-ocsp-freshness.test.js": "sha256:0d979ecdbc7d9c62bcb32e0f2594fa5425bed4a25ef3793915ad0890398fabf7",
3030
+ "test/layer-0-primitives/tls-ocsp-freshness.test.js": "sha256:ddf2723551337d3859add255644e322c74d5e55df07be181795484be0835ee2d",
3029
3031
  "test/layer-0-primitives/tls-ocsp-verify.test.js": "sha256:f726bb9387c570cee945a2b927070899bc7ae933ec09ba481c3aa5f0d11d2ba6",
3030
3032
  "test/layer-0-primitives/tls-pinset-drift.test.js": "sha256:6b7c530326a1025819257daec21711d6f889a8523f2766595f7cc28563f5badc",
3031
3033
  "test/layer-0-primitives/tls-preferred-groups.test.js": "sha256:7d3620cbde85289a98a385a3c3ba9d17f0bb2b8e5f5f9398b5641ff9d1e2b181",
@@ -3054,7 +3056,7 @@
3054
3056
  "test/layer-0-primitives/worker-pool-recycle-race.test.js": "sha256:0e9f12256864334bf49de0b0d62b9d4f9cfeb34e9fe598f9299b811dc4ac4719",
3055
3057
  "test/layer-0-primitives/worker-pool.test.js": "sha256:0a3698d862346f64f98eb1c954694ad6a6b4cedbfc569a1f4d93233b7bfa733a",
3056
3058
  "test/layer-0-primitives/worm.test.js": "sha256:0908cfbf7228de39ab68796505c29071d3117571b8a6c07cef7d1fa27d0380b8",
3057
- "test/layer-0-primitives/ws-client.test.js": "sha256:e604a0ac206825ec307624ae6df59a644c2dde2c51b965a3b51fce05990cfbde",
3059
+ "test/layer-0-primitives/ws-client.test.js": "sha256:004f5f231e7396fd8b32c7f41e7cd36becaa8919584c740497a9b08c242414c8",
3058
3060
  "test/layer-0-primitives/x509-chain-ca-enforcement.test.js": "sha256:8d68ffe4f8655af99682927524ae5a6c5336f32a57eb7c5c78fef5f2d1b2e6e3",
3059
3061
  "test/layer-1-state/api-key.test.js": "sha256:48d641b673a181746adf80dfa995f0ca29187af17aa4735590557fda4350dcb8",
3060
3062
  "test/layer-5-integration/bundler-output.test.js": "sha256:023f0504a5ae60fb39d347d05e8fe7e9d8fee685eb1bb89dd40406cf01691d22",
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.37 (2026-06-27) — **Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures.** A number of configuration options validated a numeric value with a bare `typeof === "number" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation "good" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect "retry indefinitely", inbox "retain indefinitely" — continue to accept `Infinity`. **Security:** *A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks* — b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === "number" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP "good" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`. · *WebSocket-client inbound caps can no longer be disabled by an Infinity value* — b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate "reconnect indefinitely" intent). · *Inbox, flag-cache, and audit-chain caps reject a non-finite limit* — The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely).
12
+
11
13
  - v0.15.36 (2026-06-27) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed. **Detectors:** *Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers* — Each marked bare `.split(",")` / `.split(";")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed.
12
14
 
13
15
  - v0.15.35 (2026-06-26) — **`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by.** The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{"route":"/api"}`) rather than `route=/api`. **Changed:** *shadowRegistry().snapshot() cardinality keys are canonical-JSON* — As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{"tenant":"a"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values. **Security:** *Label values can no longer forge extra Prometheus label pairs in the shadow registry* — b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.15.36",
4
- "createdAt": "2026-06-27T07:23:28.597Z",
3
+ "frameworkVersion": "0.15.37",
4
+ "createdAt": "2026-06-27T08:30:34.621Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -124,7 +124,7 @@ function create(opts) {
124
124
  var auditImpl = opts.audit || audit();
125
125
  var declaredRegimes = Object.create(null);
126
126
  for (var i = 0; i < BUILTIN_REGIMES.length; i += 1) declaredRegimes[BUILTIN_REGIMES[i]] = true;
127
- // allow:numeric-opt-Infinity — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT]; bad input falls back to default
127
+ // allow:numeric-opt-Infinity-intentional — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT] (the `<= DEFAULT_MAX_HOP_COUNT` upper bound rejects Infinity); bad input falls back to default
128
128
  var maxHopCount = typeof opts.maxHopCount === "number" && opts.maxHopCount > 0 &&
129
129
  opts.maxHopCount <= DEFAULT_MAX_HOP_COUNT
130
130
  ? Math.floor(opts.maxHopCount)
@@ -37,6 +37,7 @@ var canonicalJson = require("./canonical-json");
37
37
  var C = require("./constants");
38
38
  var clusterStorage = require("./cluster-storage");
39
39
  var frameworkSchema = require("./framework-schema");
40
+ var numericBounds = require("./numeric-bounds");
40
41
  var sql = require("./sql");
41
42
  var safeSql = require("./safe-sql");
42
43
  var safeBuffer = require("./safe-buffer");
@@ -305,7 +306,10 @@ async function verifyChain(queryAllAsync, tableName, opts) {
305
306
  // coerce so a Postgres INTEGER/BIGINT chainKey is type-stable in the
306
307
  // reported break-shape and the per-key WHERE bind, matching SQLite.
307
308
  var keyRows = frameworkSchema.coerceRows(await queryAllAsync(keysBuilt.sql, keysBuilt.params));
308
- var maxChains = (typeof opts.maxChains === "number" && opts.maxChains > 0) ? opts.maxChains : 100000; // allow:numeric-opt-Infinity — partition fan-out cap; non-number / <=0 falls back to the default
309
+ // Partition fan-out cap; a non-finite / <= 0 / non-integer value (Infinity
310
+ // would make the `keyRows.length > maxChains` cap unsatisfiable) falls back
311
+ // to the bounded default rather than disabling the cap.
312
+ var maxChains = numericBounds.isPositiveFiniteInt(opts.maxChains) ? opts.maxChains : 100000;
309
313
  if (keyRows.length > maxChains) {
310
314
  return {
311
315
  ok: false,
@@ -39,6 +39,7 @@
39
39
  var cose = require("./cose");
40
40
  var cbor = require("./cbor");
41
41
  var C = require("./constants");
42
+ var numericBounds = require("./numeric-bounds");
42
43
  var validateOpts = require("./validate-opts");
43
44
  var { defineClass } = require("./framework-error");
44
45
 
@@ -191,7 +192,11 @@ async function verify(cwt, opts) {
191
192
  }
192
193
 
193
194
  // Time claims (NumericDate, seconds). Skew tolerance both directions.
194
- var skew = (typeof opts.clockSkewSec === "number" && opts.clockSkewSec >= 0) ? opts.clockSkewSec : 60; // allow:numeric-opt-Infinity — clamped non-negative, else default / allow:raw-time-literal clock-skew in seconds (NumericDate units), not a ms duration
195
+ // A present clockSkewSec must be a non-negative finite integeran
196
+ // Infinity / NaN / negative skew would otherwise make `now > exp + skew`
197
+ // unsatisfiable and silently disable the expiry / not-before checks.
198
+ numericBounds.requireNonNegativeFiniteIntIfPresent(opts.clockSkewSec, "cwt.verify: opts.clockSkewSec", CwtError, "cwt/bad-clock-skew");
199
+ var skew = (typeof opts.clockSkewSec === "number") ? opts.clockSkewSec : 60; // allow:raw-time-literal — clock-skew in seconds (NumericDate units), not a ms duration
195
200
  var now = _nowSec(opts);
196
201
  // A present exp / nbf MUST be a well-formed NumericDate — a non-numeric
197
202
  // value would otherwise bypass the time check entirely (a token could
@@ -1338,8 +1338,9 @@ async function transaction(fn, opts) {
1338
1338
  // (serialization_failure) are transient — retry with capped attempts
1339
1339
  // and a small jittered backoff. Operators tune retries via opts.deadlockRetries (default 3).
1340
1340
  // numeric-bounds doesn't have a non-negative-int helper; use a
1341
- // direct check with allow marker (zero is permitted to disable
1342
- // retries entirely).
1341
+ // Explicit isFinite/integer guard (zero is permitted to disable retries
1342
+ // entirely); a non-finite deadlockRetries is refused here, so Math.floor
1343
+ // below only ever sees a validated finite integer.
1343
1344
  if (opts.deadlockRetries !== undefined) {
1344
1345
  if (typeof opts.deadlockRetries !== "number" || !isFinite(opts.deadlockRetries) ||
1345
1346
  opts.deadlockRetries < 0 || (opts.deadlockRetries | 0) !== opts.deadlockRetries) {
@@ -1348,7 +1349,7 @@ async function transaction(fn, opts) {
1348
1349
  }
1349
1350
  }
1350
1351
  var maxRetries = (typeof opts.deadlockRetries === "number")
1351
- ? Math.floor(opts.deadlockRetries) : 3; // allow:numeric-opt-Infinity
1352
+ ? Math.floor(opts.deadlockRetries) : 3;
1352
1353
  // Validate the transaction-level residency tag shape at entry (the
1353
1354
  // sessionGucs / deadlockRetries discipline) so an empty-string tag
1354
1355
  // fails before BEGIN rather than at the first statement.
@@ -24,6 +24,7 @@
24
24
  var validateOpts = require("./validate-opts");
25
25
  var lazyRequire = require("./lazy-require");
26
26
  var C = require("./constants");
27
+ var numericBounds = require("./numeric-bounds");
27
28
  var { defineClass } = require("./framework-error");
28
29
  var FlagError = defineClass("FlagError", { alwaysPermanent: true });
29
30
 
@@ -36,18 +37,17 @@ function cache(downstream, opts) {
36
37
  throw new FlagError("flag/bad-cache",
37
38
  "cache: downstream provider must implement .evaluate()");
38
39
  }
39
- // allow:numeric-opt-Infinity defaults clamped + ttl-floor enforced below
40
- var ttlMs = (typeof opts.ttlMs === "number" && opts.ttlMs > 0)
41
- ? opts.ttlMs
42
- : C.TIME.seconds(30);
40
+ // A present ttlMs / maxEntries must be a positive finite integer — Infinity
41
+ // would pass a bare `typeof === "number" && > 0` check and give a
42
+ // never-expiring entry (ttlMs) or an unbounded cache (maxEntries).
43
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.ttlMs, "flag.cache: opts.ttlMs", FlagError, "flag/bad-cache");
44
+ var ttlMs = (typeof opts.ttlMs === "number") ? opts.ttlMs : C.TIME.seconds(30);
43
45
  if (ttlMs < C.TIME.seconds(1)) {
44
46
  throw new FlagError("flag/bad-cache",
45
47
  "cache: ttlMs must be >= 1000ms - got " + ttlMs);
46
48
  }
47
- // allow:numeric-opt-Infinity maxEntries default + Math.floor coerce; throws on bad type at config time
48
- var maxEntries = (typeof opts.maxEntries === "number" && opts.maxEntries > 0)
49
- ? Math.floor(opts.maxEntries)
50
- : 10000; // entry-count default
49
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxEntries, "flag.cache: opts.maxEntries", FlagError, "flag/bad-cache");
50
+ var maxEntries = (typeof opts.maxEntries === "number") ? opts.maxEntries : 10000; // entry-count default
51
51
  var auditOn = opts.audit === true; // off by default — too chatty
52
52
  var entries = new Map();
53
53
  var hits = 0;
@@ -44,6 +44,7 @@
44
44
 
45
45
  var C = require("./constants");
46
46
  var codepointClass = require("./codepoint-class");
47
+ var numericBounds = require("./numeric-bounds");
47
48
  var lazyRequire = require("./lazy-require");
48
49
  var safeJson = require("./safe-json");
49
50
  var safeSql = require("./safe-sql");
@@ -155,15 +156,17 @@ function create(opts) {
155
156
  // The table identifier reaches SQL through b.sql, which validates +
156
157
  // quotes it by construction on every emitted statement; _validateTableName
157
158
  // above fails fast at create() time on a bad name.
158
- var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity
159
+ var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity-intentional — Infinity = retain indefinitely, a supported intent
159
160
  ? opts.retentionDays : 30; // default retention days
160
161
  var auditOn = opts.audit !== false;
161
- var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number" && opts.maxPayloadBytes > 0) // allow:numeric-opt-Infinity
162
- ? opts.maxPayloadBytes : C.BYTES.kib(64);
163
- var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number" && opts.messageIdMaxLen > 0) // allow:numeric-opt-Infinity
164
- ? opts.messageIdMaxLen : 256; // message-id length cap
165
- var sourceMaxLen = (typeof opts.sourceMaxLen === "number" && opts.sourceMaxLen > 0) // allow:numeric-opt-Infinity
166
- ? opts.sourceMaxLen : 256; // source length cap
162
+ // The byte / length caps must be positive finite integers Infinity would
163
+ // disable the cap and admit unbounded stored payloads / identifiers.
164
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
165
+ ["maxPayloadBytes", "messageIdMaxLen", "sourceMaxLen"],
166
+ "inbox.create", InboxError, "inbox/bad-opt");
167
+ var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number") ? opts.maxPayloadBytes : C.BYTES.kib(64);
168
+ var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number") ? opts.messageIdMaxLen : 256; // message-id length cap
169
+ var sourceMaxLen = (typeof opts.sourceMaxLen === "number") ? opts.sourceMaxLen : 256; // source length cap
167
170
 
168
171
  function _emitAudit(action, outcome, metadata) {
169
172
  if (!auditOn) return;
@@ -346,6 +349,14 @@ function create(opts) {
346
349
  }
347
350
 
348
351
  async function sweep() {
352
+ // retentionDays: Infinity is the documented "retain indefinitely" intent —
353
+ // there is no horizon to age past, so sweep deletes nothing. (Computing a
354
+ // cutoff would otherwise throw: `new Date(Date.now() - Infinity)` is an
355
+ // Invalid Date, and "Infinity days" is not a valid Postgres interval.)
356
+ if (!isFinite(retentionDays)) {
357
+ _emitAudit("inbox.swept", "success", { deleted: 0, retentionDays: retentionDays });
358
+ return 0;
359
+ }
349
360
  var dialect = _sqlDialect(externalDb);
350
361
  var deleted = 0;
351
362
  await externalDb.transaction(async function (xdb) {
@@ -51,6 +51,7 @@
51
51
  var nodeCrypto = require("node:crypto");
52
52
  var lazyRequire = require("./lazy-require");
53
53
  var validateOpts = require("./validate-opts");
54
+ var numericBounds = require("./numeric-bounds");
54
55
  var safeBuffer = require("./safe-buffer");
55
56
  var dkim = require("./mail-dkim");
56
57
  var { defineClass } = require("./framework-error");
@@ -246,8 +247,11 @@ function sign(opts) {
246
247
  "sign: headersToSign[" + hi + "] must be a non-empty string");
247
248
  }
248
249
  }
249
- var timestamp = (typeof opts.timestamp === "number" && opts.timestamp > 0) // allow:numeric-opt-Infinity
250
- ? Math.floor(opts.timestamp) : Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
250
+ // A present t= timestamp must be a positive finite integer (NumericDate
251
+ // seconds) an Infinity / NaN value would serialize into a malformed t= tag.
252
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.timestamp, "arc.sign: opts.timestamp", MailAuthError, "arc-sign/bad-timestamp");
253
+ var timestamp = (typeof opts.timestamp === "number")
254
+ ? opts.timestamp : Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
251
255
  var auditOn = opts.audit !== false;
252
256
 
253
257
  var keyObject;
@@ -52,6 +52,7 @@ var structuredFields = require("./structured-fields");
52
52
  var markupEscape = require("./markup-escape").markupEscape;
53
53
  var bCrypto = require("./crypto");
54
54
  var C = require("./constants");
55
+ var numericBounds = require("./numeric-bounds");
55
56
  var dkim = require("./mail-dkim");
56
57
  var mimeParse = require("./mime-parse");
57
58
  var safeXml = require("./parsers/safe-xml");
@@ -1575,7 +1576,11 @@ async function arcVerify(rfc822, opts) {
1575
1576
  var anyFail = false;
1576
1577
  // RFC 8617 §5.2 — operator-tunable clock skew on t= (signing
1577
1578
  // timestamp) and x= (expiration) tags. Default 5 min.
1578
- var arcClockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity operator-supplied skew, default 5 min
1579
+ // A present clockSkewMs must be a non-negative finite integer; an Infinity /
1580
+ // NaN / negative skew makes `amsX + skewSec < nowSec` (and the t= future
1581
+ // check) unsatisfiable and silently disables the RFC 8617 §5.2 expiry /
1582
+ // future-timestamp gate. Non-finite falls to the default.
1583
+ var arcClockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
1579
1584
  ? opts.clockSkewMs : C.TIME.minutes(5);
1580
1585
  var nowSec = Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
1581
1586
 
@@ -101,9 +101,9 @@ function create(opts) {
101
101
  "middleware.ageGate: opts.getAge must be a function (req) -> number | null");
102
102
  }
103
103
  var getAge = opts.getAge;
104
- var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity — age is operator domain, not a bytes/time-shaped opt
104
+ var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (denies everyone), never a bypass
105
105
  ? opts.requireAge : null;
106
- var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity — age threshold, not a bytes/time-shaped opt
106
+ var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (classifies everyone below-threshold), never a bypass
107
107
  ? opts.consentRequired : null;
108
108
  var hasParentalConsent = typeof opts.hasParentalConsent === "function" ? opts.hasParentalConsent : null;
109
109
  var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
@@ -1204,7 +1204,11 @@ function evaluateOcspResponse(ocspDer, opts) {
1204
1204
  // gets revoked, the attacker keeps presenting the cached "good" and
1205
1205
  // the framework keeps accepting it. requireGood postures depend on
1206
1206
  // freshness — reject expired or future-dated responses outright.
1207
- var clockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity operator-supplied skew, default 5 min if absent or invalid
1207
+ // A present clockSkewMs must be a non-negative finite integer; an Infinity /
1208
+ // NaN / negative skew would make the staleness check `now > nextUpdate + skew`
1209
+ // unsatisfiable and silently disable the freshness window — accepting a
1210
+ // replayed pre-revocation "good" response. Non-finite falls to the default.
1211
+ var clockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
1208
1212
  ? opts.clockSkewMs : C.TIME.minutes(5);
1209
1213
  var now = typeof opts.now === "number" ? opts.now : Date.now();
1210
1214
  // thisUpdate / nextUpdate are already unix-ms NUMBERS (parseOcspResponse →
@@ -52,6 +52,7 @@ var { EventEmitter } = require("node:events");
52
52
 
53
53
  var lazyRequire = require("./lazy-require");
54
54
  var validateOpts = require("./validate-opts");
55
+ var numericBounds = require("./numeric-bounds");
55
56
  var safeAsync = require("./safe-async");
56
57
  var safeBuffer = require("./safe-buffer");
57
58
  var bCrypto = lazyRequire(function () { return require("./crypto"); });
@@ -217,16 +218,18 @@ function connect(target, opts) {
217
218
  "wsClient.connect: subprotocols[" + sp + "] must be a non-empty string");
218
219
  }
219
220
  }
220
- var pingMs = (typeof opts.pingMs === "number" && opts.pingMs > 0) // allow:numeric-opt-Infinity
221
- ? opts.pingMs : DEFAULT_PING_MS;
222
- var pongMs = (typeof opts.pongMs === "number" && opts.pongMs > 0) // allow:numeric-opt-Infinity
223
- ? opts.pongMs : DEFAULT_PONG_MS;
224
- var maxMessageBytes = (typeof opts.maxMessageBytes === "number" && opts.maxMessageBytes > 0) // allow:numeric-opt-Infinity
225
- ? opts.maxMessageBytes : DEFAULT_MAX_BYTES;
226
- var maxFrameBytes = (typeof opts.maxFrameBytes === "number" && opts.maxFrameBytes > 0) // allow:numeric-opt-Infinity
227
- ? opts.maxFrameBytes : DEFAULT_MAX_FRAME;
228
- var handshakeTimeoutMs = (typeof opts.handshakeTimeoutMs === "number" && opts.handshakeTimeoutMs > 0) // allow:numeric-opt-Infinity
229
- ? opts.handshakeTimeoutMs : DEFAULT_HANDSHAKE_TIMEOUT_MS;
221
+ // These are keepalive/timeout intervals and inbound-OOM caps; a present
222
+ // value must be a positive finite integer. A bare `typeof === "number" && > 0`
223
+ // check accepts Infinity, which silently disables the cap (a malicious server
224
+ // could then send an unbounded message/frame, or stall the handshake forever).
225
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
226
+ ["pingMs", "pongMs", "maxMessageBytes", "maxFrameBytes", "handshakeTimeoutMs"],
227
+ "wsClient.connect", WsClientError, "ws-client/bad-opt");
228
+ var pingMs = (typeof opts.pingMs === "number") ? opts.pingMs : DEFAULT_PING_MS;
229
+ var pongMs = (typeof opts.pongMs === "number") ? opts.pongMs : DEFAULT_PONG_MS;
230
+ var maxMessageBytes = (typeof opts.maxMessageBytes === "number") ? opts.maxMessageBytes : DEFAULT_MAX_BYTES;
231
+ var maxFrameBytes = (typeof opts.maxFrameBytes === "number") ? opts.maxFrameBytes : DEFAULT_MAX_FRAME;
232
+ var handshakeTimeoutMs = (typeof opts.handshakeTimeoutMs === "number") ? opts.handshakeTimeoutMs : DEFAULT_HANDSHAKE_TIMEOUT_MS;
230
233
 
231
234
  var reconnectOpts = _normaliseReconnect(opts.reconnect);
232
235
  var permessageDeflate = opts.permessageDeflate !== false;
@@ -281,14 +284,18 @@ function _normaliseReconnect(input) {
281
284
  "wsClient.connect: reconnect must be false / null / object");
282
285
  }
283
286
  validateOpts(input, ["maxAttempts", "baseMs", "maxMs", "enabled"], "wsClient.connect.reconnect");
287
+ // baseMs / maxMs are backoff durations; a present value must be a positive
288
+ // finite integer (an Infinity base would stall the first reconnect forever).
289
+ numericBounds.requireAllPositiveFiniteIntIfPresent(input, ["baseMs", "maxMs"],
290
+ "wsClient.connect.reconnect", WsClientError, "ws-client/bad-reconnect-opt");
284
291
  return {
285
292
  enabled: input.enabled !== false,
286
- maxAttempts: (typeof input.maxAttempts === "number" && input.maxAttempts >= 0) // allow:numeric-opt-Infinity
293
+ // maxAttempts: Infinity is a deliberate "reconnect indefinitely" intent, so
294
+ // a non-finite value is accepted here (unlike the bounded caps above).
295
+ maxAttempts: (typeof input.maxAttempts === "number" && input.maxAttempts >= 0) // allow:numeric-opt-Infinity-intentional — Infinity = reconnect indefinitely, a supported intent
287
296
  ? input.maxAttempts : DEFAULT_RECONNECT_MAX_ATTEMPTS,
288
- baseMs: (typeof input.baseMs === "number" && input.baseMs > 0) // allow:numeric-opt-Infinity
289
- ? input.baseMs : DEFAULT_RECONNECT_BASE_MS,
290
- maxMs: (typeof input.maxMs === "number" && input.maxMs > 0) // allow:numeric-opt-Infinity
291
- ? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
297
+ baseMs: (typeof input.baseMs === "number") ? input.baseMs : DEFAULT_RECONNECT_BASE_MS,
298
+ maxMs: (typeof input.maxMs === "number") ? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
292
299
  };
293
300
  }
294
301
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.36",
3
+ "version": "0.15.37",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.15.37",
4
+ "date": "2026-06-27",
5
+ "headline": "Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures",
6
+ "summary": "A number of configuration options validated a numeric value with a bare `typeof === \"number\" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation \"good\" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect \"retry indefinitely\", inbox \"retain indefinitely\" — continue to accept `Infinity`.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks",
13
+ "body": "b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === \"number\" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP \"good\" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`."
14
+ },
15
+ {
16
+ "title": "WebSocket-client inbound caps can no longer be disabled by an Infinity value",
17
+ "body": "b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate \"reconnect indefinitely\" intent)."
18
+ },
19
+ {
20
+ "title": "Inbox, flag-cache, and audit-chain caps reject a non-finite limit",
21
+ "body": "The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely)."
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
@@ -340,7 +340,7 @@ var VALID_ALLOW_CLASSES = {
340
340
  "list-without-pagination": 1,
341
341
  "math-random-noncrypto-jitter-sampling": 1,
342
342
  "no-number-money-arithmetic": 1,
343
- "numeric-opt-Infinity": 1,
343
+ "numeric-opt-Infinity-intentional": 1,
344
344
  "primitive-unreachable": 1,
345
345
  "process-exit-operator-optin": 1,
346
346
  "raw-byte-literal": 1,
@@ -390,6 +390,7 @@ var RETIRED_ALLOW_TOKENS = {
390
390
  "raw-hash-compare": "renamed to 'raw-hash-compare-nonsecret-tag' (2026-06-26 re-verify pass) — the one site compares a data-residency region tag with ===, not a secret hash; re-verify before reusing",
391
391
  "seal-without-aad": "renamed to 'seal-without-aad-by-design' (2026-06-26 re-verify pass) — both sites intentionally seal without AAD (a non-regulated plain-mode table whose AAD is enforced by the posture sealEnvelopeFloor where required, and a throwaway vault-readiness probe sentinel); re-verify before reusing",
392
392
  "bare-split-on-quoted-header": "renamed to 'bare-split-on-quoted-header-token-grammar' (2026-06-26 re-verify pass) — every live marker splits an RFC token-only header grammar with no quoted-string members (RRULE / RFC 9421 component-ids / TLS-RPT rua / SCIM attribute paths), so a bare comma/semicolon split is correct; re-verify the grammar before reusing. The re-verify also found 5 markers were inert (detector pre-filter skips their file, or the line is a .replace not a split) and cleared them",
393
+ "numeric-opt-Infinity": "renamed to 'numeric-opt-Infinity-intentional' (2026-06-27 re-verify pass) — only the markers where Infinity is genuinely SAFE remain: clamped to a max (agent-posture-chain maxHopCount), fail-closed (age-gate requireAge / consentRequired), or a deliberate unbounded intent (inbox retentionDays = retain indefinitely, ws-client reconnect maxAttempts = retry forever). The re-verify found 13 sites where Infinity SILENTLY DISABLED a cap or security check (clock-skew → expiry/OCSP-freshness/ARC-expiry; ws-client maxMessageBytes/maxFrameBytes/handshakeTimeoutMs; inbox/flag-cache/audit-chain caps; mail-arc-sign timestamp) — those now route through numeric-bounds (throw / safe-default on non-finite) and lost the marker. re-verify each remaining site before reusing",
393
394
  };
394
395
 
395
396
  function testNoRetiredAllowTokenReRegistered() {
@@ -694,13 +695,13 @@ function testNoRawTimeLiterals() {
694
695
  }
695
696
 
696
697
  function testNumericOptsValidate() {
697
- // class: numeric-opt-Infinity
698
+ // class: numeric-opt-Infinity-intentional
698
699
  var matches = _scan(
699
700
  /typeof\s+opts\.\w+\s*===\s*"number"\s*&&\s*opts\.\w+\s*>=?\s*0\b/);
700
701
  // numeric-bounds itself contains the helper definitions that look
701
702
  // like the pattern.
702
703
  matches = matches.filter(function (m) { return m.file !== "lib/numeric-bounds.js"; });
703
- matches = _filterMarkers(matches, "numeric-opt-Infinity");
704
+ matches = _filterMarkers(matches, "numeric-opt-Infinity-intentional");
704
705
  _report("numeric opts route through lib/numeric-bounds (or have an allow marker)",
705
706
  matches);
706
707
  }
@@ -105,6 +105,34 @@ async function testRobustTagAndMalformedClaims() {
105
105
  check("verify: malformed (non-numeric) nbf refused", e2 && e2.code === "cwt/malformed-claim");
106
106
  }
107
107
 
108
+ async function testClockSkewInfinityRejected() {
109
+ // A non-finite clockSkewSec must be a config error, NOT a silent
110
+ // expiry-disable: the check is `now > exp + skew`, so skew === Infinity
111
+ // makes it `now > Infinity` (always false) and ANY expired token would
112
+ // verify. A present-but-invalid skew (Infinity / NaN / negative /
113
+ // non-integer) is refused at the entry point.
114
+ var now = _now();
115
+ var expired = await b.cwt.sign({ exp: now - 100000 }, { alg: "ES256", privateKey: EC.privateKey });
116
+ var errInf = null;
117
+ try {
118
+ await b.cwt.verify(expired, { algorithms: ["ES256"], publicKey: EC.publicKey, clockSkewSec: Infinity });
119
+ } catch (e) { errInf = e; }
120
+ check("verify: clockSkewSec Infinity refused (does not disable expiry)",
121
+ errInf && errInf.code === "cwt/bad-clock-skew");
122
+
123
+ var errNeg = null;
124
+ try {
125
+ await b.cwt.verify(expired, { algorithms: ["ES256"], publicKey: EC.publicKey, clockSkewSec: -1 });
126
+ } catch (e) { errNeg = e; }
127
+ check("verify: negative clockSkewSec refused", errNeg && errNeg.code === "cwt/bad-clock-skew");
128
+
129
+ // Absent skew still applies the default (marginally-expired tolerated),
130
+ // and a valid finite skew still works — the fix must not regress those.
131
+ var justExpired = await b.cwt.sign({ exp: now - 30 }, { alg: "ES256", privateKey: EC.privateKey });
132
+ var ok = await b.cwt.verify(justExpired, { algorithms: ["ES256"], publicKey: EC.publicKey, clockSkewSec: 60 });
133
+ check("verify: a valid finite skew still tolerates a marginally-expired token", ok.claims.exp === now - 30);
134
+ }
135
+
108
136
  async function testValidation() {
109
137
  var bad = null;
110
138
  try { await b.cwt.sign({ exp: "not-a-number" }, { alg: "ES256", privateKey: EC.privateKey }); } catch (e) { bad = e; }
@@ -124,6 +152,7 @@ async function run() {
124
152
  await testIssuerAudience();
125
153
  await testTaggedAndTamper();
126
154
  await testRobustTagAndMalformedClaims();
155
+ await testClockSkewInfinityRejected();
127
156
  await testValidation();
128
157
  }
129
158
 
@@ -496,6 +496,12 @@ function run() {
496
496
  function () { b.flag.cache({}); }, /must implement/);
497
497
  rejects("cache: ttlMs too small",
498
498
  function () { b.flag.cache(memProvider, { ttlMs: 100 }); }, /ttlMs/);
499
+ // A non-finite ttlMs / maxEntries must be refused (Infinity would otherwise
500
+ // give a never-expiring entry / an unbounded cache, not be clamped).
501
+ rejects("cache: maxEntries Infinity refused (unbounded cache)",
502
+ function () { b.flag.cache(memProvider, { ttlMs: 5000, maxEntries: Infinity }); }, /maxEntries|finite/);
503
+ rejects("cache: ttlMs Infinity refused",
504
+ function () { b.flag.cache(memProvider, { ttlMs: Infinity }); }, /ttlMs|finite/);
499
505
 
500
506
  var cachedProvider = b.flag.cache(memProvider, { ttlMs: 5000, maxEntries: 50 });
501
507
  check("cache: kind", cachedProvider.kind === "cache:memory");
@@ -118,6 +118,12 @@ async function run() {
118
118
  var fake = _makeFakeExternalDb();
119
119
  rejects("inbox.create: bad table name",
120
120
  function () { b.inbox.create({ externalDb: fake.db, table: "bad-name" }); }, /not a safe SQL identifier/);
121
+ // A non-finite byte/length cap must be refused — Infinity would disable the
122
+ // cap and admit unbounded stored payloads / identifiers.
123
+ rejects("inbox.create: maxPayloadBytes Infinity refused",
124
+ function () { b.inbox.create({ externalDb: fake.db, table: "t", maxPayloadBytes: Infinity }); }, /maxPayloadBytes|finite/);
125
+ rejects("inbox.create: messageIdMaxLen Infinity refused",
126
+ function () { b.inbox.create({ externalDb: fake.db, table: "t", messageIdMaxLen: Infinity }); }, /messageIdMaxLen|finite/);
121
127
 
122
128
  var inbox = b.inbox.create({
123
129
  externalDb: fake.db,
@@ -182,6 +188,16 @@ async function run() {
182
188
 
183
189
  var deleted = await inbox.sweep();
184
190
  check("inbox.sweep: returns deleted count", typeof deleted === "number");
191
+
192
+ // retentionDays: Infinity means "retain indefinitely" — sweep() must be a
193
+ // no-op (delete nothing), not compute an invalid horizon. Before the fix the
194
+ // sqlite path threw RangeError on `new Date(now - Infinity).toISOString()`.
195
+ var foreverInbox = b.inbox.create({
196
+ externalDb: fake.db, table: "forever_inbox", audit: false, retentionDays: Infinity,
197
+ });
198
+ await foreverInbox.declareSchema(fake.xdb);
199
+ var foreverDeleted = await foreverInbox.sweep();
200
+ check("inbox.sweep: retentionDays Infinity retains all (no-op, no throw)", foreverDeleted === 0);
185
201
  }
186
202
 
187
203
  module.exports = { run: run };
@@ -367,6 +367,36 @@ async function testArcVerifyBadSignatures() {
367
367
  rv.hops[0].asResult !== "pass");
368
368
  }
369
369
 
370
+ async function testArcInfinityClockSkewDoesNotDisableExpiry() {
371
+ // RFC 8617 §5.2 — a past x= (expiration) MUST be rejected with operator
372
+ // skew tolerance. The expiry gate is `amsX + skewSec < nowSec`; a non-finite
373
+ // clockSkewMs makes skewSec === Infinity, so `amsX + Infinity < nowSec` is
374
+ // always false and the expiry check is silently disabled. A present
375
+ // non-finite skew must fall back to the safe default instead. RED before the
376
+ // fix: the expired x= slips through and amsErrors carries a signature error,
377
+ // not the x-expired fault.
378
+ var nowSec = Math.floor(Date.now() / 1000);
379
+ var expiredX = nowSec - 86400; // expired a day ago
380
+ var pastT = nowSec - 90000;
381
+ var msg = "ARC-Seal: i=1; a=rsa-sha256; cv=none; d=example.com; s=arc; b=AAAA\r\n" +
382
+ "ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=arc; t=" + pastT + "; x=" + expiredX + "; bh=AAAA; h=from; b=AAAA\r\n" +
383
+ "ARC-Authentication-Results: i=1; example.com; spf=pass\r\n" +
384
+ "From: alice@example.com\r\nTo: bob@example.com\r\n\r\nbody\r\n";
385
+ var dnsLookup = async function (qname) {
386
+ if (qname === "arc._domainkey.example.com") {
387
+ var nodeCrypto = require("crypto");
388
+ var pair = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
389
+ var spki = pair.publicKey.export({ type: "spki", format: "der" });
390
+ return [["v=DKIM1; k=rsa; p=" + spki.toString("base64")]];
391
+ }
392
+ var err = new Error("ENOTFOUND"); err.code = "ENOTFOUND"; throw err;
393
+ };
394
+ var rv = await b.mail.arc.verify(msg, { dnsLookup: dnsLookup, clockSkewMs: Infinity });
395
+ var amsErrs = ((rv.hops && rv.hops[0] && rv.hops[0].amsErrors) || []).join(" ; ");
396
+ check("ARC Infinity skew: expired x= still enforced (expiry not disabled)",
397
+ /x-expired/.test(amsErrs));
398
+ }
399
+
370
400
  function _arcHopHeaders(i, cv) {
371
401
  // Synthetic ARC headers — signatures are dummy; the cv= edge tests
372
402
  // exercise the chain-rule validator, not the signature verifier.
@@ -615,6 +645,10 @@ function testArcSignChain() {
615
645
  _rejects("arc.sign: bad instance (0)",
616
646
  function () { b.mail.arc.sign({ rfc822: rfc822, instance: 0, authservId: "x", domain: "x", selector: "x", privateKey: arcKeyPem, algorithm: "rsa-sha256", cv: "none", authResults: "spf=pass" }); },
617
647
  /instance must be/);
648
+ // A non-finite t= timestamp must be refused, not floored into a malformed tag.
649
+ _rejects("arc.sign: timestamp Infinity refused",
650
+ function () { b.mail.arc.sign({ rfc822: rfc822, instance: 1, authservId: "x", domain: "x", selector: "x", privateKey: arcKeyPem, algorithm: "rsa-sha256", cv: "none", authResults: "spf=pass", timestamp: Infinity }); },
651
+ /timestamp|finite/);
618
652
  }
619
653
 
620
654
  // ---- DMARCbis (B1) — psd= / np= / org-domain via PSL ----
@@ -1474,6 +1508,7 @@ async function run() {
1474
1508
  await testArcVerifyMissing();
1475
1509
  await testArcVerifyNone();
1476
1510
  await testArcVerifyBadSignatures();
1511
+ await testArcInfinityClockSkewDoesNotDisableExpiry();
1477
1512
  await testArcVerifyDuplicateInstance();
1478
1513
  await testArcVerifyNonContiguous();
1479
1514
  await testArcVerifyTooManyHops();
@@ -175,11 +175,39 @@ function testAcceptsFreshNoNextUpdate() {
175
175
  check("no-nextUpdate fresh: certStatus good", rv.certStatus === "good");
176
176
  }
177
177
 
178
+ // A non-finite clockSkewMs must NOT disable the freshness window. The stale
179
+ // check is `now > nextUpdate + skew`; skew === Infinity makes it `now >
180
+ // Infinity` (always false), so a STALE (past-nextUpdate) response — exactly
181
+ // the pre-revocation "good" reply an attacker replays after the cert is
182
+ // revoked — would be accepted. A present-but-non-finite skew falls back to
183
+ // the safe default instead of being honored. RED before the fix: rv.ok===true.
184
+ function testInfinityClockSkewDoesNotDisableFreshness() {
185
+ var fx = _buildSignedOcsp({
186
+ thisUpdateMs: FIXED_NOW - 3 * 86400000, // 3 days ago
187
+ nextUpdateMs: FIXED_NOW - 2 * 86400000, // 2 days ago → STALE
188
+ });
189
+ var rv = b.network.tls.ocsp.evaluate(fx.der, {
190
+ issuerPem: fx.issuerPem, serialHex: fx.serialHex, now: FIXED_NOW,
191
+ clockSkewMs: Infinity,
192
+ });
193
+ check("Infinity skew: stale response still rejected (freshness not disabled)", rv.ok === false);
194
+ check("Infinity skew: rejected for the STALE reason",
195
+ /nextUpdate|stale/i.test((rv.errors || []).join(" ; ")));
196
+ // A negative skew is likewise treated as invalid → safe default, so the
197
+ // stale response stays rejected.
198
+ var rvNeg = b.network.tls.ocsp.evaluate(fx.der, {
199
+ issuerPem: fx.issuerPem, serialHex: fx.serialHex, now: FIXED_NOW,
200
+ clockSkewMs: -1,
201
+ });
202
+ check("negative skew: stale response still rejected", rvNeg.ok === false);
203
+ }
204
+
178
205
  async function run() {
179
206
  testRejectsStaleResponse();
180
207
  testAcceptsFreshResponse();
181
208
  testRejectsFutureThisUpdate();
182
209
  testAcceptsFreshNoNextUpdate();
210
+ testInfinityClockSkewDoesNotDisableFreshness();
183
211
  }
184
212
 
185
213
  module.exports = { run: run };
@@ -205,6 +205,22 @@ async function _runTests() {
205
205
  function () { b.wsClient.connect("ws://localhost:1", { reconnect: 42 }); },
206
206
  /reconnect must be/);
207
207
 
208
+ // ---- non-finite resource caps ----
209
+ // maxMessageBytes / maxFrameBytes / handshakeTimeoutMs are inbound-OOM and
210
+ // hang defenses. An Infinity value passes a bare `typeof === "number" && > 0`
211
+ // check and silently DISABLES the cap (a malicious server can then send an
212
+ // unbounded message / frame, or stall the handshake forever). A present
213
+ // non-finite value is refused at connect time.
214
+ rejects("connect: maxMessageBytes Infinity refused (OOM cap not disabled)",
215
+ function () { b.wsClient.connect("ws://localhost:1", { maxMessageBytes: Infinity }); },
216
+ /maxMessageBytes|finite/);
217
+ rejects("connect: maxFrameBytes Infinity refused",
218
+ function () { b.wsClient.connect("ws://localhost:1", { maxFrameBytes: Infinity }); },
219
+ /maxFrameBytes|finite/);
220
+ rejects("connect: handshakeTimeoutMs Infinity refused",
221
+ function () { b.wsClient.connect("ws://localhost:1", { handshakeTimeoutMs: Infinity }); },
222
+ /handshakeTimeoutMs|finite/);
223
+
208
224
  // ---- happy path: connect + send + echo ----
209
225
  var server = await _makeServer({});
210
226
  var port = server.address().port;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {