@blamejs/blamejs-shop 0.4.107 → 0.4.109
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +31 -27
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/lib/asyncapi.js +13 -13
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -5
- package/lib/vendor/blamejs/lib/dsr.js +16 -4
- package/lib/vendor/blamejs/lib/network-dns.js +28 -1
- package/lib/vendor/blamejs/lib/network-tls.js +21 -1
- package/lib/vendor/blamejs/lib/openapi.js +13 -13
- package/lib/vendor/blamejs/lib/safe-json.js +56 -0
- package/lib/vendor/blamejs/lib/sandbox.js +18 -6
- package/lib/vendor/blamejs/lib/vex.js +6 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.28.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.15.29.json +27 -0
- package/lib/vendor/blamejs/test/00-primitives.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/access-lock.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +10 -10
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-eaa.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-mtls.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.109 (2026-06-26) — **Vendored framework refreshed to 0.15.29.** Updates the vendored blamejs framework to 0.15.29, an upstream security-hardening release. Upstream now routes the parsing of operator- and untrusted-supplied JSON through the framework's safe-JSON primitive — which strips prototype-pollution keys ("__proto__" and friends) and bounds parse depth, key count, and byte size — instead of a raw JSON.parse, across the OpenAPI and AsyncAPI document parsers, the sandbox result decoder, and the internal sealed-store and VEX parses. This storefront consumes those surfaces through the vendored framework, so the hardening applies on upgrade with no API or configuration change and no operator action required. Valid input parses identically; a "__proto__"-laden or oversized payload is defused. **Changed:** *Vendored framework refreshed to 0.15.29* — Refreshes the vendored blamejs framework to 0.15.29. Upstream routes operator- and untrusted-supplied JSON through the safe-JSON primitive (prototype-pollution-key strip + depth / key / size caps, fail-closed) rather than a raw JSON.parse — in the OpenAPI / AsyncAPI parsers, the sandbox result decoder, and the internal sealed-store and VEX parses — and adds a parse-string-or-object helper so a raw JSON.parse on operator input can't be reintroduced one call site at a time. Valid documents are unaffected; a prototype-pollution-laden or oversized payload is now stripped and bounded. No runtime API change and no operator action required.
|
|
12
|
+
|
|
13
|
+
- v0.4.108 (2026-06-26) — **Vendored framework refreshed to 0.15.28.** Updates the vendored blamejs framework to 0.15.28. The upstream change corrects the terminal-vs-transient retry signal (err.permanent) on the framework's DNS and TLS network errors, so the vendored HTTP client's internal retry behavior only re-attempts failures a retry can actually fix and stops retrying hopeless ones. This storefront consumes those network primitives only transitively through the vendored HTTP client (payment-provider and outbound-webhook calls), so the improvement applies automatically with no API or configuration change and no operator action required. **Changed:** *Vendored framework refreshed to 0.15.28* — Refreshes the vendored blamejs framework to 0.15.28. Upstream, the DNS and TLS network errors now expose a correct err.permanent flag (fail-closed: an unknown code is treated as permanent), so a retry loop re-attempts only transient network round-trip failures and never spins on a permanent one — while a trust-verification failure stays terminal and is never silently retried. This storefront uses these primitives only through the vendored HTTP client, so the better retry behavior applies transitively to payment-provider and outbound-webhook calls; there is no API change and no operator action required.
|
|
14
|
+
|
|
11
15
|
- v0.4.107 (2026-06-25) — **Vendored framework refreshed to 0.15.27.** Updates the vendored blamejs framework to 0.15.27. The 0.15.27 release is an upstream test-harness reliability change only — its published library is byte-for-byte identical to 0.15.26, with no runtime API or behavior change. No operator action required. **Changed:** *Vendored framework refreshed to 0.15.27* — Refreshes the vendored blamejs framework to 0.15.27, an upstream release whose shipped library is byte-for-byte identical to 0.15.26 (the change is to the framework's own test harness). There are no runtime API changes and no operator-visible behavior change; no action is required.
|
|
12
16
|
|
|
13
17
|
- v0.4.106 (2026-06-25) — **A capped discount rule's redemption counter no longer drifts toward its limit after a transient database error mid-claim.** Claiming a redemption against a discount rule with a total-redemption cap reserves a slot by incrementing the rule's used-count, then writes the per-order claim row. If writing that claim row failed for a reason other than the expected duplicate-claim collision — a transient backend error — the reserved slot was not returned, so the rule's used-count drifted one higher than the claims actually made and a capped rule could reach its limit early and start refusing legitimate redemptions. The claim path already returns the reservation when it reuses an existing claim and when the per-customer cap refuses; it now also returns it on this transient-failure path. The return is safe and cannot over-grant: the code re-reads the claim by its unique key and only returns the slot when that read confirms no claim row was written, so there is no case where a slot is returned for a claim that actually landed. **Fixed:** *A failed redemption claim returns its reserved cap slot instead of leaking it* — A redemption claim against a total-capped discount rule increments the rule's used-count to reserve a slot before inserting the per-order claim row. A failure inserting that row that was not the expected duplicate-claim collision (a transient backend error) left the reserved slot un-returned, drifting the used-count above the real claim count so the rule could hit its cap and refuse valid redemptions prematurely. The claim path now returns the reserved slot on that path too, matching the existing reuse and per-customer-cap refusals. It is safe against over-granting because it returns the slot only after a re-read on the claim's unique key confirms no row was written — a claim that actually landed is detected and reused, never double-counted.
|
package/lib/asset-manifest.json
CHANGED
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -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.
|
|
7
|
-
"tag": "v0.15.
|
|
6
|
+
"version": "0.15.29",
|
|
7
|
+
"tag": "v0.15.29",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -849,6 +849,8 @@
|
|
|
849
849
|
"release-notes/v0.15.25.json": "lib/vendor/blamejs/release-notes/v0.15.25.json",
|
|
850
850
|
"release-notes/v0.15.26.json": "lib/vendor/blamejs/release-notes/v0.15.26.json",
|
|
851
851
|
"release-notes/v0.15.27.json": "lib/vendor/blamejs/release-notes/v0.15.27.json",
|
|
852
|
+
"release-notes/v0.15.28.json": "lib/vendor/blamejs/release-notes/v0.15.28.json",
|
|
853
|
+
"release-notes/v0.15.29.json": "lib/vendor/blamejs/release-notes/v0.15.29.json",
|
|
852
854
|
"release-notes/v0.15.3.json": "lib/vendor/blamejs/release-notes/v0.15.3.json",
|
|
853
855
|
"release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
|
|
854
856
|
"release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
|
|
@@ -1527,7 +1529,7 @@
|
|
|
1527
1529
|
"test/smoke.js": "lib/vendor/blamejs/test/smoke.js"
|
|
1528
1530
|
},
|
|
1529
1531
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
1530
|
-
"bundledAt": "2026-06-
|
|
1532
|
+
"bundledAt": "2026-06-26",
|
|
1531
1533
|
"hashes": {
|
|
1532
1534
|
".clusterfuzzlite/Dockerfile": "sha256:1eed98fcc49172efc1c1a7740dba20ce6cfcb3830cccd9f28cf114b4b39794f3",
|
|
1533
1535
|
".clusterfuzzlite/build.sh": "sha256:b9f5a627272c65f6569fa20dcbb32c383ca3a1183ce2562d481408a2fbaf9881",
|
|
@@ -1558,7 +1560,7 @@
|
|
|
1558
1560
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1559
1561
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1560
1562
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1561
|
-
"CHANGELOG.md": "sha256:
|
|
1563
|
+
"CHANGELOG.md": "sha256:12cbd8cd887ee507d270a0be83d2e5572cb034eaf7d47ff9ddaf69322b57a817",
|
|
1562
1564
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1563
1565
|
"CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
|
|
1564
1566
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
@@ -1568,7 +1570,7 @@
|
|
|
1568
1570
|
"NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
|
|
1569
1571
|
"README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
|
|
1570
1572
|
"SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
|
|
1571
|
-
"api-snapshot.json": "sha256:
|
|
1573
|
+
"api-snapshot.json": "sha256:58e5c4106b1fd4f10e1b838057abc26b4f28d2f2b6f1396ddcb5e72021c8623b",
|
|
1572
1574
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1573
1575
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1574
1576
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -1812,7 +1814,7 @@
|
|
|
1812
1814
|
"lib/asn1-der.js": "sha256:bf085ff06bc12f08207ee641e63373ea2bf20e3dd52b8f6f026fd67700cf905c",
|
|
1813
1815
|
"lib/asyncapi-bindings.js": "sha256:2f6f3d7ea836d0483e40f08b8202d78bae54bdc3d76b5faf57d918e699ebeac8",
|
|
1814
1816
|
"lib/asyncapi-traits.js": "sha256:82ec58b77f0a65ef1b5e8b67ee9555e9cb984de769c3393fe5c6d4d0640546f0",
|
|
1815
|
-
"lib/asyncapi.js": "sha256:
|
|
1817
|
+
"lib/asyncapi.js": "sha256:cd48847ed363023e248cf0fbe275b545b0002b85e93ed91315990ef8a6abe483",
|
|
1816
1818
|
"lib/atomic-file.js": "sha256:b93b008c121f785d149d4450681467b18785781c3a82b9b001bb2a7d636c3ad1",
|
|
1817
1819
|
"lib/audit-chain.js": "sha256:1c9895a2bbf6ce3b4c546b1b3c95ac9d5ed55d82a2b58c0de4e3ecc1947c3c31",
|
|
1818
1820
|
"lib/audit-daily-review.js": "sha256:61cebe47d6e5eeeedcab45515473acfaab623217810a7ab3629b3aeca5028519",
|
|
@@ -1847,7 +1849,7 @@
|
|
|
1847
1849
|
"lib/auth/sd-jwt-vc-disclosure.js": "sha256:bc1eff5def71d2eedb6f17c8bede650050af9d790145e8697871c75ddc8431ae",
|
|
1848
1850
|
"lib/auth/sd-jwt-vc-holder.js": "sha256:9d22b691e6a698aa2240c3ac611bc91e5b8d6acaa4b8fa03e28077ddb5fd56df",
|
|
1849
1851
|
"lib/auth/sd-jwt-vc-issuer.js": "sha256:8cbfd25af7df5db7e7f61d004059530c46cc18fd1afbcebbe94f8f81f503c413",
|
|
1850
|
-
"lib/auth/sd-jwt-vc.js": "sha256:
|
|
1852
|
+
"lib/auth/sd-jwt-vc.js": "sha256:717df0e7162e11c76e47221d6877d2bf07dba9974c58cf84719bb365fce52ee0",
|
|
1851
1853
|
"lib/auth/status-list.js": "sha256:2861dafbe8dab3d6fba161663850c35acf2eb1d29c481dd030339c5c013602a2",
|
|
1852
1854
|
"lib/auth/step-up-policy.js": "sha256:dca5810bd13d1e4d279b9d34b3e777cf2455c938502b25b41c773e513d90b379",
|
|
1853
1855
|
"lib/auth/step-up.js": "sha256:3c426fc31ccf4180a9958f3c9e81573fd90ea7afa7688e52b8547a74ea987fbd",
|
|
@@ -1931,7 +1933,7 @@
|
|
|
1931
1933
|
"lib/dora.js": "sha256:1b2ed45582623978c9f1087a0a45b6707d1a3d7b5da9eb420f780ec44c6ddd37",
|
|
1932
1934
|
"lib/dr-runbook.js": "sha256:4da7ce04f88e79e103c8cb349d833b716259b99dff4fceb40d2edbac638e6cbf",
|
|
1933
1935
|
"lib/dsa.js": "sha256:93240f4a5477e06a1f76c90ddf27f0209324261bc841b237aa6d6023b796a32b",
|
|
1934
|
-
"lib/dsr.js": "sha256:
|
|
1936
|
+
"lib/dsr.js": "sha256:784102f593cf3f42bda74288b291f5005fef7bf4f8569ef60f32d5fe1b0c7231",
|
|
1935
1937
|
"lib/dual-control.js": "sha256:7b0bc61722be7df45d2ae161ed2a2ec7e33ac5f118175f29b4c58fec364b54e3",
|
|
1936
1938
|
"lib/early-hints.js": "sha256:cb0daba98c9259f1ad7694db888e25a1f8df866ecbca6c19f7a976a22236b475",
|
|
1937
1939
|
"lib/eat.js": "sha256:35030327fb3349f7f32ebea939a0d98d93cfca4c2c31a51bd2aaeb272d2a9e6a",
|
|
@@ -2167,13 +2169,13 @@
|
|
|
2167
2169
|
"lib/network-byte-quota.js": "sha256:79eafbf602a10ced990d89cfd26bea3992e42b177407b8e512bd4790999d17e3",
|
|
2168
2170
|
"lib/network-dane.js": "sha256:1f60a47618bd99a487d6c2faad423546896f753e33b516bdbc354138438d06f8",
|
|
2169
2171
|
"lib/network-dns-resolver.js": "sha256:3fd08605a077a8de1c15b3f436d2c2910af77e0078d27ce9d62fdce2e5949b7e",
|
|
2170
|
-
"lib/network-dns.js": "sha256:
|
|
2172
|
+
"lib/network-dns.js": "sha256:ff986202f7800719efe32d6f49900432783eb40a21185f4e8acdb7da45ba4139",
|
|
2171
2173
|
"lib/network-dnssec.js": "sha256:2b0758d09d5af89b49c69892fabaebabba125c3f9f162996c12b9abbbbae4bca",
|
|
2172
2174
|
"lib/network-heartbeat.js": "sha256:f14ec7fa4667cbb04477d91ae59d6253b6c9ccc43071bc558e737cc672f43975",
|
|
2173
2175
|
"lib/network-nts.js": "sha256:90ce47584ad58c171e8029eb1d5df8ba4ba130c4038d1fe260109685c34c8f75",
|
|
2174
2176
|
"lib/network-proxy.js": "sha256:9342c534863f905000d2432b67c26dd7a5ea02be25aaf9ebe05976079e61d4c9",
|
|
2175
2177
|
"lib/network-smtp-policy.js": "sha256:a7b6c91983b312a16c436d3e15259f8f6a84579e2e55f64106285b22795e1510",
|
|
2176
|
-
"lib/network-tls.js": "sha256:
|
|
2178
|
+
"lib/network-tls.js": "sha256:554b64c77389e1c961da8d061c63c806be994960466fb26f2b2268dbec0a369b",
|
|
2177
2179
|
"lib/network-tsig.js": "sha256:41dc85a6e816cce1c58c36891dfc6bafc612b68978936107a09a5a553563e0d7",
|
|
2178
2180
|
"lib/network.js": "sha256:40cb01627bf45741cac22ef79f5985f69323e47732d54cf3ce28c06882830c25",
|
|
2179
2181
|
"lib/nis2-report.js": "sha256:fa106a4f95dd9301d7da966c5792197bdbebac477a4f1d2fd2ac2481fbd818fd",
|
|
@@ -2200,7 +2202,7 @@
|
|
|
2200
2202
|
"lib/openapi-schema-walk.js": "sha256:8a46b681dce7887902f59a86adf3fc6226eeb903e5205228a7397f7fec036efe",
|
|
2201
2203
|
"lib/openapi-security.js": "sha256:091ca0f5ee89bda7474850eecb90be9501054f28390e9825139e1cad2bf997d7",
|
|
2202
2204
|
"lib/openapi-yaml.js": "sha256:25b03bcd1c3d0c336aa597eaf79879c840a6b87b729195c573620a47cadaa0b4",
|
|
2203
|
-
"lib/openapi.js": "sha256:
|
|
2205
|
+
"lib/openapi.js": "sha256:497f251cf81a2b02c8ed9a17c99c5b3eec49f7d8fa1ed95ff363adf4c5d54a2a",
|
|
2204
2206
|
"lib/otel-export.js": "sha256:c2f33ee7584638b5b27e2c52223eef111e1486da056d4cc5ef19e2e8a0322d9d",
|
|
2205
2207
|
"lib/outbox.js": "sha256:7ce4a9540fc6a6f0b78897647e7b3cc1f4df900341b0966142228f45c6504486",
|
|
2206
2208
|
"lib/pagination.js": "sha256:0b5141bc978dd727707cf4006fc928282c1c88508c8a4dec5bc7c42585d2e303",
|
|
@@ -2250,7 +2252,7 @@
|
|
|
2250
2252
|
"lib/safe-dns.js": "sha256:60fbf77f8dfcc0ac66a943ba94f960a4a4b724c78f07f9150779525d3d460001",
|
|
2251
2253
|
"lib/safe-ical.js": "sha256:a8f40466b21a1416d5aa3a5a09b75cd48c29be2f9aabec6632db26b9b7868e23",
|
|
2252
2254
|
"lib/safe-icap.js": "sha256:23f334de6ee463238e26bf9612fc7bfc623519630674c48bf1590f2952c0568b",
|
|
2253
|
-
"lib/safe-json.js": "sha256:
|
|
2255
|
+
"lib/safe-json.js": "sha256:fa42506b545f78dc8e184dd989843e777d0b04e1f61d38a7e062b4515ad76146",
|
|
2254
2256
|
"lib/safe-jsonpath.js": "sha256:0d5a0191de5cba7564847d6419a63c9949fb310f6a77c5637d2ad7b8ca30623c",
|
|
2255
2257
|
"lib/safe-mime.js": "sha256:885bf0b2932d5ad492ec981f21068e1f85bb6738383a140b27e33659b690931e",
|
|
2256
2258
|
"lib/safe-mount-info.js": "sha256:790d047481e0a2c04d3dd1f6f1645e2d6fada2f1dd1efde93ca647d2177ca7d4",
|
|
@@ -2263,7 +2265,7 @@
|
|
|
2263
2265
|
"lib/safe-url.js": "sha256:36e3d2c72e7adc5c90cce7f80e469cf50451ccadd3791a1e3aa406754a4ce6e1",
|
|
2264
2266
|
"lib/safe-vcard.js": "sha256:be2964d302b9317206fab1740ebbd0acff00258b138810aa6d64ad6cc1797ce5",
|
|
2265
2267
|
"lib/sandbox-worker.js": "sha256:eefe4e76b2a736208f2e2e90347fc1ff2c2018a98e06bca3103aeb6f96298c6b",
|
|
2266
|
-
"lib/sandbox.js": "sha256:
|
|
2268
|
+
"lib/sandbox.js": "sha256:0fa2d3c7d52afa3091826083ec5b08fd8273e0a6fd33b207e714fd16b734d5b5",
|
|
2267
2269
|
"lib/scheduler.js": "sha256:1e7ddf15a27776c738dc8112a3ef1c8fcc616f1c6f909ec2d549d3c96cdd9c80",
|
|
2268
2270
|
"lib/scitt.js": "sha256:c094cef31630aed5dc7adbf494701ab0a825c79a4e406277cbc757ce54bfec9f",
|
|
2269
2271
|
"lib/sd-notify.js": "sha256:2ef7395bbdab2ac4eb96083c57d401921c94278545f14427fc88cdd970bdb9eb",
|
|
@@ -2322,7 +2324,7 @@
|
|
|
2322
2324
|
"lib/vendor/public-suffix-list.data.js": "sha256:c840d9ae5c6bf4a07967340a5c649df2c6a66a287db41e408461c46cc32348e6",
|
|
2323
2325
|
"lib/vendor/simplewebauthn-server.cjs": "sha256:49411d893f5e9b0e2fcaa564b4ec7921f73a9a06229b5e53d49c1453ea1a365c",
|
|
2324
2326
|
"lib/vendor/vendor-data-pubkey.js": "sha256:a12afa34cd7472e2eaebad2fcd44714102d3edd0601e45769404124a513926d0",
|
|
2325
|
-
"lib/vex.js": "sha256:
|
|
2327
|
+
"lib/vex.js": "sha256:bed6880980e7f166fd30bbddab57d7fa06085aaae0dc13ed82a17531b76878b7",
|
|
2326
2328
|
"lib/watcher.js": "sha256:8618da919affabbe4c4d33915647b3ee4b27e6ea091638f032d4bfead797baf8",
|
|
2327
2329
|
"lib/web-push-vapid.js": "sha256:54d7d4beb764681bc97dc338490fc2afc3996a032e080401830206814b8c97f0",
|
|
2328
2330
|
"lib/webhook-dispatcher.js": "sha256:f879d127c0c9ba81ee43ee2b1f38c3fac99d8ff97df1555707beda48caad53f8",
|
|
@@ -2339,7 +2341,7 @@
|
|
|
2339
2341
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2340
2342
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2341
2343
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2342
|
-
"package.json": "sha256:
|
|
2344
|
+
"package.json": "sha256:4f8db3f9b9bee033a270a974dd57ba37387f0d41073c8b6ce260073876edd105",
|
|
2343
2345
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2344
2346
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2345
2347
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2368,6 +2370,8 @@
|
|
|
2368
2370
|
"release-notes/v0.15.25.json": "sha256:6f7c4eb0d57068317bfb68f739d93b5926f9723a57a0701c966b0f96eb46ed69",
|
|
2369
2371
|
"release-notes/v0.15.26.json": "sha256:a96b1c7409f863b5a920f51a92b653d5f7a0b671e000bb5dfc2e9bf47e4120dc",
|
|
2370
2372
|
"release-notes/v0.15.27.json": "sha256:f9e8309bc32c2245ec8d0682e6205417879f090403727d4780b42c0536125f02",
|
|
2373
|
+
"release-notes/v0.15.28.json": "sha256:86d4b034ccd17a51abd706fcd3ec0c1dfca224ae92a74e8d501af1e6f57cb6aa",
|
|
2374
|
+
"release-notes/v0.15.29.json": "sha256:9e912ac2c32bb84bf1d89b8cc5f04d73a78d74a2ac1be24e42e6462f5135b4f5",
|
|
2371
2375
|
"release-notes/v0.15.3.json": "sha256:19a0074c445545468ca3cc411b21ec8bdb27be2669ae1950347cc244f6aa348c",
|
|
2372
2376
|
"release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
|
|
2373
2377
|
"release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
|
|
@@ -2409,7 +2413,7 @@
|
|
|
2409
2413
|
"scripts/vendor-data-gen.js": "sha256:76b627bc6e19b4a122edfca6f514bcb8ca11af02902f0957e641f503337a8a0f",
|
|
2410
2414
|
"scripts/vendor-data-keygen.js": "sha256:94eaa4d8f832b4aac9ccbcb2a07e6b99cd35cf7b044e1412079cebdefc1f4c0e",
|
|
2411
2415
|
"scripts/vendor-update.sh": "sha256:373d7f024b7caa3345a3598fa3a586078dd842f0071e6fdad00a473f48a3a929",
|
|
2412
|
-
"test/00-primitives.js": "sha256:
|
|
2416
|
+
"test/00-primitives.js": "sha256:918967f91118ca29fc24b7d25465c840c8fe2e3fd4e19fe09075db986e3fe230",
|
|
2413
2417
|
"test/10-state.js": "sha256:45c177da8158bda413b783f68d9b01ef72796aec114c26acea084f92de874f57",
|
|
2414
2418
|
"test/20-db.js": "sha256:241ef6b7ef305d077aeafb22ee3bcc75b6b549a8fa9b1a6b5d6d5fba43b48d7d",
|
|
2415
2419
|
"test/30-chain.js": "sha256:81d3615c276138d9b71136047ce80f03170951a7ce1e6aa6be117cb673cca4f8",
|
|
@@ -2484,9 +2488,9 @@
|
|
|
2484
2488
|
"test/integration/ws-client-roundtrip.test.js": "sha256:e6e9592dcc3962fffbc2ad399567befea6177fd48e31541a79f4cb630fcf7006",
|
|
2485
2489
|
"test/layer-0-primitives/a2a-tasks.test.js": "sha256:094532bd9306e76c87db08b678edd88792b64855147501963b156f612bb8ec2f",
|
|
2486
2490
|
"test/layer-0-primitives/a2a.test.js": "sha256:fb5d0ad7e3d0e4fb3d4b2e85acb344a4ba5140f9a9dc533b75d7ab81a64bba43",
|
|
2487
|
-
"test/layer-0-primitives/access-lock.test.js": "sha256:
|
|
2491
|
+
"test/layer-0-primitives/access-lock.test.js": "sha256:26f14cad57db47c88546186393fbdd7a4adc47675d7b45407cd803c3ee3fa080",
|
|
2488
2492
|
"test/layer-0-primitives/acme.test.js": "sha256:23ac86ef3b4ab261a9bf55ee936ea543e245d6600ab79c00d167aa4e145e2cba",
|
|
2489
|
-
"test/layer-0-primitives/age-gate.test.js": "sha256:
|
|
2493
|
+
"test/layer-0-primitives/age-gate.test.js": "sha256:b5b2813c423a2dd817c69bb05dbce1aa690ffb40cac7a76227ebf515244cabfc",
|
|
2490
2494
|
"test/layer-0-primitives/agent-event-bus.test.js": "sha256:d428f6237d48a101837ff77ae62a94de885a1d99864346c8d78561dcf97351d2",
|
|
2491
2495
|
"test/layer-0-primitives/agent-idempotency.test.js": "sha256:cfe26bf5890d70d634659199292a525efd4885c55bfd1384e05bd3dca07ac398",
|
|
2492
2496
|
"test/layer-0-primitives/agent-orchestrator.test.js": "sha256:122bc275b5dbec60efc541f48d8e121f37d1d1e9f0d3b914a2c27d8ebe28a0e2",
|
|
@@ -2615,11 +2619,11 @@
|
|
|
2615
2619
|
"test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
|
|
2616
2620
|
"test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
|
|
2617
2621
|
"test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
|
|
2618
|
-
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:
|
|
2622
|
+
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:38c5ad5bf99da425f563b892451a39a4ed3a323694c2fb9b43271f63e9075266",
|
|
2619
2623
|
"test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
|
|
2620
2624
|
"test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
|
|
2621
2625
|
"test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
|
|
2622
|
-
"test/layer-0-primitives/compliance-eaa.test.js": "sha256:
|
|
2626
|
+
"test/layer-0-primitives/compliance-eaa.test.js": "sha256:2eb41e48e9face72cb0d21ee0cd2b69ca7fcaba305cb9e30741d35cd2bd9f4a7",
|
|
2623
2627
|
"test/layer-0-primitives/compliance-eu-ai-act-posture.test.js": "sha256:3f45bb76ef390b7cc4396a37153d48f0f9465c5abc59851f0971fcc40c28dcea",
|
|
2624
2628
|
"test/layer-0-primitives/compliance-sanctions.test.js": "sha256:e4cb4e7b73cb0834958c1c18c9d3bb70087ee784216a888991dbe41b518b87f0",
|
|
2625
2629
|
"test/layer-0-primitives/compliance.test.js": "sha256:d743b6e2df985daed1df4a7808e7e5bf91207d5909f5bd7d8f882e0c27dcfe1b",
|
|
@@ -2697,7 +2701,7 @@
|
|
|
2697
2701
|
"test/layer-0-primitives/dr-runbook.test.js": "sha256:9665caaa90f356d0237e0e6c6889c56a3467b20c7eaf78d2e2afa51c4b3af2cb",
|
|
2698
2702
|
"test/layer-0-primitives/dsa.test.js": "sha256:2cdbdd29b9c58d738920bb4f82f73492eab56e7d7f8111f1cdb14838de3a3ab3",
|
|
2699
2703
|
"test/layer-0-primitives/dsr-state-rules.test.js": "sha256:0ddc7fd6d5d3f8817d8f4aad7bb66c6bf6e64eafe61fdedaafa8f60957d20bd0",
|
|
2700
|
-
"test/layer-0-primitives/dsr.test.js": "sha256:
|
|
2704
|
+
"test/layer-0-primitives/dsr.test.js": "sha256:d7be3fcf28348936ffd73ec6bc06d59ea7ab22b40fa3316eba18c587d7575680",
|
|
2701
2705
|
"test/layer-0-primitives/dual-control.test.js": "sha256:2d9255f96bddbb45d75182ef826a355536e40caeb5864483c56eaf5ba3c7e0ac",
|
|
2702
2706
|
"test/layer-0-primitives/early-hints.test.js": "sha256:9827c66c7558ebeb39a448ec69129517a517d440865ac5341afcbc7c1047e085",
|
|
2703
2707
|
"test/layer-0-primitives/eat.test.js": "sha256:6f0b0180f659b008a6a86f9af4f8674e2ddb98d00408e10d52d03291bda96f6e",
|
|
@@ -2724,7 +2728,7 @@
|
|
|
2724
2728
|
"test/layer-0-primitives/fsm.test.js": "sha256:7037831083426478ed4efd7d1c17d4b96c3609816a41819b5c9da71d2b6187e8",
|
|
2725
2729
|
"test/layer-0-primitives/gate-contract-content-gate.test.js": "sha256:d9f8b94231efb0e02a94b7934bf58a3f8e343829ce11346b7302e69e13efd551",
|
|
2726
2730
|
"test/layer-0-primitives/gcs-bucket-ops.test.js": "sha256:c1a0982190506dba72430c16972a296abc2de865786cc72c63c971edf450502c",
|
|
2727
|
-
"test/layer-0-primitives/gdpr-ropa.test.js": "sha256:
|
|
2731
|
+
"test/layer-0-primitives/gdpr-ropa.test.js": "sha256:de4d2b6db19c87a0ea00c49bc865c39802a664f0ee6b6a3f98f62319d79a4fe7",
|
|
2728
2732
|
"test/layer-0-primitives/graphql-federation.test.js": "sha256:c920cdc6f8740733b712d139d11da08c4fa5c8add3b0749fa74c6fbce04965cf",
|
|
2729
2733
|
"test/layer-0-primitives/guard-agent-registry.test.js": "sha256:d432339273cde552cde8101ac1b32df44c36e7dd9381a328669f63884ac9016b",
|
|
2730
2734
|
"test/layer-0-primitives/guard-all.test.js": "sha256:fdd9a746ecfa6e29f0e5f727fbd66c5ab2e29a263ef0d8c2c1d62fc430cdd59f",
|
|
@@ -2737,7 +2741,7 @@
|
|
|
2737
2741
|
"test/layer-0-primitives/guard-event-bus-topic.test.js": "sha256:538e6d338541b724fc5bedb7f2fd33fae12777d292c771bb5d70720e30bf9156",
|
|
2738
2742
|
"test/layer-0-primitives/guard-filename.test.js": "sha256:6718194054554e246e171e1c23a970dfc5e618fa3bd6a79a8925289fd339fd06",
|
|
2739
2743
|
"test/layer-0-primitives/guard-gate-disposition-coverage.test.js": "sha256:e4e3a971fe2659593bb76aebed6d65ddea937eda558191085c1ad31fb1c5eaf3",
|
|
2740
|
-
"test/layer-0-primitives/guard-html-wcag.test.js": "sha256:
|
|
2744
|
+
"test/layer-0-primitives/guard-html-wcag.test.js": "sha256:cdbec2cc892cbe615865a1cfc6b69fe128af81ad953fc35fb6f00c284a5a7fc4",
|
|
2741
2745
|
"test/layer-0-primitives/guard-html.test.js": "sha256:31ac5ba5728ad18df1e188c5251ec97698f7d7bd840011c2870fccd115b35969",
|
|
2742
2746
|
"test/layer-0-primitives/guard-idempotency-key.test.js": "sha256:18b233de627ea68cc289cb2fcd6ad5ca18db3bf911581747fd293e1bd3ee8789",
|
|
2743
2747
|
"test/layer-0-primitives/guard-image.test.js": "sha256:0f45eae95851d2e9f5b8338d2a99dddabb59b60e1be69ac9ec5d0e4d7cd21428",
|
|
@@ -2861,12 +2865,12 @@
|
|
|
2861
2865
|
"test/layer-0-primitives/network-dns-lookup-timeout-default.test.js": "sha256:6251b8d093f6402a13d08afea2d7164fccdb8d2edbbbe63915b88b4e2bcba47b",
|
|
2862
2866
|
"test/layer-0-primitives/network-dns-resolver-timeout.test.js": "sha256:9ba8ca49b8095ec4780959134520ac869c80ca9b9664b5beadce2d135037777d",
|
|
2863
2867
|
"test/layer-0-primitives/network-dns-resolver.test.js": "sha256:8966ba1b24c9ceb5754a3ef21a6d6e61a6cade983367bece1034110b7928fc1c",
|
|
2864
|
-
"test/layer-0-primitives/network-dns.test.js": "sha256:
|
|
2868
|
+
"test/layer-0-primitives/network-dns.test.js": "sha256:b627a0c6006229be8d0c3409857dcff6bd7b97a22f5ff5c3b4f9fdd28178997a",
|
|
2865
2869
|
"test/layer-0-primitives/network-heartbeat-passive.test.js": "sha256:4dae1d9f61c85489d1a91a6f4a242198d0bee89fdbacff9efba143e78e882ad0",
|
|
2866
2870
|
"test/layer-0-primitives/network-nts-handshake-byte-cap.test.js": "sha256:b20b7a6e218613282d7d817d7a675f9ee81ba07af1ed5c089283e62736e2c515",
|
|
2867
2871
|
"test/layer-0-primitives/network-tls-build-options.test.js": "sha256:0380ef886acc0a3bb0f7bc032c11991891fe9f19648e9f8d38829c7ea16b61b0",
|
|
2868
2872
|
"test/layer-0-primitives/network-tls-ct-inclusion.test.js": "sha256:951b5eeded5762f667c534c0433e4b5f4b0a6a48c7c66a88492c0d4cebd21636",
|
|
2869
|
-
"test/layer-0-primitives/network-tls.test.js": "sha256:
|
|
2873
|
+
"test/layer-0-primitives/network-tls.test.js": "sha256:f914692bc98176d2f425bcca7a7023eee5f0258eb293da2d56afc8232c10fa2f",
|
|
2870
2874
|
"test/layer-0-primitives/network-tsig.test.js": "sha256:8845cb8f23876e6c68436491a412bbbd2c5415af23438c5ab2613358b3a4168f",
|
|
2871
2875
|
"test/layer-0-primitives/network.test.js": "sha256:5424adaab9c3fe6e1b96e21eef90e51c5cf4c7fa8e96132306399d95cdfacdf1",
|
|
2872
2876
|
"test/layer-0-primitives/nis2-report.test.js": "sha256:4ee2d6a5c6e05c317c34a4f7eef91a0c91079d227b8b69c65e1d58e4444613e0",
|
|
@@ -2920,7 +2924,7 @@
|
|
|
2920
2924
|
"test/layer-0-primitives/request-id-async-context.test.js": "sha256:3aad8f145e7ca37c7220767779050ffaa5da2b2bb2cb7e4b65d3652d554b0322",
|
|
2921
2925
|
"test/layer-0-primitives/request-log.test.js": "sha256:31479f63311b31a74aa7f636b7f9ef04c2f6bcb152b8b8c465a40d08461cc03f",
|
|
2922
2926
|
"test/layer-0-primitives/require-auth-cache-control.test.js": "sha256:36b9d77791285e2b2606a5d2f8f8c8536a46e7ec1f4f2c5e54ee1b17a3dece3d",
|
|
2923
|
-
"test/layer-0-primitives/require-mtls.test.js": "sha256:
|
|
2927
|
+
"test/layer-0-primitives/require-mtls.test.js": "sha256:355759e9a2e03add272af8f18f8297c60444d30fceac99455dd6ce339e8980f6",
|
|
2924
2928
|
"test/layer-0-primitives/resource-access-lock.test.js": "sha256:f436bcd187b317229266b4f27d89e7f87cb28c7dc400643594b22d05232dbac4",
|
|
2925
2929
|
"test/layer-0-primitives/restore-blob-remap.test.js": "sha256:8caee31bd1c75dc15fb51ffc3db7981660c7bc20a01ff8e82e9dccbdefe9e671",
|
|
2926
2930
|
"test/layer-0-primitives/retention-dryrun-no-vacuum.test.js": "sha256:b454fb508b22418097839a20f7b1ff5240a57f8ee37e575768780e7b8cc772b6",
|
|
@@ -2951,7 +2955,7 @@
|
|
|
2951
2955
|
"test/layer-0-primitives/safe-url-canonicalize.test.js": "sha256:5a85baeec34770f9efef04fe8036fc75f80278188ed6f09adcb97f72d39a0dc8",
|
|
2952
2956
|
"test/layer-0-primitives/safe-url-idn-homograph.test.js": "sha256:a68b8307a0711270ce865d80937793c3bd48445f0ac6adb5814bd9b948e06193",
|
|
2953
2957
|
"test/layer-0-primitives/safe-vcard.test.js": "sha256:10a0695050afee64599411352b34382e15b8f9bd9045ed3951cc6bc561918c89",
|
|
2954
|
-
"test/layer-0-primitives/safe-xml.test.js": "sha256:
|
|
2958
|
+
"test/layer-0-primitives/safe-xml.test.js": "sha256:0a2127b6803bea6e028a0350c02ffd6812c17de5c6c0a260c54ea2c642eb0c93",
|
|
2955
2959
|
"test/layer-0-primitives/saml-slo.test.js": "sha256:b8b3a5e71ad28a0a3e9b69bca9f570410bc8b8eab6a7fdfd186fdf12cd90854f",
|
|
2956
2960
|
"test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js": "sha256:2deac25891a4eb2c5d1073d99c352a01322e6fab6497616209ec6a15cc1fbf71",
|
|
2957
2961
|
"test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js": "sha256:1396afcc34135ab025880ac74738f3eb1984f5725dcf2e676e6a2f1bd7f852c2",
|
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.29 (2026-06-25) — **JSON parsing of operator and untrusted input is routed through `b.safeJson` everywhere, so a `"__proto__"` member is stripped and the parse is size-bounded — `b.openapi.parse`, `b.asyncapi.parse`, and `b.sandbox.run` were using a raw `JSON.parse`.** b.openapi.parse and b.asyncapi.parse parsed an operator-supplied document string with a raw JSON.parse, which re-creates a "__proto__" member as an own key and imposes no size bound; b.sandbox.run parsed the untrusted sandboxed code's serialized result the same way (proto member kept, depth unbounded). All three now route through b.safeJson, which strips the prototype-pollution keys and applies the depth / key / size caps. A new b.safeJson.parseStringOrObject primitive captures the recurring "accept a document as a JSON string OR a pre-built object" shape (a string is parsed safely, an object passes through) that openapi and asyncapi had each hand-rolled, with each consumer's typed error class and a generous document size cap passed as data. Valid input parses identically to before; a malicious or oversized document is now defused rather than accepted. **Added:** *b.safeJson.parseStringOrObject(input, opts?)* — Accepts either a JSON string — parsed through b.safeJson.parse, so the prototype-pollution-key strip and depth / key / size caps apply — or an already-decoded plain object (returned unchanged). For the recurring "operator hands me a document as a JSON string or a pre-built object" surface (b.openapi / b.asyncapi). The consumer's typed error class, error codes, label, and size cap are passed as options, so a raw JSON.parse on operator input cannot be hand-rolled per consumer. **Security:** *Operator and untrusted JSON parses strip __proto__ and bound size (no raw JSON.parse)* — A raw JSON.parse keeps a "__proto__" object member as an own key (a prototype-pollution gadget for any downstream merge) and parses unbounded input. b.openapi.parse and b.asyncapi.parse used it on operator document strings, and b.sandbox.run used it on the untrusted sandboxed code's JSON-serialized result. All now route through b.safeJson — the prototype-pollution keys are stripped and the depth / key / size caps apply (a 16 MiB document cap for openapi/asyncapi, the existing result-bytes cap for the sandbox). Internal JSON parses (the sealed DSR store payloads, the VEX canonical re-format) route through the same primitive too, so the guarantee cannot be bypassed one call site at a time. Valid input is unaffected; a "__proto__"-laden or oversized payload is defused.
|
|
12
|
+
|
|
13
|
+
- v0.15.28 (2026-06-25) — **`b.network.dns` and `b.network.tls` errors now carry a usable terminal-vs-transient signal on `err.permanent`, so a caller's retry loop re-attempts only the failures a retry can fix.** DnsError was declared always-transient and NetworkTlsError always-permanent, so a consumer driving its own retry loop got the wrong signal in both directions: it would retry a permanent DNS failure (a bad host, bad options, an unsupported query, an NXDOMAIN-style no-result, or a caller-shape/config error such as an unconfigured transport or an invalid resolver list) forever, and it would refuse to retry a transient TLS-over-network failure (an ECH connection failure, a handshake timeout, DNS momentarily unavailable). err.permanent now reflects each error's actual transience, derived from its code and failing closed (an unknown code is permanent, so a hopeless target is not retried indefinitely): for DnsError only a failed network round-trip (a lookup timeout, a resolve/reverse query that failed on the wire, a failed DoH/DoT exchange, a DDR discovery that found nothing) is transient — config, input, and environment errors raised before any network work are permanent; for NetworkTlsError only the network-layer ECH failures are transient. TlsTrustError remains always-permanent by design — a trust-verification failure (bad CA, fingerprint mismatch, OCSP not-good, CT violation, an unreachable OCSP responder) must never be silently retried past the trust decision. **Fixed:** *DnsError / NetworkTlsError expose a correct terminal/transient signal; TlsTrustError stays terminal* — b.network.dns's DnsError was always-transient and b.network.tls's NetworkTlsError always-permanent, so err.permanent misled a consumer's retry loop in both directions — retrying a permanent DNS error (bad host / options / unsupported type / no-result) indefinitely, and never retrying a transient TLS network error (ECH connect failure, handshake timeout, DNS unavailable). err.permanent now reflects each error's actual transience by code and fails closed (unknown codes are permanent): DnsError is transient only for a failed network round-trip (dns/lookup-timeout, dns/resolve-failed, dns/reverse-failed, dns/doh-failed, dns/dot-failed, dns/dot-handshake-failed, dns/ddr-not-discovered, dns/system-failed), and permanent otherwise — including the caller-shape / environment errors raised before any network work (dns/transport-unavailable for an unconfigured transport, dns/dnr-no-resolvers for an empty/invalid resolver list, dns/setservers-failed for an invalid address, dns/no-system-resolvers when none are configured). NetworkTlsError is transient only for the network-layer ECH failures (tls/ech-connect-failed, tls/ech-timeout, tls/ech-dns-unavailable), permanent otherwise. TlsTrustError remains always-permanent so a trust-verification failure is never silently retried.
|
|
14
|
+
|
|
11
15
|
- v0.15.27 (2026-06-25) — **Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.26.** The legacy single-layer smoke files used fixed-duration setTimeout sleeps to wait for asynchronous conditions (a job processed, a lease TTL lapsing, an audit flush completing). On a contended CI runner a fixed sleep is both flake-prone (too short under load) and slow (it always burns the full budget). Those condition-waits are converted to the harness's polling helpers — waitUntil for observable predicates, passiveObserve for deliberate real-time elapses, withTestTimeout for hang guards — which exit early on fast platforms and give contended platforms the full budget. No shipped framework code changed; this release is byte-for-byte identical to 0.15.26 for operators. **Fixed:** *Smoke layer files poll for conditions instead of fixed setTimeout sleeps* — The single-layer smoke files waited on asynchronous conditions with fixed-duration setTimeout sleeps, which flake under SMOKE_PARALLEL load and always burn their full budget. The condition-waits are converted to the polling helpers (waitUntil / passiveObserve / withTestTimeout) so they exit early on fast platforms and stay robust on contended ones; non-wait timers (abort triggers, child/socket watchdogs, simulated-latency mocks) are unchanged. This is test-harness reliability only — no shipped framework behavior changed.
|
|
12
16
|
|
|
13
17
|
- v0.15.26 (2026-06-25) — **Internal test-harness correctness only — the published library is byte-for-byte identical to 0.15.25.** The smoke runner requires each test module and awaits its exported run(). Several tests were instead written as a top-level (async function () {...})() IIFE that runs detached at require-time, so the runner measured and reported the test's result before the IIFE's post-await assertions executed — those checks silently never ran (one parser test exercised 4 of its 26 assertions, and a failure after the first await would have gone unseen as a false pass). Those tests are converted to the exported-run form so the runner awaits their full assertion set, and a codebase-patterns detector now refuses a top-level async IIFE in a test file so the pattern cannot return. No shipped framework code changed; this release is byte-for-byte identical to 0.15.25 for operators. **Fixed:** *Detached-IIFE tests now run their full assertion set under the smoke runner* — A test written as a top-level (async function () {...})() IIFE runs detached when the runner requires it: the runner only awaits an exported run(), so it reported the file's result before the IIFE's awaited assertions executed, and every check after the first await silently did not count (one parsers test ran 4 of 26). Such tests are rewritten to define async function run(), export it, and invoke it under if (require.main === module), so the runner awaits the complete set; a detector refuses a re-introduced top-level async IIFE in a test file. This is test-harness correctness only — no shipped framework behavior changed.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.15.
|
|
4
|
-
"createdAt": "2026-06-
|
|
3
|
+
"frameworkVersion": "0.15.29",
|
|
4
|
+
"createdAt": "2026-06-26T06:52:58.579Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -50884,6 +50884,10 @@
|
|
|
50884
50884
|
"type": "function",
|
|
50885
50885
|
"arity": 3
|
|
50886
50886
|
},
|
|
50887
|
+
"parseStringOrObject": {
|
|
50888
|
+
"type": "function",
|
|
50889
|
+
"arity": 2
|
|
50890
|
+
},
|
|
50887
50891
|
"registerFormat": {
|
|
50888
50892
|
"type": "function",
|
|
50889
50893
|
"arity": 2
|
|
@@ -40,6 +40,8 @@ var openapiSecurity = require("./openapi-security");
|
|
|
40
40
|
var openapiYaml = require("./openapi-yaml");
|
|
41
41
|
var bindingsMod = require("./asyncapi-bindings");
|
|
42
42
|
var traitsMod = require("./asyncapi-traits");
|
|
43
|
+
var safeJson = require("./safe-json");
|
|
44
|
+
var C = require("./constants");
|
|
43
45
|
var { defineClass } = require("./framework-error");
|
|
44
46
|
var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
|
|
45
47
|
|
|
@@ -470,19 +472,17 @@ function _validateServerEntry(entry, label) {
|
|
|
470
472
|
* bad.errors[0]; // → 'operations.pub.channel: $ref "#/channels/missing" does not resolve to a declared channel'
|
|
471
473
|
*/
|
|
472
474
|
function parse(jsonStringOrObject) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
"asyncapi.parse: input must be a JSON string or a plain object");
|
|
485
|
-
}
|
|
475
|
+
// A JSON string is parsed through safeJson (proto-pollution-key strip + depth
|
|
476
|
+
// / size caps — a raw JSON.parse here kept a "__proto__" member and was
|
|
477
|
+
// unbounded on an operator-supplied document); a pre-built object passes
|
|
478
|
+
// through. The 16 MiB cap is generous for any real AsyncAPI document.
|
|
479
|
+
var doc = safeJson.parseStringOrObject(jsonStringOrObject, {
|
|
480
|
+
maxBytes: C.BYTES.mib(16),
|
|
481
|
+
errorClass: AsyncApiError,
|
|
482
|
+
jsonCode: "asyncapi/bad-json",
|
|
483
|
+
inputCode: "asyncapi/bad-input",
|
|
484
|
+
label: "asyncapi.parse",
|
|
485
|
+
});
|
|
486
486
|
var errors = [];
|
|
487
487
|
if (typeof doc.asyncapi !== "string") {
|
|
488
488
|
errors.push("missing or non-string `asyncapi` version field (must be 3.0.x)");
|
|
@@ -165,8 +165,8 @@ function _verifyJwt(token, publicKey, algorithm) {
|
|
|
165
165
|
var headerStr = _b64uDecodeStr(parts[0]);
|
|
166
166
|
var payloadStr = _b64uDecodeStr(parts[1]);
|
|
167
167
|
return {
|
|
168
|
-
header: safeJson.parse(headerStr, { maxBytes: 64 * 1024 }), // allow:
|
|
169
|
-
payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:
|
|
168
|
+
header: safeJson.parse(headerStr, { maxBytes: 64 * 1024 }), // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
169
|
+
payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:raw-byte-literal — JWT payload cap (1 MB)
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -330,7 +330,7 @@ function present(opts) {
|
|
|
330
330
|
if (_jwtParts.length === 3) {
|
|
331
331
|
try {
|
|
332
332
|
_issuerPayload = safeJson.parse(_b64uDecodeStr(_jwtParts[1]),
|
|
333
|
-
{ maxBytes: 64 * 1024 }); // allow:
|
|
333
|
+
{ maxBytes: 64 * 1024 }); // allow:raw-byte-literal — JWT payload cap (64 KB)
|
|
334
334
|
} catch (_e) { _issuerPayload = null; }
|
|
335
335
|
}
|
|
336
336
|
var _sdAlg = (_issuerPayload && typeof _issuerPayload._sd_alg === "string")
|
|
@@ -448,7 +448,7 @@ async function verify(presentation, opts) {
|
|
|
448
448
|
"verify: JWT must have 3 dot-separated parts");
|
|
449
449
|
}
|
|
450
450
|
var headerObj;
|
|
451
|
-
try { headerObj = safeJson.parse(_b64uDecodeStr(jwtParts[0]), { maxBytes: 64 * 1024 }); } // allow:
|
|
451
|
+
try { headerObj = safeJson.parse(_b64uDecodeStr(jwtParts[0]), { maxBytes: 64 * 1024 }); } // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
452
452
|
catch (e) {
|
|
453
453
|
throw new AuthError("auth-sd-jwt-vc/bad-header",
|
|
454
454
|
"verify: malformed JWT header: " + e.message);
|
|
@@ -622,7 +622,7 @@ async function verify(presentation, opts) {
|
|
|
622
622
|
}
|
|
623
623
|
// Verify KB-JWT signature
|
|
624
624
|
var kbHeaderObj;
|
|
625
|
-
try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); }
|
|
625
|
+
try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); }
|
|
626
626
|
catch (e) {
|
|
627
627
|
throw new AuthError("auth-sd-jwt-vc/bad-kb-header",
|
|
628
628
|
"verify: malformed KB-JWT header: " + e.message);
|
|
@@ -114,6 +114,7 @@ var bCrypto = require("./crypto");
|
|
|
114
114
|
var lazyRequire = require("./lazy-require");
|
|
115
115
|
var validateOpts = require("./validate-opts");
|
|
116
116
|
var safeSql = require("./safe-sql");
|
|
117
|
+
var safeJson = require("./safe-json");
|
|
117
118
|
var boundedMap = require("./bounded-map");
|
|
118
119
|
var { defineClass } = require("./framework-error");
|
|
119
120
|
|
|
@@ -1142,12 +1143,23 @@ function dbTicketStore(opts) {
|
|
|
1142
1143
|
// the derived lookup hashes are computed from the plaintext; vault-less
|
|
1143
1144
|
// it stores plaintext (matching the agent-* fallback).
|
|
1144
1145
|
function _sealColumns(id, ticket) {
|
|
1146
|
+
// The payload column is read back through safeJson.parse, whose hard
|
|
1147
|
+
// ceiling (safeJson.ABSOLUTE_MAX_BYTES) caps what any read can accept.
|
|
1148
|
+
// Refuse a ticket whose serialized form exceeds that same ceiling on
|
|
1149
|
+
// write so the store never holds a payload it cannot read back later
|
|
1150
|
+
// (write cap == read cap, measured the same way: UTF-8 byte length).
|
|
1151
|
+
var serializedPayload = JSON.stringify(ticket);
|
|
1152
|
+
if (Buffer.byteLength(serializedPayload, "utf8") > safeJson.ABSOLUTE_MAX_BYTES) {
|
|
1153
|
+
throw new DsrError("dsr/ticket-too-large",
|
|
1154
|
+
"_sealColumns: ticket " + id + " payload exceeds the " +
|
|
1155
|
+
safeJson.ABSOLUTE_MAX_BYTES + "-byte store limit");
|
|
1156
|
+
}
|
|
1145
1157
|
var row = {
|
|
1146
1158
|
id: id,
|
|
1147
1159
|
subject_id: (ticket.subject && ticket.subject.subjectId) || null,
|
|
1148
1160
|
subject_email: (ticket.subject && ticket.subject.email) || null,
|
|
1149
1161
|
subject_phone: (ticket.subject && ticket.subject.phone) || null,
|
|
1150
|
-
payload:
|
|
1162
|
+
payload: serializedPayload,
|
|
1151
1163
|
};
|
|
1152
1164
|
var out = {
|
|
1153
1165
|
$sid: row.subject_id,
|
|
@@ -1252,7 +1264,7 @@ function dbTicketStore(opts) {
|
|
|
1252
1264
|
var rows = db.prepare("SELECT id, payload FROM " + qTable + " WHERE id = $id")
|
|
1253
1265
|
.all({ $id: id });
|
|
1254
1266
|
if (!rows || rows.length === 0) return null;
|
|
1255
|
-
return
|
|
1267
|
+
return safeJson.parse(_unsealPayload(rows[0].payload, rows[0].id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES });
|
|
1256
1268
|
},
|
|
1257
1269
|
list: async function (filter) {
|
|
1258
1270
|
filter = filter || {};
|
|
@@ -1267,7 +1279,7 @@ function dbTicketStore(opts) {
|
|
|
1267
1279
|
if (conds.length > 0) sql += " WHERE " + conds.join(" AND ");
|
|
1268
1280
|
sql += " ORDER BY submitted_at DESC";
|
|
1269
1281
|
var rows = db.prepare(sql).all(params);
|
|
1270
|
-
return rows.map(function (r) { return
|
|
1282
|
+
return rows.map(function (r) { return safeJson.parse(_unsealPayload(r.payload, r.id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES }); });
|
|
1271
1283
|
},
|
|
1272
1284
|
update: async function (id, ticket) {
|
|
1273
1285
|
var cols = _sealColumns(id, ticket);
|
|
@@ -1314,7 +1326,7 @@ function dbTicketStore(opts) {
|
|
|
1314
1326
|
var del = db.prepare("DELETE FROM " + qTable + " WHERE id = $id");
|
|
1315
1327
|
for (var i = 0; i < rows.length; i++) {
|
|
1316
1328
|
try {
|
|
1317
|
-
var t =
|
|
1329
|
+
var t = safeJson.parse(_unsealPayload(rows[i].payload, rows[i].id), { maxBytes: safeJson.ABSOLUTE_MAX_BYTES });
|
|
1318
1330
|
if (t.retentionUntil && t.retentionUntil < asOf) {
|
|
1319
1331
|
del.run({ $id: rows[i].id });
|
|
1320
1332
|
purged += 1;
|
|
@@ -16,7 +16,34 @@ var validateOpts = require("./validate-opts");
|
|
|
16
16
|
var { defineClass } = require("./framework-error");
|
|
17
17
|
var { boundedMap } = require("./bounded-map");
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// DnsError carries a terminal-vs-transient signal on err.permanent so a caller
|
|
20
|
+
// driving a retry loop knows which failures are worth re-attempting. Fail
|
|
21
|
+
// CLOSED: only a failure of an actual network round-trip that a retry can
|
|
22
|
+
// plausibly fix is transient (a lookup timeout, a resolve/reverse query that
|
|
23
|
+
// failed on the wire, a DoH/DoT exchange that failed, a DDR discovery query that
|
|
24
|
+
// found nothing). Everything else is permanent — bad config / options, an
|
|
25
|
+
// unsupported query, a malformed or empty/NXDOMAIN-style reply, AND the
|
|
26
|
+
// caller-shape / environment errors raised BEFORE any network work: requesting a
|
|
27
|
+
// transport that was never configured (dns/transport-unavailable), an
|
|
28
|
+
// empty/invalid designated-resolver list (dns/dnr-no-resolvers), an invalid
|
|
29
|
+
// setServers address (dns/setservers-failed), or no system resolvers configured
|
|
30
|
+
// (dns/no-system-resolvers). A retry cannot make invalid input or absent
|
|
31
|
+
// configuration valid.
|
|
32
|
+
var DNS_TRANSIENT_CODES = {
|
|
33
|
+
"dns/lookup-timeout": true,
|
|
34
|
+
"dns/system-failed": true,
|
|
35
|
+
"dns/resolve-failed": true,
|
|
36
|
+
"dns/reverse-failed": true,
|
|
37
|
+
"dns/doh-http": true,
|
|
38
|
+
"dns/doh-failed": true,
|
|
39
|
+
"dns/dot-handshake-failed": true,
|
|
40
|
+
"dns/dot-failed": true,
|
|
41
|
+
"dns/ddr-not-discovered": true,
|
|
42
|
+
};
|
|
43
|
+
function _dnsErrorIsPermanent(code) {
|
|
44
|
+
return !Object.prototype.hasOwnProperty.call(DNS_TRANSIENT_CODES, code);
|
|
45
|
+
}
|
|
46
|
+
var DnsError = defineClass("DnsError", { permanentClassifier: _dnsErrorIsPermanent });
|
|
20
47
|
|
|
21
48
|
// Protocol-fixed byte counts and radixes — passthrough through C.BYTES
|
|
22
49
|
// keeps every numeric literal routed through one helper.
|
|
@@ -16,8 +16,28 @@ var lazyRequire = require("./lazy-require");
|
|
|
16
16
|
var safeAsync = require("./safe-async");
|
|
17
17
|
var { defineClass } = require("./framework-error");
|
|
18
18
|
|
|
19
|
+
// TlsTrustError is a TRUST-verification failure (bad CA/PEM, fingerprint
|
|
20
|
+
// mismatch, OCSP not-good / revoked, CT violation, hostname/PKIX failure, an
|
|
21
|
+
// unreachable OCSP responder). These are ALWAYS permanent: a caller must never
|
|
22
|
+
// silently retry past a trust decision — a transient-looking OCSP-fetch failure
|
|
23
|
+
// is the operator's soft-fail policy to make explicitly, not something a retry
|
|
24
|
+
// loop should paper over. Fail closed.
|
|
19
25
|
var TlsTrustError = defineClass("TlsTrustError", { alwaysPermanent: true });
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
// NetworkTlsError carries a terminal-vs-transient signal on err.permanent. Fail
|
|
28
|
+
// CLOSED: only the network-layer ECH failures a retry can plausibly fix (a
|
|
29
|
+
// connect failure, a timeout, DNS being momentarily unavailable) are transient;
|
|
30
|
+
// bad options, a malformed ECH config, and PKIX hostname/SAN validation failures
|
|
31
|
+
// are permanent (config / validation errors a retry cannot fix).
|
|
32
|
+
var TLS_TRANSIENT_CODES = {
|
|
33
|
+
"tls/ech-connect-failed": true,
|
|
34
|
+
"tls/ech-timeout": true,
|
|
35
|
+
"tls/ech-dns-unavailable": true,
|
|
36
|
+
};
|
|
37
|
+
function _networkTlsErrorIsPermanent(code) {
|
|
38
|
+
return !Object.prototype.hasOwnProperty.call(TLS_TRANSIENT_CODES, code);
|
|
39
|
+
}
|
|
40
|
+
var NetworkTlsError = defineClass("NetworkTlsError", { permanentClassifier: _networkTlsErrorIsPermanent });
|
|
21
41
|
|
|
22
42
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
23
43
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -47,6 +47,8 @@ var schemaWalk = require("./openapi-schema-walk");
|
|
|
47
47
|
var pathsBuilderMod = require("./openapi-paths-builder");
|
|
48
48
|
var openapiSecurity = require("./openapi-security");
|
|
49
49
|
var openapiYaml = require("./openapi-yaml");
|
|
50
|
+
var safeJson = require("./safe-json");
|
|
51
|
+
var C = require("./constants");
|
|
50
52
|
var { defineClass } = require("./framework-error");
|
|
51
53
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
52
54
|
|
|
@@ -520,19 +522,17 @@ function _validateItemOperations(item, label, errors, securitySchemes) {
|
|
|
520
522
|
* bad.errors[0]; // → 'path "users" must start with \'/\''
|
|
521
523
|
*/
|
|
522
524
|
function parse(jsonStringOrObject) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
"openapi.parse: input must be a JSON string or a plain object");
|
|
535
|
-
}
|
|
525
|
+
// A JSON string is parsed through safeJson (proto-pollution-key strip + depth
|
|
526
|
+
// / size caps — a raw JSON.parse here kept a "__proto__" member and was
|
|
527
|
+
// unbounded on an operator-supplied document); a pre-built object passes
|
|
528
|
+
// through. The 16 MiB cap is generous for any real OpenAPI document.
|
|
529
|
+
var doc = safeJson.parseStringOrObject(jsonStringOrObject, {
|
|
530
|
+
maxBytes: C.BYTES.mib(16),
|
|
531
|
+
errorClass: OpenApiError,
|
|
532
|
+
jsonCode: "openapi/bad-json",
|
|
533
|
+
inputCode: "openapi/bad-input",
|
|
534
|
+
label: "openapi.parse",
|
|
535
|
+
});
|
|
536
536
|
var errors = [];
|
|
537
537
|
if (typeof doc.openapi !== "string") {
|
|
538
538
|
errors.push("missing or non-string `openapi` version field (must be 3.1.x or 3.2.x)");
|
|
@@ -240,6 +240,61 @@ function parseOrDefault(input, fallback, opts) {
|
|
|
240
240
|
catch (_e) { return fallback; }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* @primitive b.safeJson.parseStringOrObject
|
|
245
|
+
* @signature b.safeJson.parseStringOrObject(input, opts?)
|
|
246
|
+
* @since 0.15.29
|
|
247
|
+
* @status stable
|
|
248
|
+
* @related b.safeJson.parse
|
|
249
|
+
*
|
|
250
|
+
* Accept EITHER a JSON string — parsed through `parse`, so the
|
|
251
|
+
* proto-pollution-key strip, depth/key caps, and size cap all apply — OR an
|
|
252
|
+
* already-decoded plain object (returned unchanged). This is the recurring
|
|
253
|
+
* "operator hands me a document as a JSON string or a pre-built object" surface
|
|
254
|
+
* (b.openapi / b.asyncapi). Routing it here means a raw `JSON.parse` on operator
|
|
255
|
+
* input — which keeps a `"__proto__"` member as an own key and imposes no size
|
|
256
|
+
* bound — cannot be hand-rolled per consumer. The divergence each consumer needs
|
|
257
|
+
* (its typed error class + codes + a generous document size cap) is carried as
|
|
258
|
+
* data, so there is no per-consumer branch.
|
|
259
|
+
*
|
|
260
|
+
* @opts
|
|
261
|
+
* maxBytes: number, // forwarded to parse (default 1 MiB; capped 64 MiB)
|
|
262
|
+
* maxDepth: number, // forwarded to parse
|
|
263
|
+
* maxKeys: number, // forwarded to parse
|
|
264
|
+
* errorClass: function, // typed error class to throw (else SafeJsonError)
|
|
265
|
+
* jsonCode: string, // error code for invalid JSON (used with errorClass)
|
|
266
|
+
* inputCode: string, // error code for a non-string/non-object input
|
|
267
|
+
* label: string, // message prefix (default "safeJson.parseStringOrObject")
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* var doc = b.safeJson.parseStringOrObject(input, {
|
|
271
|
+
* maxBytes: C.BYTES.mib(16), errorClass: OpenApiError,
|
|
272
|
+
* jsonCode: "openapi/bad-json", inputCode: "openapi/bad-input",
|
|
273
|
+
* label: "openapi.parse",
|
|
274
|
+
* });
|
|
275
|
+
*/
|
|
276
|
+
function parseStringOrObject(input, opts) {
|
|
277
|
+
opts = opts || {};
|
|
278
|
+
var label = opts.label || "safeJson.parseStringOrObject";
|
|
279
|
+
if (typeof input === "string") {
|
|
280
|
+
try { return parse(input, opts); }
|
|
281
|
+
catch (e) {
|
|
282
|
+
if (typeof opts.errorClass === "function") {
|
|
283
|
+
throw new opts.errorClass(opts.jsonCode, label + ": invalid JSON — " + (e && e.message));
|
|
284
|
+
}
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (input !== null && typeof input === "object" &&
|
|
289
|
+
!Buffer.isBuffer(input) && !(input instanceof Uint8Array)) {
|
|
290
|
+
return input;
|
|
291
|
+
}
|
|
292
|
+
if (typeof opts.errorClass === "function") {
|
|
293
|
+
throw new opts.errorClass(opts.inputCode, label + ": input must be a JSON string or a plain object");
|
|
294
|
+
}
|
|
295
|
+
throw new SafeJsonError(label + ": input must be a JSON string or a plain object", "json/wrong-input-type");
|
|
296
|
+
}
|
|
297
|
+
|
|
243
298
|
function _stripProtoKeys(key, value) {
|
|
244
299
|
if (pick.isPoisonedKey(key)) return undefined;
|
|
245
300
|
return value;
|
|
@@ -997,6 +1052,7 @@ function isJsonObject(value) {
|
|
|
997
1052
|
module.exports = {
|
|
998
1053
|
parse: parse,
|
|
999
1054
|
parseOrDefault: parseOrDefault,
|
|
1055
|
+
parseStringOrObject: parseStringOrObject,
|
|
1000
1056
|
isJsonObject: isJsonObject,
|
|
1001
1057
|
stringify: stringify,
|
|
1002
1058
|
stringifyForScript: stringifyForScript,
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
* - Timeout (default: 250ms, max: 10s) terminates the worker.
|
|
32
32
|
* - Heap caps (maxOldGenerationSizeMb / maxYoungGenerationSizeMb)
|
|
33
33
|
* derived from maxBytes; v8 kills the worker on overflow.
|
|
34
|
-
* - Result size cap = maxBytes / 4
|
|
34
|
+
* - Result size cap = min(maxBytes / 4, 64 MiB) - the host re-parses
|
|
35
|
+
* the untrusted result through safeJson, whose hard ceiling bounds
|
|
36
|
+
* it at 64 MiB regardless of a larger maxBytes.
|
|
35
37
|
*
|
|
36
38
|
* Allowed-globals list:
|
|
37
39
|
* The allowed opt names which extra globals operator source may
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
* - sandbox/bad-input - input is not JSON-serializable
|
|
64
66
|
* - sandbox/input-too-large - JSON.stringify(input).length > maxBytes
|
|
65
67
|
* - sandbox/timeout - worker exceeded timeoutMs
|
|
66
|
-
* - sandbox/oversized-result - worker output > maxBytes / 4
|
|
68
|
+
* - sandbox/oversized-result - worker output > min(maxBytes / 4, 64 MiB)
|
|
67
69
|
* - sandbox/parse-error - source did not parse inside the worker
|
|
68
70
|
* - sandbox/runtime-error - operator transform threw
|
|
69
71
|
* - sandbox/spawn-failed - worker thread failed to spawn
|
|
@@ -83,6 +85,7 @@ var lazyRequire = require("./lazy-require");
|
|
|
83
85
|
var validateOpts = require("./validate-opts");
|
|
84
86
|
var numericBounds = require("./numeric-bounds");
|
|
85
87
|
var safeBuffer = require("./safe-buffer");
|
|
88
|
+
var safeJson = require("./safe-json");
|
|
86
89
|
var C = require("./constants");
|
|
87
90
|
var { SandboxError } = require("./framework-error");
|
|
88
91
|
|
|
@@ -236,9 +239,14 @@ function run(opts) {
|
|
|
236
239
|
stackSizeMb: stackMib,
|
|
237
240
|
};
|
|
238
241
|
|
|
239
|
-
// Reserve 1/4 of maxBytes as the per-result hard cap
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
+
// Reserve 1/4 of maxBytes as the per-result hard cap, clamped to the
|
|
243
|
+
// host-side JSON parse ceiling so the worker's output cap and the host's
|
|
244
|
+
// safeJson.parse cap agree exactly. Without the clamp a sandbox with
|
|
245
|
+
// maxBytes over 256 MiB would let the worker emit a result the host then
|
|
246
|
+
// refuses (safeJson hard-caps at ABSOLUTE_MAX_BYTES) — the worker refuses
|
|
247
|
+
// it too, so an oversized untrusted result fails the same way on both
|
|
248
|
+
// sides rather than passing the worker and crossing to a rejecting host.
|
|
249
|
+
var maxResultBytes = Math.min(Math.floor(maxBytes / 4), safeJson.ABSOLUTE_MAX_BYTES);
|
|
242
250
|
|
|
243
251
|
return new Promise(function (resolve, reject) {
|
|
244
252
|
var startedAt = Date.now();
|
|
@@ -312,7 +320,11 @@ function run(opts) {
|
|
|
312
320
|
var peakBytes = (typeof msg.peakBytes === "number") ? msg.peakBytes : 0;
|
|
313
321
|
if (msg.ok) {
|
|
314
322
|
var parsed;
|
|
315
|
-
|
|
323
|
+
// resultJson is the JSON.stringify of UNTRUSTED sandboxed code's return
|
|
324
|
+
// value: parse it through safeJson so a "__proto__" member is stripped
|
|
325
|
+
// and a pathologically deep/large result is bounded (maxResultBytes), not
|
|
326
|
+
// a raw JSON.parse that re-creates the key and is depth-unbounded.
|
|
327
|
+
try { parsed = (msg.resultJson === undefined) ? undefined : safeJson.parse(msg.resultJson, { maxBytes: maxResultBytes }); }
|
|
316
328
|
catch (eParse) {
|
|
317
329
|
_emitAudit("sandbox.run.refused", "failure", {
|
|
318
330
|
reason: "sandbox/bad-result-json", runtimeMs: runtimeMs, peakBytes: peakBytes, sourceBytes: sourceBytes,
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
var canonicalJson = require("./canonical-json");
|
|
51
|
+
var safeJson = require("./safe-json");
|
|
52
|
+
var C = require("./constants");
|
|
51
53
|
var validateOpts = require("./validate-opts");
|
|
52
54
|
var { defineClass } = require("./framework-error");
|
|
53
55
|
|
|
@@ -633,7 +635,10 @@ function serialize(doc) {
|
|
|
633
635
|
// of truth for sort/scrub behaviour across the framework (rule
|
|
634
636
|
// §canonicalize).
|
|
635
637
|
var canonical = canonicalJson.stringify(doc);
|
|
636
|
-
|
|
638
|
+
// Re-indent the canonical bytes for human-diffable output. allowProto keeps
|
|
639
|
+
// the canonicalizer's exact key set (this is a faithful re-format of trusted
|
|
640
|
+
// framework output, not a defense boundary); the size cap bounds the parse.
|
|
641
|
+
return JSON.stringify(safeJson.parse(canonical, { allowProto: true, maxBytes: C.BYTES.mib(16) }), null, 2);
|
|
637
642
|
}
|
|
638
643
|
|
|
639
644
|
module.exports = {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.28",
|
|
4
|
+
"date": "2026-06-25",
|
|
5
|
+
"headline": "`b.network.dns` and `b.network.tls` errors now carry a usable terminal-vs-transient signal on `err.permanent`, so a caller's retry loop re-attempts only the failures a retry can fix",
|
|
6
|
+
"summary": "DnsError was declared always-transient and NetworkTlsError always-permanent, so a consumer driving its own retry loop got the wrong signal in both directions: it would retry a permanent DNS failure (a bad host, bad options, an unsupported query, an NXDOMAIN-style no-result, or a caller-shape/config error such as an unconfigured transport or an invalid resolver list) forever, and it would refuse to retry a transient TLS-over-network failure (an ECH connection failure, a handshake timeout, DNS momentarily unavailable). err.permanent now reflects each error's actual transience, derived from its code and failing closed (an unknown code is permanent, so a hopeless target is not retried indefinitely): for DnsError only a failed network round-trip (a lookup timeout, a resolve/reverse query that failed on the wire, a failed DoH/DoT exchange, a DDR discovery that found nothing) is transient — config, input, and environment errors raised before any network work are permanent; for NetworkTlsError only the network-layer ECH failures are transient. TlsTrustError remains always-permanent by design — a trust-verification failure (bad CA, fingerprint mismatch, OCSP not-good, CT violation, an unreachable OCSP responder) must never be silently retried past the trust decision.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "DnsError / NetworkTlsError expose a correct terminal/transient signal; TlsTrustError stays terminal",
|
|
13
|
+
"body": "b.network.dns's DnsError was always-transient and b.network.tls's NetworkTlsError always-permanent, so err.permanent misled a consumer's retry loop in both directions — retrying a permanent DNS error (bad host / options / unsupported type / no-result) indefinitely, and never retrying a transient TLS network error (ECH connect failure, handshake timeout, DNS unavailable). err.permanent now reflects each error's actual transience by code and fails closed (unknown codes are permanent): DnsError is transient only for a failed network round-trip (dns/lookup-timeout, dns/resolve-failed, dns/reverse-failed, dns/doh-failed, dns/dot-failed, dns/dot-handshake-failed, dns/ddr-not-discovered, dns/system-failed), and permanent otherwise — including the caller-shape / environment errors raised before any network work (dns/transport-unavailable for an unconfigured transport, dns/dnr-no-resolvers for an empty/invalid resolver list, dns/setservers-failed for an invalid address, dns/no-system-resolvers when none are configured). NetworkTlsError is transient only for the network-layer ECH failures (tls/ech-connect-failed, tls/ech-timeout, tls/ech-dns-unavailable), permanent otherwise. TlsTrustError remains always-permanent so a trust-verification failure is never silently retried."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.29",
|
|
4
|
+
"date": "2026-06-25",
|
|
5
|
+
"headline": "JSON parsing of operator and untrusted input is routed through `b.safeJson` everywhere, so a `\"__proto__\"` member is stripped and the parse is size-bounded — `b.openapi.parse`, `b.asyncapi.parse`, and `b.sandbox.run` were using a raw `JSON.parse`",
|
|
6
|
+
"summary": "b.openapi.parse and b.asyncapi.parse parsed an operator-supplied document string with a raw JSON.parse, which re-creates a \"__proto__\" member as an own key and imposes no size bound; b.sandbox.run parsed the untrusted sandboxed code's serialized result the same way (proto member kept, depth unbounded). All three now route through b.safeJson, which strips the prototype-pollution keys and applies the depth / key / size caps. A new b.safeJson.parseStringOrObject primitive captures the recurring \"accept a document as a JSON string OR a pre-built object\" shape (a string is parsed safely, an object passes through) that openapi and asyncapi had each hand-rolled, with each consumer's typed error class and a generous document size cap passed as data. Valid input parses identically to before; a malicious or oversized document is now defused rather than accepted.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Security",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Operator and untrusted JSON parses strip __proto__ and bound size (no raw JSON.parse)",
|
|
13
|
+
"body": "A raw JSON.parse keeps a \"__proto__\" object member as an own key (a prototype-pollution gadget for any downstream merge) and parses unbounded input. b.openapi.parse and b.asyncapi.parse used it on operator document strings, and b.sandbox.run used it on the untrusted sandboxed code's JSON-serialized result. All now route through b.safeJson — the prototype-pollution keys are stripped and the depth / key / size caps apply (a 16 MiB document cap for openapi/asyncapi, the existing result-bytes cap for the sandbox). Internal JSON parses (the sealed DSR store payloads, the VEX canonical re-format) route through the same primitive too, so the guarantee cannot be bypassed one call site at a time. Valid input is unaffected; a \"__proto__\"-laden or oversized payload is defused."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Added",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "b.safeJson.parseStringOrObject(input, opts?)",
|
|
22
|
+
"body": "Accepts either a JSON string — parsed through b.safeJson.parse, so the prototype-pollution-key strip and depth / key / size caps apply — or an already-decoded plain object (returned unchanged). For the recurring \"operator hands me a document as a JSON string or a pre-built object\" surface (b.openapi / b.asyncapi). The consumer's typed error class, error codes, label, and size cap are passed as options, so a raw JSON.parse on operator input cannot be hand-rolled per consumer."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -13645,6 +13645,41 @@ function testJsonParse() {
|
|
|
13645
13645
|
check("parse accepts Buffer input", fromBuf.y === 2);
|
|
13646
13646
|
}
|
|
13647
13647
|
|
|
13648
|
+
function testJsonParseStringOrObject() {
|
|
13649
|
+
// String → parsed through parse() (proto strip + caps); valid input is
|
|
13650
|
+
// byte-identical to a raw JSON.parse.
|
|
13651
|
+
var fromStr = b.safeJson.parseStringOrObject('{"a":1,"b":[2,3]}');
|
|
13652
|
+
check("parseStringOrObject parses a string", fromStr.a === 1 && fromStr.b[1] === 3);
|
|
13653
|
+
// Object → passed through unchanged (same reference).
|
|
13654
|
+
var obj = { openapi: "3.0.0" };
|
|
13655
|
+
check("parseStringOrObject passes an object through", b.safeJson.parseStringOrObject(obj) === obj);
|
|
13656
|
+
// The proto-pollution defense applies to the STRING path (a raw JSON.parse
|
|
13657
|
+
// would keep "__proto__" as an own key); the marker'd hand-rolls bypassed it.
|
|
13658
|
+
var stripped = b.safeJson.parseStringOrObject('{"__proto__":{"isAdmin":true},"name":"alice"}');
|
|
13659
|
+
check("parseStringOrObject strips __proto__ from a string", stripped.isAdmin === undefined && stripped.name === "alice");
|
|
13660
|
+
check("parseStringOrObject does not pollute Object.prototype", ({}).isAdmin === undefined);
|
|
13661
|
+
// maxBytes is enforced on the string path.
|
|
13662
|
+
var capped = false;
|
|
13663
|
+
try { b.safeJson.parseStringOrObject('{"x":"' + new Array(2000).join("y") + '"}', { maxBytes: 100 }); }
|
|
13664
|
+
catch (e) { capped = e.code === "json/too-large"; }
|
|
13665
|
+
check("parseStringOrObject enforces maxBytes", capped);
|
|
13666
|
+
// A typed errorClass wraps invalid JSON / wrong input type with the caller's codes.
|
|
13667
|
+
function MyErr(code, msg) { this.code = code; this.message = msg; }
|
|
13668
|
+
var jc = null;
|
|
13669
|
+
try { b.safeJson.parseStringOrObject("{not json", { errorClass: MyErr, jsonCode: "x/bad-json", inputCode: "x/bad-input" }); }
|
|
13670
|
+
catch (e) { jc = e.code; }
|
|
13671
|
+
check("parseStringOrObject wraps invalid JSON via errorClass", jc === "x/bad-json");
|
|
13672
|
+
var ic = null;
|
|
13673
|
+
try { b.safeJson.parseStringOrObject(42, { errorClass: MyErr, jsonCode: "x/bad-json", inputCode: "x/bad-input" }); }
|
|
13674
|
+
catch (e) { ic = e.code; }
|
|
13675
|
+
check("parseStringOrObject wraps wrong input via errorClass", ic === "x/bad-input");
|
|
13676
|
+
// Without errorClass, a non-string/non-object throws the framework SafeJsonError.
|
|
13677
|
+
var sje = false;
|
|
13678
|
+
try { b.safeJson.parseStringOrObject(123); }
|
|
13679
|
+
catch (e) { sje = e.code === "json/wrong-input-type"; }
|
|
13680
|
+
check("parseStringOrObject throws SafeJsonError without errorClass", sje);
|
|
13681
|
+
}
|
|
13682
|
+
|
|
13648
13683
|
function testJsonStringify() {
|
|
13649
13684
|
var s = b.safeJson.stringify({ a: 1, b: [1, 2, 3] });
|
|
13650
13685
|
check("stringify produces valid JSON", JSON.parse(s).a === 1);
|
|
@@ -19005,6 +19040,7 @@ async function run() {
|
|
|
19005
19040
|
// json-safe primitive
|
|
19006
19041
|
testJsonModuleSurface();
|
|
19007
19042
|
testJsonParse();
|
|
19043
|
+
testJsonParseStringOrObject();
|
|
19008
19044
|
testJsonStringify();
|
|
19009
19045
|
testJsonCanonical();
|
|
19010
19046
|
testJsonValidate();
|
|
@@ -19598,6 +19634,7 @@ module.exports = {
|
|
|
19598
19634
|
testLazyRequire: testLazyRequire,
|
|
19599
19635
|
testJsonModuleSurface: testJsonModuleSurface,
|
|
19600
19636
|
testJsonParse: testJsonParse,
|
|
19637
|
+
testJsonParseStringOrObject: testJsonParseStringOrObject,
|
|
19601
19638
|
testJsonStringify: testJsonStringify,
|
|
19602
19639
|
testJsonCanonical: testJsonCanonical,
|
|
19603
19640
|
testJsonValidate: testJsonValidate,
|
|
@@ -124,7 +124,7 @@ function testCreateRefusesBadStartMode() {
|
|
|
124
124
|
check("create refuses bad startMode", threw);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
function run() {
|
|
128
128
|
testOpenModePassesEverything();
|
|
129
129
|
testReadOnlyModeRefusesWrites();
|
|
130
130
|
testLockedModeRefusesEverythingExceptPassthrough();
|
|
@@ -133,4 +133,7 @@ function testCreateRefusesBadStartMode() {
|
|
|
133
133
|
testSetRefusesUnknownMode();
|
|
134
134
|
testCreateRefusesBadStartMode();
|
|
135
135
|
console.log("OK — access-lock tests");
|
|
136
|
-
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { run: run };
|
|
139
|
+
if (require.main === module) run();
|
|
@@ -18,7 +18,7 @@ function _mockRes() {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
function run() {
|
|
22
22
|
var aboveCalls = 0;
|
|
23
23
|
var ag = b.middleware.ageGate({
|
|
24
24
|
audit: false,
|
|
@@ -88,4 +88,7 @@ function _mockRes() {
|
|
|
88
88
|
check("ageGate refuses non-fn getAge", threwBadGetAge);
|
|
89
89
|
|
|
90
90
|
console.log("OK — ageGate tests");
|
|
91
|
-
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { run: run };
|
|
94
|
+
if (require.main === module) run();
|
|
@@ -299,7 +299,6 @@ var VALID_ALLOW_CLASSES = {
|
|
|
299
299
|
"backup-adapter-storage-without-posture-check": 1,
|
|
300
300
|
"bare-canonicalize-walk": 1,
|
|
301
301
|
"bare-error-throw": 1,
|
|
302
|
-
"bare-json-parse": 1,
|
|
303
302
|
"bare-split-on-quoted-header": 1,
|
|
304
303
|
"console-direct": 1,
|
|
305
304
|
"deny-path-hardcoded-response": 1,
|
|
@@ -1595,17 +1594,18 @@ function testRawNewURL() {
|
|
|
1595
1594
|
// ---- Pattern 14: silent JSON.parse() on operator-supplied data ----
|
|
1596
1595
|
|
|
1597
1596
|
function testNoBareJsonParse() {
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1597
|
+
// Inverse guard (NO allow path): every JSON parse in lib/ routes through
|
|
1598
|
+
// `safeJson.parse` / `safeJson.parseStringOrObject`, which add the
|
|
1599
|
+
// proto-pollution-key strip + depth / key / size caps a raw `JSON.parse`
|
|
1600
|
+
// lacks. A raw `JSON.parse` re-creates a `"__proto__"` member as an own key
|
|
1601
|
+
// and is unbounded — even "internal" callers (a sealed store payload, a
|
|
1602
|
+
// sandbox worker's result, a canonical re-format) route through the primitive
|
|
1603
|
+
// so the guarantee can't be bypassed one site at a time. safe-json.js IS the
|
|
1604
|
+
// wrapper, so its own `JSON.parse` (the thing being wrapped) is the sole site.
|
|
1601
1605
|
var matches = _scan(/\bJSON\.parse\(/);
|
|
1602
|
-
// safe-json.js IS the safe wrapper; the bare JSON.parse call lives
|
|
1603
|
-
// there by definition (it's what safe-json wraps with maxBytes /
|
|
1604
|
-
// depth / proto-pollution defenses).
|
|
1605
1606
|
matches = matches.filter(function (m) { return m.file !== "lib/safe-json.js"; });
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
"(or has allow marker)",
|
|
1607
|
+
_report("JSON parse in lib/ routes through safeJson.parse / safeJson.parseStringOrObject " +
|
|
1608
|
+
"(no raw JSON.parse — use the primitive; it carries the proto / depth / size defenses)",
|
|
1609
1609
|
matches);
|
|
1610
1610
|
}
|
|
1611
1611
|
|
|
@@ -3,7 +3,7 @@ var helpers = require("../helpers");
|
|
|
3
3
|
var b = helpers.b;
|
|
4
4
|
var check = helpers.check;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function run() {
|
|
7
7
|
var eaa = b.compliance.eaa.create({
|
|
8
8
|
audit: false,
|
|
9
9
|
productName: "Acme Portal",
|
|
@@ -33,4 +33,7 @@ var check = helpers.check;
|
|
|
33
33
|
check("eaa refuses bad conformance", threwBadConformance);
|
|
34
34
|
|
|
35
35
|
console.log("OK — compliance.eaa tests");
|
|
36
|
-
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { run: run };
|
|
39
|
+
if (require.main === module) run();
|
|
@@ -833,6 +833,42 @@ async function testDbStoreSealsAtRest() {
|
|
|
833
833
|
}
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
async function testDbStoreLargePayloadRoundTrips() {
|
|
837
|
+
// A completed access/portability ticket can carry a data export larger
|
|
838
|
+
// than safeJson's 1 MiB parse default. The store reads its payload column
|
|
839
|
+
// back with the store-matched ceiling, so a multi-MiB ticket round-trips
|
|
840
|
+
// through get() and list() rather than failing json/too-large.
|
|
841
|
+
var tmpDir = _tmp();
|
|
842
|
+
await setupTestDb(tmpDir);
|
|
843
|
+
try {
|
|
844
|
+
var h = _dbDsr();
|
|
845
|
+
var bigExport = "x".repeat(C.BYTES.mib(2)); // ~2 MiB > the 1 MiB read default
|
|
846
|
+
var ticket = {
|
|
847
|
+
id: "dsr-big-1",
|
|
848
|
+
type: "access",
|
|
849
|
+
status: "completed",
|
|
850
|
+
subject: { subjectId: "u-1", email: "alice@example.com", phone: "+15550001111" },
|
|
851
|
+
submittedAt: Date.now(),
|
|
852
|
+
deadlineAt: Date.now() + C.TIME.minutes(1),
|
|
853
|
+
processedAt: Date.now(),
|
|
854
|
+
verificationLevel: "primary",
|
|
855
|
+
posture: "gdpr",
|
|
856
|
+
export: bigExport,
|
|
857
|
+
};
|
|
858
|
+
await h.store.insert(ticket);
|
|
859
|
+
|
|
860
|
+
var got = await h.store.get("dsr-big-1");
|
|
861
|
+
check("dbStore: >1 MiB ticket round-trips through get()",
|
|
862
|
+
got && got.export === bigExport && got.status === "completed");
|
|
863
|
+
|
|
864
|
+
var listed = await h.store.list({ subject: { email: "alice@example.com" } });
|
|
865
|
+
check("dbStore: >1 MiB ticket round-trips through list()",
|
|
866
|
+
listed.length === 1 && listed[0].export === bigExport);
|
|
867
|
+
} finally {
|
|
868
|
+
await teardownTestDb(tmpDir);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
836
872
|
async function testDbStoreErasurePurgesPriorTickets() {
|
|
837
873
|
var tmpDir = _tmp();
|
|
838
874
|
await setupTestDb(tmpDir);
|
|
@@ -1056,6 +1092,7 @@ async function run() {
|
|
|
1056
1092
|
|
|
1057
1093
|
// dbTicketStore at-rest sealing + erasure purge + upgrade path
|
|
1058
1094
|
await testDbStoreSealsAtRest();
|
|
1095
|
+
await testDbStoreLargePayloadRoundTrips();
|
|
1059
1096
|
await testDbStoreErasurePurgesPriorTickets();
|
|
1060
1097
|
await testDbStoreUpgradePath();
|
|
1061
1098
|
await testDbStoreFindsLegacyKeyedMacRows();
|
|
@@ -3,7 +3,7 @@ var helpers = require("../helpers");
|
|
|
3
3
|
var b = helpers.b;
|
|
4
4
|
var check = helpers.check;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function run() {
|
|
7
7
|
var ropa = b.gdpr.ropa.create({
|
|
8
8
|
audit: false,
|
|
9
9
|
controller: { name: "Acme", contact: "dpo@acme.example" },
|
|
@@ -38,4 +38,7 @@ var check = helpers.check;
|
|
|
38
38
|
check("ropa.remove deletes", ropa.list().length === 0);
|
|
39
39
|
|
|
40
40
|
console.log("OK — gdpr.ropa tests");
|
|
41
|
-
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { run: run };
|
|
44
|
+
if (require.main === module) run();
|
|
@@ -844,7 +844,7 @@ function testFormsStandalone() {
|
|
|
844
844
|
|
|
845
845
|
// ---- Run all ----
|
|
846
846
|
|
|
847
|
-
|
|
847
|
+
function run() {
|
|
848
848
|
testHtmlLangMissing();
|
|
849
849
|
testHtmlLangPresent();
|
|
850
850
|
testPageTitleMissing();
|
|
@@ -915,4 +915,7 @@ function testFormsStandalone() {
|
|
|
915
915
|
testInputEmailNoAutocomplete();
|
|
916
916
|
testSkipForms();
|
|
917
917
|
testFormsStandalone();
|
|
918
|
-
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
module.exports = { run: run };
|
|
921
|
+
if (require.main === module) run();
|
|
@@ -220,6 +220,35 @@ async function _drainTcpHandles() {
|
|
|
220
220
|
}, { timeoutMs: 5000, label: "network-dns: TCP handle drain after _resetForTest" });
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// DnsError carries a terminal-vs-transient signal on err.permanent so a caller
|
|
224
|
+
// driving a retry loop re-attempts only the failures a retry can fix. Fails
|
|
225
|
+
// CLOSED: network / resolver-availability failures are transient; bad config,
|
|
226
|
+
// malformed input, NXDOMAIN-style no-result, and any unknown code are permanent.
|
|
227
|
+
function testDnsErrorPermanentClassification() {
|
|
228
|
+
var DnsError = dnsModule.DnsError;
|
|
229
|
+
check("DnsError exported", typeof DnsError === "function");
|
|
230
|
+
// Permanent — config / input / no-answer (retrying cannot fix these).
|
|
231
|
+
check("DnsError dns/bad-host is permanent", new DnsError("dns/bad-host", "x").permanent === true);
|
|
232
|
+
check("DnsError dns/bad-transport is permanent", new DnsError("dns/bad-transport", "x").permanent === true);
|
|
233
|
+
check("DnsError dns/unsupported-type is permanent", new DnsError("dns/unsupported-type", "x").permanent === true);
|
|
234
|
+
check("DnsError dns/no-result is permanent", new DnsError("dns/no-result", "x").permanent === true);
|
|
235
|
+
check("DnsError unknown code is permanent (fail closed)", new DnsError("dns/never-defined", "x").permanent === true);
|
|
236
|
+
// Caller-shape / environment config errors raised before any network work are
|
|
237
|
+
// permanent — a retry cannot make absent config or invalid input valid.
|
|
238
|
+
check("DnsError dns/transport-unavailable is permanent (transport not configured)",
|
|
239
|
+
new DnsError("dns/transport-unavailable", "x").permanent === true);
|
|
240
|
+
check("DnsError dns/dnr-no-resolvers is permanent (empty/invalid resolver list)",
|
|
241
|
+
new DnsError("dns/dnr-no-resolvers", "x").permanent === true);
|
|
242
|
+
check("DnsError dns/setservers-failed is permanent (invalid resolver address)",
|
|
243
|
+
new DnsError("dns/setservers-failed", "x").permanent === true);
|
|
244
|
+
check("DnsError dns/no-system-resolvers is permanent (none configured)",
|
|
245
|
+
new DnsError("dns/no-system-resolvers", "x").permanent === true);
|
|
246
|
+
// Transient — a network round-trip that a retry can plausibly fix.
|
|
247
|
+
check("DnsError dns/lookup-timeout is transient", new DnsError("dns/lookup-timeout", "x").permanent === false);
|
|
248
|
+
check("DnsError dns/resolve-failed is transient", new DnsError("dns/resolve-failed", "x").permanent === false);
|
|
249
|
+
check("DnsError dns/dot-failed is transient", new DnsError("dns/dot-failed", "x").permanent === false);
|
|
250
|
+
}
|
|
251
|
+
|
|
223
252
|
async function run() {
|
|
224
253
|
try { await _runTests(); }
|
|
225
254
|
finally { await _drainTcpHandles(); }
|
|
@@ -228,6 +257,8 @@ async function run() {
|
|
|
228
257
|
async function _runTests() {
|
|
229
258
|
_resetAll();
|
|
230
259
|
|
|
260
|
+
testDnsErrorPermanentClassification();
|
|
261
|
+
|
|
231
262
|
// ============================================================
|
|
232
263
|
// Item B4 — SVCB / HTTPS rdata parsing
|
|
233
264
|
// ============================================================
|
|
@@ -452,7 +452,40 @@ function testInsecureTlsAudit() {
|
|
|
452
452
|
check("auditInsecureTls is drop-silent on bad input (never throws into a connect)", threw === false);
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// NetworkTlsError carries a terminal-vs-transient signal on err.permanent;
|
|
456
|
+
// TlsTrustError is always permanent (a trust-verification failure must never be
|
|
457
|
+
// silently retried). Fails CLOSED: only the network-layer ECH failures are
|
|
458
|
+
// transient; bad options, malformed config, PKIX validation, and unknown codes
|
|
459
|
+
// are permanent.
|
|
460
|
+
function testNetworkTlsErrorPermanentClassification() {
|
|
461
|
+
var NetworkTlsError = b.network.tls.NetworkTlsError;
|
|
462
|
+
var TlsTrustError = b.network.tls.TlsTrustError;
|
|
463
|
+
// NetworkTlsError — permanent (config / validation, retry cannot fix).
|
|
464
|
+
check("NetworkTlsError bad-tls-options is permanent",
|
|
465
|
+
new NetworkTlsError("network-tls/bad-tls-options", "x").permanent === true);
|
|
466
|
+
check("NetworkTlsError pkix-hostname-mismatch is permanent",
|
|
467
|
+
new NetworkTlsError("tls/pkix-hostname-mismatch", "x").permanent === true);
|
|
468
|
+
check("NetworkTlsError ech-config-malformed is permanent",
|
|
469
|
+
new NetworkTlsError("tls/ech-config-malformed", "x").permanent === true);
|
|
470
|
+
check("NetworkTlsError unknown code is permanent (fail closed)",
|
|
471
|
+
new NetworkTlsError("tls/never-defined", "x").permanent === true);
|
|
472
|
+
// NetworkTlsError — transient (network-layer ECH failure, a retry may succeed).
|
|
473
|
+
check("NetworkTlsError ech-connect-failed is transient",
|
|
474
|
+
new NetworkTlsError("tls/ech-connect-failed", "x").permanent === false);
|
|
475
|
+
check("NetworkTlsError ech-timeout is transient",
|
|
476
|
+
new NetworkTlsError("tls/ech-timeout", "x").permanent === false);
|
|
477
|
+
check("NetworkTlsError ech-dns-unavailable is transient",
|
|
478
|
+
new NetworkTlsError("tls/ech-dns-unavailable", "x").permanent === false);
|
|
479
|
+
// TlsTrustError — ALWAYS permanent (trust failures, incl. a network failure
|
|
480
|
+
// during the trust check, must not auto-retry past a trust decision).
|
|
481
|
+
check("TlsTrustError ocsp-not-good is permanent",
|
|
482
|
+
new TlsTrustError("tls/ocsp-not-good", "x").permanent === true);
|
|
483
|
+
check("TlsTrustError connect-failed during trust is still permanent",
|
|
484
|
+
new TlsTrustError("tls/connect-failed", "x").permanent === true);
|
|
485
|
+
}
|
|
486
|
+
|
|
455
487
|
async function run() {
|
|
488
|
+
testNetworkTlsErrorPermanentClassification();
|
|
456
489
|
testInsecureTlsAudit();
|
|
457
490
|
testEchSurface();
|
|
458
491
|
testEchParseDraft22();
|
|
@@ -24,11 +24,14 @@ function _mockRes() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
function run() {
|
|
28
28
|
var requireMtls = b.middleware.requireMtls({ audit: false });
|
|
29
29
|
var noPeerRes = _mockRes();
|
|
30
30
|
requireMtls(_mockReq({ authorized: false }), noPeerRes, function () {});
|
|
31
31
|
check("requireMtls refuses unauthorized peer 401", noPeerRes._captured.status === 401);
|
|
32
32
|
|
|
33
33
|
console.log("OK — requireMtls tests");
|
|
34
|
-
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { run: run };
|
|
37
|
+
if (require.main === module) run();
|
|
@@ -124,7 +124,7 @@ function testConstructorAttributeNoFalseDuplicate() {
|
|
|
124
124
|
threw && threw.code === "xml/forbidden-name");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
function run() {
|
|
128
128
|
try {
|
|
129
129
|
testParsesAttributedElement();
|
|
130
130
|
testRejectsDoctype();
|
|
@@ -140,4 +140,7 @@ function testConstructorAttributeNoFalseDuplicate() {
|
|
|
140
140
|
process.exit(1);
|
|
141
141
|
}
|
|
142
142
|
console.log("OK — safe-xml tests");
|
|
143
|
-
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { run: run };
|
|
146
|
+
if (require.main === module) run();
|
package/package.json
CHANGED