@blamejs/blamejs-shop 0.4.108 → 0.4.110

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.110 (2026-06-26) — **Inbound webhook bodies and external API responses are parsed through the size-bounded, prototype-pollution-stripping JSON primitive instead of a raw parse.** The remaining places that parsed externally-supplied JSON with a raw JSON.parse — the Stripe and PayPal inbound webhook bodies, the captcha provider's verification response, and the Stripe / PayPal / R2 API responses — now route through the framework's safe-JSON parser, which strips prototype-pollution keys ("__proto__") and bounds parse depth, key count, and byte size. This matters most for the PayPal webhook body, which is parsed up front before its signature is verified, and for every inbound or provider response that previously had no size bound. An audit confirmed the rest of the app's JSON parsing already reads only values it wrote itself to the database (validated at write time) or already used the safe parser, so those are unchanged. Valid payloads parse identically; a malformed, prototype-pollution-laden, or oversized body is now defused. No configuration changes. **Changed:** *Externally-supplied JSON (webhook bodies, provider responses) is parsed with the safe-JSON primitive* — The inbound Stripe and PayPal webhook bodies, the captcha siteverify response, and the Stripe / PayPal / R2 API responses were parsed with a raw JSON.parse, which keeps a "__proto__" member as an own key and imposes no size bound. These now route through the framework's safe-JSON parser (prototype-pollution-key strip + depth / key / size caps), so an inbound or external payload can no longer carry a prototype-pollution gadget or force an unbounded parse — notably the PayPal webhook body, which is decoded before its signature is verified. The change is parse-only: valid payloads decode exactly as before, each call keeps its existing error fallback, and the rest of the app's JSON parsing (values it serialized to its own database, validated at write time) is left as-is.
12
+
13
+ - 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.
14
+
11
15
  - 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.
12
16
 
13
17
  - 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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.108",
2
+ "version": "0.4.110",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
package/lib/payment.js CHANGED
@@ -280,7 +280,7 @@ async function _verifyWebhook(headers, rawBody, secret, opts) {
280
280
  }
281
281
 
282
282
  try {
283
- return { ok: true, event: JSON.parse(rawBody) };
283
+ return { ok: true, event: b.safeJson.parse(rawBody) };
284
284
  } catch (_e) {
285
285
  return { ok: false, reason: "bad-json" };
286
286
  }
@@ -335,7 +335,7 @@ async function _stripeCall(opts, method, path, params, idempotencyKey) {
335
335
  });
336
336
  var text = res.body && res.body.toString ? res.body.toString("utf8") : "";
337
337
  var parsed = null;
338
- try { parsed = text.length ? JSON.parse(text) : {}; } catch (_e) { parsed = { _raw: text }; }
338
+ try { parsed = text.length ? b.safeJson.parse(text) : {}; } catch (_e) { parsed = { _raw: text }; }
339
339
  if (res.statusCode < 200 || res.statusCode >= 300) {
340
340
  var err = new Error("stripe: " + method + " " + path + " → HTTP " + res.statusCode +
341
341
  (parsed && parsed.error && parsed.error.message ? " — " + parsed.error.message : ""));
@@ -919,7 +919,7 @@ async function _paypalToken(opts, state) {
919
919
  allowedHosts: [_paypalAllowedHost(opts)],
920
920
  });
921
921
  var text = res.body && res.body.toString ? res.body.toString("utf8") : "";
922
- var parsed; try { parsed = text.length ? JSON.parse(text) : {}; } catch (_e) { parsed = {}; }
922
+ var parsed; try { parsed = text.length ? b.safeJson.parse(text) : {}; } catch (_e) { parsed = {}; }
923
923
  if (res.statusCode < 200 || res.statusCode >= 300 || !parsed.access_token) {
924
924
  var err = new Error("paypal: OAuth2 token exchange failed → HTTP " + res.statusCode +
925
925
  (parsed && parsed.error_description ? " — " + parsed.error_description : ""));
@@ -974,7 +974,7 @@ async function _paypalCall(opts, state, method, path, bodyObj, requestId, breake
974
974
  allowedHosts: [allowedHost],
975
975
  });
976
976
  var text = res.body && res.body.toString ? res.body.toString("utf8") : "";
977
- var parsed; try { parsed = text.length ? JSON.parse(text) : {}; } catch (_e) { parsed = { _raw: text }; }
977
+ var parsed; try { parsed = text.length ? b.safeJson.parse(text) : {}; } catch (_e) { parsed = { _raw: text }; }
978
978
  if (res.statusCode < 200 || res.statusCode >= 300) {
979
979
  var detail = parsed && (parsed.message || (parsed.details && parsed.details[0] && parsed.details[0].description)) || "";
980
980
  var err = new Error("paypal: " + method + " " + path + " → HTTP " + res.statusCode + (detail ? " — " + detail : ""));
@@ -1130,7 +1130,7 @@ function paypal(opts) {
1130
1130
  return { ok: false, reason: "missing-transmission-headers" };
1131
1131
  }
1132
1132
  var event;
1133
- try { event = typeof rawBody === "string" ? JSON.parse(rawBody) : rawBody; }
1133
+ try { event = typeof rawBody === "string" ? b.safeJson.parse(rawBody) : rawBody; }
1134
1134
  catch (_e) { return { ok: false, reason: "malformed-body" }; }
1135
1135
  var verifyBody = {
1136
1136
  auth_algo: authAlgo,
package/lib/r2-bridge.js CHANGED
@@ -140,7 +140,7 @@ function create(opts) {
140
140
  }
141
141
  var text = res.body && res.body.toString ? res.body.toString("utf8") : "";
142
142
  var json = null;
143
- try { json = text.length ? JSON.parse(text) : null; } catch (_e) { json = null; }
143
+ try { json = text.length ? b.safeJson.parse(text) : null; } catch (_e) { json = null; }
144
144
  if (res.statusCode < 200 || res.statusCode >= 300) {
145
145
  var err = _httpErr(res.statusCode, "upload");
146
146
  if (json && json.error) {
@@ -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.28",
7
- "tag": "v0.15.28",
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",
@@ -850,6 +850,7 @@
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
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",
853
854
  "release-notes/v0.15.3.json": "lib/vendor/blamejs/release-notes/v0.15.3.json",
854
855
  "release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
855
856
  "release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
@@ -1559,7 +1560,7 @@
1559
1560
  ".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
1560
1561
  ".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
1561
1562
  "ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
1562
- "CHANGELOG.md": "sha256:44632bfff523c7ec344cf3394b536b476c3ff41ec7c2926f50d5f8b8322a863d",
1563
+ "CHANGELOG.md": "sha256:12cbd8cd887ee507d270a0be83d2e5572cb034eaf7d47ff9ddaf69322b57a817",
1563
1564
  "CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
1564
1565
  "CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
1565
1566
  "GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
@@ -1569,7 +1570,7 @@
1569
1570
  "NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
1570
1571
  "README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
1571
1572
  "SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
1572
- "api-snapshot.json": "sha256:99a6f96b8f2044cace0fa369fd1cebc772043fd6bb1fdb76c722a5cb0729a502",
1573
+ "api-snapshot.json": "sha256:58e5c4106b1fd4f10e1b838057abc26b4f28d2f2b6f1396ddcb5e72021c8623b",
1573
1574
  "assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
1574
1575
  "assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
1575
1576
  "bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
@@ -1813,7 +1814,7 @@
1813
1814
  "lib/asn1-der.js": "sha256:bf085ff06bc12f08207ee641e63373ea2bf20e3dd52b8f6f026fd67700cf905c",
1814
1815
  "lib/asyncapi-bindings.js": "sha256:2f6f3d7ea836d0483e40f08b8202d78bae54bdc3d76b5faf57d918e699ebeac8",
1815
1816
  "lib/asyncapi-traits.js": "sha256:82ec58b77f0a65ef1b5e8b67ee9555e9cb984de769c3393fe5c6d4d0640546f0",
1816
- "lib/asyncapi.js": "sha256:c26da54ab6760da0043178abb85960e857b6e8f77f31dc3ced8db95760e25a00",
1817
+ "lib/asyncapi.js": "sha256:cd48847ed363023e248cf0fbe275b545b0002b85e93ed91315990ef8a6abe483",
1817
1818
  "lib/atomic-file.js": "sha256:b93b008c121f785d149d4450681467b18785781c3a82b9b001bb2a7d636c3ad1",
1818
1819
  "lib/audit-chain.js": "sha256:1c9895a2bbf6ce3b4c546b1b3c95ac9d5ed55d82a2b58c0de4e3ecc1947c3c31",
1819
1820
  "lib/audit-daily-review.js": "sha256:61cebe47d6e5eeeedcab45515473acfaab623217810a7ab3629b3aeca5028519",
@@ -1848,7 +1849,7 @@
1848
1849
  "lib/auth/sd-jwt-vc-disclosure.js": "sha256:bc1eff5def71d2eedb6f17c8bede650050af9d790145e8697871c75ddc8431ae",
1849
1850
  "lib/auth/sd-jwt-vc-holder.js": "sha256:9d22b691e6a698aa2240c3ac611bc91e5b8d6acaa4b8fa03e28077ddb5fd56df",
1850
1851
  "lib/auth/sd-jwt-vc-issuer.js": "sha256:8cbfd25af7df5db7e7f61d004059530c46cc18fd1afbcebbe94f8f81f503c413",
1851
- "lib/auth/sd-jwt-vc.js": "sha256:3485b354f420e8e6fe4d5231dd00e4037dcc7e0a014695e3ed5a20ec535d3815",
1852
+ "lib/auth/sd-jwt-vc.js": "sha256:717df0e7162e11c76e47221d6877d2bf07dba9974c58cf84719bb365fce52ee0",
1852
1853
  "lib/auth/status-list.js": "sha256:2861dafbe8dab3d6fba161663850c35acf2eb1d29c481dd030339c5c013602a2",
1853
1854
  "lib/auth/step-up-policy.js": "sha256:dca5810bd13d1e4d279b9d34b3e777cf2455c938502b25b41c773e513d90b379",
1854
1855
  "lib/auth/step-up.js": "sha256:3c426fc31ccf4180a9958f3c9e81573fd90ea7afa7688e52b8547a74ea987fbd",
@@ -1932,7 +1933,7 @@
1932
1933
  "lib/dora.js": "sha256:1b2ed45582623978c9f1087a0a45b6707d1a3d7b5da9eb420f780ec44c6ddd37",
1933
1934
  "lib/dr-runbook.js": "sha256:4da7ce04f88e79e103c8cb349d833b716259b99dff4fceb40d2edbac638e6cbf",
1934
1935
  "lib/dsa.js": "sha256:93240f4a5477e06a1f76c90ddf27f0209324261bc841b237aa6d6023b796a32b",
1935
- "lib/dsr.js": "sha256:1d2cc42afffbf40ea917a1a8a952f77b87398c13d03e0e4107494f1cc27db545",
1936
+ "lib/dsr.js": "sha256:784102f593cf3f42bda74288b291f5005fef7bf4f8569ef60f32d5fe1b0c7231",
1936
1937
  "lib/dual-control.js": "sha256:7b0bc61722be7df45d2ae161ed2a2ec7e33ac5f118175f29b4c58fec364b54e3",
1937
1938
  "lib/early-hints.js": "sha256:cb0daba98c9259f1ad7694db888e25a1f8df866ecbca6c19f7a976a22236b475",
1938
1939
  "lib/eat.js": "sha256:35030327fb3349f7f32ebea939a0d98d93cfca4c2c31a51bd2aaeb272d2a9e6a",
@@ -2201,7 +2202,7 @@
2201
2202
  "lib/openapi-schema-walk.js": "sha256:8a46b681dce7887902f59a86adf3fc6226eeb903e5205228a7397f7fec036efe",
2202
2203
  "lib/openapi-security.js": "sha256:091ca0f5ee89bda7474850eecb90be9501054f28390e9825139e1cad2bf997d7",
2203
2204
  "lib/openapi-yaml.js": "sha256:25b03bcd1c3d0c336aa597eaf79879c840a6b87b729195c573620a47cadaa0b4",
2204
- "lib/openapi.js": "sha256:94280b4945d8ddbe7f7484e8b425789f6380264f0a7f3ed8c0d9dc1c179140b8",
2205
+ "lib/openapi.js": "sha256:497f251cf81a2b02c8ed9a17c99c5b3eec49f7d8fa1ed95ff363adf4c5d54a2a",
2205
2206
  "lib/otel-export.js": "sha256:c2f33ee7584638b5b27e2c52223eef111e1486da056d4cc5ef19e2e8a0322d9d",
2206
2207
  "lib/outbox.js": "sha256:7ce4a9540fc6a6f0b78897647e7b3cc1f4df900341b0966142228f45c6504486",
2207
2208
  "lib/pagination.js": "sha256:0b5141bc978dd727707cf4006fc928282c1c88508c8a4dec5bc7c42585d2e303",
@@ -2251,7 +2252,7 @@
2251
2252
  "lib/safe-dns.js": "sha256:60fbf77f8dfcc0ac66a943ba94f960a4a4b724c78f07f9150779525d3d460001",
2252
2253
  "lib/safe-ical.js": "sha256:a8f40466b21a1416d5aa3a5a09b75cd48c29be2f9aabec6632db26b9b7868e23",
2253
2254
  "lib/safe-icap.js": "sha256:23f334de6ee463238e26bf9612fc7bfc623519630674c48bf1590f2952c0568b",
2254
- "lib/safe-json.js": "sha256:be211b9ba0da82c95dd655eb764c06e4db6b76b901f4193a886761c062f57c7b",
2255
+ "lib/safe-json.js": "sha256:fa42506b545f78dc8e184dd989843e777d0b04e1f61d38a7e062b4515ad76146",
2255
2256
  "lib/safe-jsonpath.js": "sha256:0d5a0191de5cba7564847d6419a63c9949fb310f6a77c5637d2ad7b8ca30623c",
2256
2257
  "lib/safe-mime.js": "sha256:885bf0b2932d5ad492ec981f21068e1f85bb6738383a140b27e33659b690931e",
2257
2258
  "lib/safe-mount-info.js": "sha256:790d047481e0a2c04d3dd1f6f1645e2d6fada2f1dd1efde93ca647d2177ca7d4",
@@ -2264,7 +2265,7 @@
2264
2265
  "lib/safe-url.js": "sha256:36e3d2c72e7adc5c90cce7f80e469cf50451ccadd3791a1e3aa406754a4ce6e1",
2265
2266
  "lib/safe-vcard.js": "sha256:be2964d302b9317206fab1740ebbd0acff00258b138810aa6d64ad6cc1797ce5",
2266
2267
  "lib/sandbox-worker.js": "sha256:eefe4e76b2a736208f2e2e90347fc1ff2c2018a98e06bca3103aeb6f96298c6b",
2267
- "lib/sandbox.js": "sha256:6b3a16b248864f090b172481d008009e47e7a1db7506b552e2ca460bff27ddea",
2268
+ "lib/sandbox.js": "sha256:0fa2d3c7d52afa3091826083ec5b08fd8273e0a6fd33b207e714fd16b734d5b5",
2268
2269
  "lib/scheduler.js": "sha256:1e7ddf15a27776c738dc8112a3ef1c8fcc616f1c6f909ec2d549d3c96cdd9c80",
2269
2270
  "lib/scitt.js": "sha256:c094cef31630aed5dc7adbf494701ab0a825c79a4e406277cbc757ce54bfec9f",
2270
2271
  "lib/sd-notify.js": "sha256:2ef7395bbdab2ac4eb96083c57d401921c94278545f14427fc88cdd970bdb9eb",
@@ -2323,7 +2324,7 @@
2323
2324
  "lib/vendor/public-suffix-list.data.js": "sha256:c840d9ae5c6bf4a07967340a5c649df2c6a66a287db41e408461c46cc32348e6",
2324
2325
  "lib/vendor/simplewebauthn-server.cjs": "sha256:49411d893f5e9b0e2fcaa564b4ec7921f73a9a06229b5e53d49c1453ea1a365c",
2325
2326
  "lib/vendor/vendor-data-pubkey.js": "sha256:a12afa34cd7472e2eaebad2fcd44714102d3edd0601e45769404124a513926d0",
2326
- "lib/vex.js": "sha256:b45c1c9729dfc69f2140478d46eec91f47da94b5f440be36fa825cb733a718fc",
2327
+ "lib/vex.js": "sha256:bed6880980e7f166fd30bbddab57d7fa06085aaae0dc13ed82a17531b76878b7",
2327
2328
  "lib/watcher.js": "sha256:8618da919affabbe4c4d33915647b3ee4b27e6ea091638f032d4bfead797baf8",
2328
2329
  "lib/web-push-vapid.js": "sha256:54d7d4beb764681bc97dc338490fc2afc3996a032e080401830206814b8c97f0",
2329
2330
  "lib/webhook-dispatcher.js": "sha256:f879d127c0c9ba81ee43ee2b1f38c3fac99d8ff97df1555707beda48caad53f8",
@@ -2340,7 +2341,7 @@
2340
2341
  "oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
2341
2342
  "oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
2342
2343
  "oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
2343
- "package.json": "sha256:0e42d663df6e40194fe8542b545b73f46681f6e81b0047a4a6b87284f6ab27bf",
2344
+ "package.json": "sha256:4f8db3f9b9bee033a270a974dd57ba37387f0d41073c8b6ce260073876edd105",
2344
2345
  "release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
2345
2346
  "release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
2346
2347
  "release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
@@ -2370,6 +2371,7 @@
2370
2371
  "release-notes/v0.15.26.json": "sha256:a96b1c7409f863b5a920f51a92b653d5f7a0b671e000bb5dfc2e9bf47e4120dc",
2371
2372
  "release-notes/v0.15.27.json": "sha256:f9e8309bc32c2245ec8d0682e6205417879f090403727d4780b42c0536125f02",
2372
2373
  "release-notes/v0.15.28.json": "sha256:86d4b034ccd17a51abd706fcd3ec0c1dfca224ae92a74e8d501af1e6f57cb6aa",
2374
+ "release-notes/v0.15.29.json": "sha256:9e912ac2c32bb84bf1d89b8cc5f04d73a78d74a2ac1be24e42e6462f5135b4f5",
2373
2375
  "release-notes/v0.15.3.json": "sha256:19a0074c445545468ca3cc411b21ec8bdb27be2669ae1950347cc244f6aa348c",
2374
2376
  "release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
2375
2377
  "release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
@@ -2411,7 +2413,7 @@
2411
2413
  "scripts/vendor-data-gen.js": "sha256:76b627bc6e19b4a122edfca6f514bcb8ca11af02902f0957e641f503337a8a0f",
2412
2414
  "scripts/vendor-data-keygen.js": "sha256:94eaa4d8f832b4aac9ccbcb2a07e6b99cd35cf7b044e1412079cebdefc1f4c0e",
2413
2415
  "scripts/vendor-update.sh": "sha256:373d7f024b7caa3345a3598fa3a586078dd842f0071e6fdad00a473f48a3a929",
2414
- "test/00-primitives.js": "sha256:99992ebfdf1bcd14a8ac20c71d96741c11b9ff793c788f220226261652b07152",
2416
+ "test/00-primitives.js": "sha256:918967f91118ca29fc24b7d25465c840c8fe2e3fd4e19fe09075db986e3fe230",
2415
2417
  "test/10-state.js": "sha256:45c177da8158bda413b783f68d9b01ef72796aec114c26acea084f92de874f57",
2416
2418
  "test/20-db.js": "sha256:241ef6b7ef305d077aeafb22ee3bcc75b6b549a8fa9b1a6b5d6d5fba43b48d7d",
2417
2419
  "test/30-chain.js": "sha256:81d3615c276138d9b71136047ce80f03170951a7ce1e6aa6be117cb673cca4f8",
@@ -2617,7 +2619,7 @@
2617
2619
  "test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
2618
2620
  "test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
2619
2621
  "test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
2620
- "test/layer-0-primitives/codebase-patterns.test.js": "sha256:59c479be996fdec3635bbc75d05f4c8582043fbb026e2e67085f19b6b9412cce",
2622
+ "test/layer-0-primitives/codebase-patterns.test.js": "sha256:38c5ad5bf99da425f563b892451a39a4ed3a323694c2fb9b43271f63e9075266",
2621
2623
  "test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
2622
2624
  "test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
2623
2625
  "test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
@@ -2699,7 +2701,7 @@
2699
2701
  "test/layer-0-primitives/dr-runbook.test.js": "sha256:9665caaa90f356d0237e0e6c6889c56a3467b20c7eaf78d2e2afa51c4b3af2cb",
2700
2702
  "test/layer-0-primitives/dsa.test.js": "sha256:2cdbdd29b9c58d738920bb4f82f73492eab56e7d7f8111f1cdb14838de3a3ab3",
2701
2703
  "test/layer-0-primitives/dsr-state-rules.test.js": "sha256:0ddc7fd6d5d3f8817d8f4aad7bb66c6bf6e64eafe61fdedaafa8f60957d20bd0",
2702
- "test/layer-0-primitives/dsr.test.js": "sha256:e930c1cd058ac8de7280fd7e9a27373bf5d971ebe4e9133726c8f615811cd7c0",
2704
+ "test/layer-0-primitives/dsr.test.js": "sha256:d7be3fcf28348936ffd73ec6bc06d59ea7ab22b40fa3316eba18c587d7575680",
2703
2705
  "test/layer-0-primitives/dual-control.test.js": "sha256:2d9255f96bddbb45d75182ef826a355536e40caeb5864483c56eaf5ba3c7e0ac",
2704
2706
  "test/layer-0-primitives/early-hints.test.js": "sha256:9827c66c7558ebeb39a448ec69129517a517d440865ac5341afcbc7c1047e085",
2705
2707
  "test/layer-0-primitives/eat.test.js": "sha256:6f0b0180f659b008a6a86f9af4f8674e2ddb98d00408e10d52d03291bda96f6e",
@@ -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.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
+
11
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.
12
14
 
13
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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.15.28",
4
- "createdAt": "2026-06-26T05:54:16.583Z",
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
- var doc;
474
- if (typeof jsonStringOrObject === "string") {
475
- try { doc = JSON.parse(jsonStringOrObject); } // allow:bare-json-parse — operator-supplied AsyncAPI doc; size-bounded by caller
476
- catch (e) {
477
- throw new AsyncApiError("asyncapi/bad-json",
478
- "asyncapi.parse: invalid JSON — " + e.message);
479
- }
480
- } else if (jsonStringOrObject != null && typeof jsonStringOrObject === "object") {
481
- doc = jsonStringOrObject;
482
- } else {
483
- throw new AsyncApiError("asyncapi/bad-input",
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:bare-json-parse — header from cryptographically-verified JWT; signature verifies the bytes // allow:raw-byte-literal — JWT header cap (64 KB)
169
- payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:bare-json-parse — payload from cryptographically-verified JWT; signature verifies the bytes // allow:raw-byte-literal — JWT payload cap (1 MB)
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:bare-json-parse — payload only read to pull _sd_alg; final auth happens in verify() // allow:raw-byte-literal — JWT payload cap (64 KB)
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:bare-json-parse — pre-verify header parse to look up the key resolver; checked again post-signature // allow:raw-byte-literal — JWT header cap (64 KB)
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 }); } // allow:bare-json-parse — kb header from validated KB-JWT; signature verifies
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: JSON.stringify(ticket),
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 JSON.parse(_unsealPayload(rows[0].payload, rows[0].id)); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store (unsealed above), never from operator/network input
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 JSON.parse(_unsealPayload(r.payload, r.id)); }); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store (unsealed above), never from operator/network input
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 = JSON.parse(_unsealPayload(rows[i].payload, rows[i].id)); // allow:bare-json-parse — payload was JSON.stringify-ed by this same store (unsealed above), never from operator/network input
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;
@@ -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
- var doc;
524
- if (typeof jsonStringOrObject === "string") {
525
- try { doc = JSON.parse(jsonStringOrObject); } // allow:bare-json-parse — operator-supplied OpenAPI doc; size-bounded by caller
526
- catch (e) {
527
- throw new OpenApiError("openapi/bad-json",
528
- "openapi.parse: invalid JSON — " + e.message);
529
- }
530
- } else if (jsonStringOrObject != null && typeof jsonStringOrObject === "object") {
531
- doc = jsonStringOrObject;
532
- } else {
533
- throw new OpenApiError("openapi/bad-input",
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. The worker
240
- // refuses any result whose stringified form exceeds this.
241
- var maxResultBytes = Math.floor(maxBytes / 4);
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
- try { parsed = (msg.resultJson === undefined) ? undefined : JSON.parse(msg.resultJson); } // allow:bare-json-parse — resultJson is produced by lib/sandbox-worker.js via JSON.stringify and bounded by maxResultBytes; never directly from operator/network input
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
- return JSON.stringify(JSON.parse(canonical), null, 2); // allow:bare-json-parse canonical is canonicalJson.stringify output, not operator input
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 = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.28",
3
+ "version": "0.15.29",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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,
@@ -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
- // `JSON.parse(operatorInput)` lacks the maxBytes / depth / proto
1599
- // pollution defenses that `safeJson.parse` adds. Internal JSON
1600
- // (vendor manifest, tests, internal state) is fine with bare parse.
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
- matches = _filterMarkers(matches, "bare-json-parse");
1607
- _report("JSON.parse on operator input routes through safeJson.parse " +
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
 
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.108",
3
+ "version": "0.4.110",
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": {