@blamejs/blamejs-shop 0.4.109 → 0.4.111
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/payment.js +5 -5
- package/lib/r2-bridge.js +1 -1
- package/lib/vendor/MANIFEST.json +12 -10
- package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/webhook-dispatcher.js +6 -2
- package/lib/vendor/blamejs/lib/webhook.js +22 -8
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.30.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +8 -30
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +50 -0
- 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.111 (2026-06-26) — **Vendored framework refreshed to 0.15.30.** Updates the vendored blamejs framework to 0.15.30. Upstream makes the webhook module's inbound signature-verification path loadable in a Worker / edge runtime: the outbound HTTP client and the delivery dispatcher are now resolved lazily (only on the send path), so importing the webhook module directly to verify an inbound signature no longer pulls the Node networking stack. This is the capability needed to compose the framework's Stripe-signature verifier at the edge instead of a hand-rolled check; this release vendors it, and the edge adoption follows separately. No runtime API or behavior change for this storefront in this release; no operator action required. **Changed:** *Vendored framework refreshed to 0.15.30* — Refreshes the vendored blamejs framework to 0.15.30. Upstream resolves the webhook module's outbound HTTP client and delivery dispatcher lazily (on the send/dispatch path only), so the inbound signature-verification path loads no Node networking module and can run in a Worker / edge runtime when the webhook module is imported directly. No runtime API change and no operator action required in this release.
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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
16
|
|
|
13
17
|
- 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.
|
package/lib/asset-manifest.json
CHANGED
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:
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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" ?
|
|
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 ?
|
|
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) {
|
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.30",
|
|
7
|
+
"tag": "v0.15.30",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -852,6 +852,7 @@
|
|
|
852
852
|
"release-notes/v0.15.28.json": "lib/vendor/blamejs/release-notes/v0.15.28.json",
|
|
853
853
|
"release-notes/v0.15.29.json": "lib/vendor/blamejs/release-notes/v0.15.29.json",
|
|
854
854
|
"release-notes/v0.15.3.json": "lib/vendor/blamejs/release-notes/v0.15.3.json",
|
|
855
|
+
"release-notes/v0.15.30.json": "lib/vendor/blamejs/release-notes/v0.15.30.json",
|
|
855
856
|
"release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
|
|
856
857
|
"release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
|
|
857
858
|
"release-notes/v0.15.6.json": "lib/vendor/blamejs/release-notes/v0.15.6.json",
|
|
@@ -1531,7 +1532,7 @@
|
|
|
1531
1532
|
"bundler": "shallow git clone of release tag from github.com/blamejs/blamejs",
|
|
1532
1533
|
"bundledAt": "2026-06-26",
|
|
1533
1534
|
"hashes": {
|
|
1534
|
-
".clusterfuzzlite/Dockerfile": "sha256:
|
|
1535
|
+
".clusterfuzzlite/Dockerfile": "sha256:6587c80f02f3d622b782380ff1aaa7753b948023ff1d4af8ac27709d1876c218",
|
|
1535
1536
|
".clusterfuzzlite/build.sh": "sha256:b9f5a627272c65f6569fa20dcbb32c383ca3a1183ce2562d481408a2fbaf9881",
|
|
1536
1537
|
".clusterfuzzlite/project.yaml": "sha256:8eafa9380e6f975b0b56350f37e5096db283d44c1ea448b25fde1958586e06e0",
|
|
1537
1538
|
".dockerignore": "sha256:37cd6917661eb77d1a056640ca92c50b0330aafe79b7b7eb648e464de38b9e01",
|
|
@@ -1560,7 +1561,7 @@
|
|
|
1560
1561
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1561
1562
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1562
1563
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1563
|
-
"CHANGELOG.md": "sha256:
|
|
1564
|
+
"CHANGELOG.md": "sha256:321082dca053d6850267b324eef441e8e5e8b0127e513338c94a926565719eb7",
|
|
1564
1565
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1565
1566
|
"CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
|
|
1566
1567
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
@@ -1570,7 +1571,7 @@
|
|
|
1570
1571
|
"NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
|
|
1571
1572
|
"README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
|
|
1572
1573
|
"SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
|
|
1573
|
-
"api-snapshot.json": "sha256:
|
|
1574
|
+
"api-snapshot.json": "sha256:4eaa496230e6d8b721d189857ab24f9d8a806f5289058ef139a057fe02f011d9",
|
|
1574
1575
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1575
1576
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1576
1577
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -2327,8 +2328,8 @@
|
|
|
2327
2328
|
"lib/vex.js": "sha256:bed6880980e7f166fd30bbddab57d7fa06085aaae0dc13ed82a17531b76878b7",
|
|
2328
2329
|
"lib/watcher.js": "sha256:8618da919affabbe4c4d33915647b3ee4b27e6ea091638f032d4bfead797baf8",
|
|
2329
2330
|
"lib/web-push-vapid.js": "sha256:54d7d4beb764681bc97dc338490fc2afc3996a032e080401830206814b8c97f0",
|
|
2330
|
-
"lib/webhook-dispatcher.js": "sha256:
|
|
2331
|
-
"lib/webhook.js": "sha256:
|
|
2331
|
+
"lib/webhook-dispatcher.js": "sha256:3a8517f6dd3bbc37c1fa2e5e23e81ce60b8488aec75ab4598ec57df743d186db",
|
|
2332
|
+
"lib/webhook.js": "sha256:0db5e3aed7b1f10eeeb15b5fd94424f2cb44e443fbfdc6029800ac89b2f8dad1",
|
|
2332
2333
|
"lib/websocket-channels.js": "sha256:d8f39368acc3ff17d8aaa7f0ef5bb5e64b6b406154e314c21833dce0415aea14",
|
|
2333
2334
|
"lib/websocket.js": "sha256:a8d4dfa48de0100de0ade5ae0db7d67f99e1564d4587a8d4e56c82a6b36137f8",
|
|
2334
2335
|
"lib/wiki-concepts.js": "sha256:54789790b950d926cbd0657dd51b32435c4988012b520819615df93673680de5",
|
|
@@ -2341,7 +2342,7 @@
|
|
|
2341
2342
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2342
2343
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2343
2344
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2344
|
-
"package.json": "sha256:
|
|
2345
|
+
"package.json": "sha256:c5301b20b46e0cf52bf9e7d936bb532a4655e1674fed66ea624a775121ba6ade",
|
|
2345
2346
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2346
2347
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2347
2348
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2373,6 +2374,7 @@
|
|
|
2373
2374
|
"release-notes/v0.15.28.json": "sha256:86d4b034ccd17a51abd706fcd3ec0c1dfca224ae92a74e8d501af1e6f57cb6aa",
|
|
2374
2375
|
"release-notes/v0.15.29.json": "sha256:9e912ac2c32bb84bf1d89b8cc5f04d73a78d74a2ac1be24e42e6462f5135b4f5",
|
|
2375
2376
|
"release-notes/v0.15.3.json": "sha256:19a0074c445545468ca3cc411b21ec8bdb27be2669ae1950347cc244f6aa348c",
|
|
2377
|
+
"release-notes/v0.15.30.json": "sha256:d40ce2c5ffb7ae5a2b501ea0f949bd29510d8544a5f588f136a37ce4981d50ad",
|
|
2376
2378
|
"release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
|
|
2377
2379
|
"release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
|
|
2378
2380
|
"release-notes/v0.15.6.json": "sha256:0e3b9e5e43b70b61dd258c3003d1b8729cd3c26c62a34dedcca81bbec5d31077",
|
|
@@ -2619,7 +2621,7 @@
|
|
|
2619
2621
|
"test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
|
|
2620
2622
|
"test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
|
|
2621
2623
|
"test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
|
|
2622
|
-
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:
|
|
2624
|
+
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:7f58d0fc1a47d2d085b6d88c282a672f6a94315a427594cfd12dc5e077a3796a",
|
|
2623
2625
|
"test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
|
|
2624
2626
|
"test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
|
|
2625
2627
|
"test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
|
|
@@ -3034,7 +3036,7 @@
|
|
|
3034
3036
|
"test/layer-0-primitives/web-push-vapid.test.js": "sha256:4634dcf1c3fdae300b291d20473285a9d9fe2f49f9c54e4e3a2e149768d3fe32",
|
|
3035
3037
|
"test/layer-0-primitives/webhook-dispatcher.test.js": "sha256:080b999087102eccaa73e070b3f08ec2ab1fecd3a38d25cdcd2937241c9d9689",
|
|
3036
3038
|
"test/layer-0-primitives/webhook-verify-nonce-atomic.test.js": "sha256:42441d6d923d2ba9f83da932e1e4715f2e519735c816e6396f1390e87ff378f6",
|
|
3037
|
-
"test/layer-0-primitives/webhook.test.js": "sha256:
|
|
3039
|
+
"test/layer-0-primitives/webhook.test.js": "sha256:d41b509bf96e674fba6900df3468e176a34ec3ed75e26e26be362b48423c5906",
|
|
3038
3040
|
"test/layer-0-primitives/websocket-channels.test.js": "sha256:8e1249fd11c4ae1253c8b76cc8638c9cdce2081046d6b80a9a6db0cd71d2716c",
|
|
3039
3041
|
"test/layer-0-primitives/websocket-extension-header.test.js": "sha256:8f3a29eeb269c7393342e4e7966c330413964cd251b76d5cdefd6455aa8883be",
|
|
3040
3042
|
"test/layer-0-primitives/worker-pool-recycle-race.test.js": "sha256:0e9f12256864334bf49de0b0d62b9d4f9cfeb34e9fe598f9299b811dc4ac4719",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# oss-fuzz/projects/blamejs/ copy is intentionally left on the floating tag —
|
|
16
16
|
# OSS-Fuzz's own build infra expects projects to track the latest base-builder,
|
|
17
17
|
# so pinning there would diverge from upstream convention.
|
|
18
|
-
FROM gcr.io/oss-fuzz-base/base-builder-javascript@sha256:
|
|
18
|
+
FROM gcr.io/oss-fuzz-base/base-builder-javascript@sha256:454290c335fd888ca5231b585d5674841f35bd38d0d2de40db64da83926c8b1a
|
|
19
19
|
|
|
20
20
|
# Copy the framework source. ClusterFuzzLite mounts the repo at
|
|
21
21
|
# $SRC/blamejs, so we COPY everything (including fuzz/) into one
|
|
@@ -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.30 (2026-06-26) — **Importing the webhook module for inbound signature verification no longer pulls the Node HTTP client (or its `node:http` / `node:https` / `node:http2` chain), so `b.webhook.verify` runs in a Worker / edge runtime when the module is imported directly.** lib/webhook and its delivery store eagerly required the outbound HTTP client at module load — directly, and transitively through the persistence-backed dispatcher — so importing the webhook module threw in a Cloudflare Workers / edge runtime that has no node:http / node:net / node:tls, even though b.webhook.verify needs only HMAC-SHA-256 and a timing-safe compare. The HTTP client and the SQL-backed dispatcher are now resolved lazily, only on the outbound send / dispatch paths; importing the module for inbound verification loads no Node networking module. Verified by asserting no networking builtin appears in process.moduleLoadList on the verify path. The framework's aggregate entry point still initializes the networking stack eagerly, so the edge verify path is the webhook module imported directly rather than the aggregate b namespace. **Fixed:** *Inbound webhook verification loads without the Node networking stack (Worker / edge runtime)* — The webhook module and its dispatcher delivery store required the outbound HTTP client (node:http / node:https / node:http2) at module load, so importing the module threw in a runtime without those builtins — even though b.webhook.verify is HMAC-SHA-256 plus a timing-safe compare and the dispatcher (SQL-backed persistence) is only needed for outbound delivery. The HTTP client and the dispatcher are now resolved lazily, on the outbound send / dispatch paths only; the inbound verify path loads no Node networking module. An edge handler that imports the webhook module directly can pre-verify an inbound Stripe webhook signature with the primitive's hardening (Stripe-Signature size cap, v1-hex anti-DoS cap, minimum-tolerance floor, optional atomic replay store) instead of hand-rolling the t=/v1= parse, tolerance window, and timing-safe compare. The framework's aggregate entry still loads the networking stack eagerly; import the webhook module directly for the edge verify path.
|
|
12
|
+
|
|
11
13
|
- 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
14
|
|
|
13
15
|
- 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.
|
|
@@ -26,7 +26,6 @@ var safeSql = require("./safe-sql");
|
|
|
26
26
|
var safeUrl = require("./safe-url");
|
|
27
27
|
var safeJson = require("./safe-json");
|
|
28
28
|
var bCrypto = require("./crypto");
|
|
29
|
-
var httpClient = require("./http-client");
|
|
30
29
|
var frameworkSchema = require("./framework-schema");
|
|
31
30
|
var validateOpts = require("./validate-opts");
|
|
32
31
|
var lazyRequire = require("./lazy-require");
|
|
@@ -44,6 +43,11 @@ var vault = lazyRequire(function () { return require("./vault"); });
|
|
|
44
43
|
var ssrfGuard = lazyRequire(function () { return require("./ssrf-guard"); });
|
|
45
44
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
46
45
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
46
|
+
// Lazy — http-client pulls in node:http / node:https / node:http2; only the
|
|
47
|
+
// default delivery transport touches it. Keeping it lazy keeps b.webhook (which
|
|
48
|
+
// re-exports this module) free of the Node networking chain on its inbound
|
|
49
|
+
// verify path, so b.webhook.verify stays loadable in a Worker / edge runtime.
|
|
50
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
47
51
|
|
|
48
52
|
var _err = WebhookDispatcherError.factory;
|
|
49
53
|
|
|
@@ -223,7 +227,7 @@ function dispatcher(opts) {
|
|
|
223
227
|
// Default transport: a real signed POST through the framework http client.
|
|
224
228
|
// Injectable so a test (or a non-HTTP transport) substitutes its own.
|
|
225
229
|
var httpRequest = opts.httpRequest || function (url, body, headers) {
|
|
226
|
-
return httpClient.request({
|
|
230
|
+
return httpClient().request({
|
|
227
231
|
method: "POST",
|
|
228
232
|
url: url,
|
|
229
233
|
headers: headers,
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
var nodeCrypto = require("node:crypto");
|
|
51
51
|
var numericBounds = require("./numeric-bounds");
|
|
52
52
|
var bCrypto = require("./crypto");
|
|
53
|
-
var httpClient = require("./http-client");
|
|
54
53
|
var safeBuffer = require("./safe-buffer");
|
|
55
54
|
var safeUrl = require("./safe-url");
|
|
56
55
|
var retryHelper = require("./retry");
|
|
@@ -60,12 +59,20 @@ var numericChecks = require("./numeric-checks");
|
|
|
60
59
|
var requestHelpers = require("./request-helpers");
|
|
61
60
|
// Lazy — ssrf-guard pulls in the DNS/network stack; only touched on send().
|
|
62
61
|
var ssrfGuard = lazyRequire(function () { return require("./ssrf-guard"); });
|
|
62
|
+
// Lazy — http-client pulls in node:http / node:https / node:http2; only the
|
|
63
|
+
// outbound send() path touches it. Keeping it lazy lets b.webhook.verify (HMAC
|
|
64
|
+
// + timing-safe compare only) load in a Worker / edge runtime with no Node
|
|
65
|
+
// networking module available.
|
|
66
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
63
67
|
var validateOpts = require("./validate-opts");
|
|
64
68
|
var { WebhookError } = require("./framework-error");
|
|
65
69
|
// b.webhook.dispatcher — durable signed-webhook delivery store. Lives in its
|
|
66
|
-
// own module (distinct domain: persistence) and
|
|
67
|
-
//
|
|
68
|
-
|
|
70
|
+
// own module (distinct domain: persistence) and pulls the SQL stack (and so
|
|
71
|
+
// node:fs). Lazy — only resolved when b.webhook.dispatcher is accessed — so
|
|
72
|
+
// importing the webhook module for the inbound verify path stays free of the
|
|
73
|
+
// persistence/networking stack and loads in a Worker / edge runtime. The
|
|
74
|
+
// module lazyRequires this file back, so the cycle stays deferred on both ends.
|
|
75
|
+
var webhookDispatcher = lazyRequire(function () { return require("./webhook-dispatcher"); });
|
|
69
76
|
|
|
70
77
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
71
78
|
|
|
@@ -472,7 +479,7 @@ function signer(opts) {
|
|
|
472
479
|
} catch (_e) { hostLabel = ""; }
|
|
473
480
|
try {
|
|
474
481
|
var res = await retryHelper.withRetry(function () {
|
|
475
|
-
return httpClient.request(requestOpts);
|
|
482
|
+
return httpClient().request(requestOpts);
|
|
476
483
|
}, retryOpts);
|
|
477
484
|
var statusCode = (res && (res.statusCode || res.status)) || 0;
|
|
478
485
|
_emitEvent("webhook.send", 1, {
|
|
@@ -1011,7 +1018,14 @@ module.exports = {
|
|
|
1011
1018
|
// v0.11.25 — Stripe-shaped inbound HMAC-SHA-256 verifier + signer.
|
|
1012
1019
|
verify: verify,
|
|
1013
1020
|
sign: sign,
|
|
1014
|
-
// Durable signed-webhook delivery store (sign + persist + deliver + retry +
|
|
1015
|
-
// dead-letter + operator replay). Defined in lib/webhook-dispatcher.js.
|
|
1016
|
-
dispatcher: webhookDispatcher.dispatcher,
|
|
1017
1021
|
};
|
|
1022
|
+
|
|
1023
|
+
// Durable signed-webhook delivery store (sign + persist + deliver + retry +
|
|
1024
|
+
// dead-letter + operator replay). Defined in lib/webhook-dispatcher.js, which
|
|
1025
|
+
// pulls the SQL/persistence stack — exposed as a lazy getter so requiring the
|
|
1026
|
+
// webhook module for the inbound verify path doesn't load it (edge-safe).
|
|
1027
|
+
Object.defineProperty(module.exports, "dispatcher", {
|
|
1028
|
+
enumerable: true,
|
|
1029
|
+
configurable: true,
|
|
1030
|
+
get: function () { return webhookDispatcher().dispatcher; },
|
|
1031
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.30",
|
|
4
|
+
"date": "2026-06-26",
|
|
5
|
+
"headline": "Importing the webhook module for inbound signature verification no longer pulls the Node HTTP client (or its `node:http` / `node:https` / `node:http2` chain), so `b.webhook.verify` runs in a Worker / edge runtime when the module is imported directly",
|
|
6
|
+
"summary": "lib/webhook and its delivery store eagerly required the outbound HTTP client at module load — directly, and transitively through the persistence-backed dispatcher — so importing the webhook module threw in a Cloudflare Workers / edge runtime that has no node:http / node:net / node:tls, even though b.webhook.verify needs only HMAC-SHA-256 and a timing-safe compare. The HTTP client and the SQL-backed dispatcher are now resolved lazily, only on the outbound send / dispatch paths; importing the module for inbound verification loads no Node networking module. Verified by asserting no networking builtin appears in process.moduleLoadList on the verify path. The framework's aggregate entry point still initializes the networking stack eagerly, so the edge verify path is the webhook module imported directly rather than the aggregate b namespace.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Inbound webhook verification loads without the Node networking stack (Worker / edge runtime)",
|
|
13
|
+
"body": "The webhook module and its dispatcher delivery store required the outbound HTTP client (node:http / node:https / node:http2) at module load, so importing the module threw in a runtime without those builtins — even though b.webhook.verify is HMAC-SHA-256 plus a timing-safe compare and the dispatcher (SQL-backed persistence) is only needed for outbound delivery. The HTTP client and the dispatcher are now resolved lazily, on the outbound send / dispatch paths only; the inbound verify path loads no Node networking module. An edge handler that imports the webhook module directly can pre-verify an inbound Stripe webhook signature with the primitive's hardening (Stripe-Signature size cap, v1-hex anti-DoS cap, minimum-tolerance floor, optional atomic replay store) instead of hand-rolling the t=/v1= parse, tolerance window, and timing-safe compare. The framework's aggregate entry still loads the networking stack eagerly; import the webhook module directly for the edge verify path."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -631,36 +631,14 @@ function testNoUnresolvedMarkers() {
|
|
|
631
631
|
// or an item on the gap backlog being worked down. The key is the file; the
|
|
632
632
|
// value is a list of distinctive content substrings to permit. Remove an entry
|
|
633
633
|
// when the comment is corrected or the gap is closed — that is the backlog.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
],
|
|
643
|
-
// Deliberate: Sieve extension refused per RFC 5228 §3.2 — script-declared
|
|
644
|
-
// capability gating, defer-with-condition (operator demand).
|
|
645
|
-
"lib/safe-sieve.js": ["not implemented in v0.9.55 — script refused"],
|
|
646
|
-
// Conditional on a future vendoring decision (no bundled EXIF/IPTC reader);
|
|
647
|
-
// operator-feeds-metadata escape hatch. Defer-with-condition.
|
|
648
|
-
"lib/ai-content-detect.js": ["IPTC PhotoMetadata reader lands in v0.10.9"],
|
|
649
|
-
// GAP BACKLOG (being worked down — these are real overdue items):
|
|
650
|
-
// archive-read ZIP64 read (promised v0.12.8) — building. (The
|
|
651
|
-
// fromTrustedStream defer was reworded to a version-free "not
|
|
652
|
-
// implemented / re-opens when needed" in v0.13.15, so it no longer
|
|
653
|
-
// needs an allowlist entry.)
|
|
654
|
-
"lib/archive-read.js": [
|
|
655
|
-
"not supported in v0.12.7. Will land",
|
|
656
|
-
"switch to tar — lands v0.12.8",
|
|
657
|
-
"carries ZIP64 sentinel sizes (not supported in v0.12.7)",
|
|
658
|
-
],
|
|
659
|
-
"lib/safe-archive.js": [
|
|
660
|
-
"tar lands v0.12.8, gz v0.12.9",
|
|
661
|
-
"fromTrustedStream` is deferred to v0.12.8",
|
|
662
|
-
],
|
|
663
|
-
};
|
|
634
|
+
// Empty: every prior entry's promised-landing comment has been reworded to a
|
|
635
|
+
// version-free re-open-condition (the established shape — a deferred feature
|
|
636
|
+
// states its escape hatch, not a version it "lands in"), so no source line
|
|
637
|
+
// matches the PROMISE pattern below and nothing needs allowlisting. Re-add an
|
|
638
|
+
// entry { "lib/<file>.js": ["<distinctive substring>"] } only for a deliberate
|
|
639
|
+
// defer-with-condition whose comment unavoidably names a version; prefer
|
|
640
|
+
// rewording the comment instead.
|
|
641
|
+
var STALE_DEFER_ALLOWLIST = {};
|
|
664
642
|
|
|
665
643
|
function testNoStaleDefers() {
|
|
666
644
|
var path = require("node:path");
|
|
@@ -736,6 +736,54 @@ async function testStripeNonceStoreConcurrentAtomic() {
|
|
|
736
736
|
if (ns.close) await ns.close();
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
// The webhook module is the Worker/edge-safe import for inbound verification:
|
|
740
|
+
// require it directly (e.g. require("@blamejs/core/lib/webhook")) rather than
|
|
741
|
+
// the framework's aggregate entry, which eagerly initializes the Node
|
|
742
|
+
// networking stack. b.webhook.verify is HMAC + timing-safe compare only, so the
|
|
743
|
+
// module must NOT pull the outbound http-client (node:http / node:https /
|
|
744
|
+
// node:http2 chain) at load — directly, OR transitively through the
|
|
745
|
+
// webhook-dispatcher delivery store. This child process requires the module in
|
|
746
|
+
// isolation and verifies a Stripe signature; the edge guarantee holds iff no
|
|
747
|
+
// Node networking builtin appears in process.moduleLoadList on that path. RED on
|
|
748
|
+
// a top-level `require("./http-client")` in either module (node:http loads at
|
|
749
|
+
// module eval); GREEN once both lazyRequire it and only the delivery path
|
|
750
|
+
// resolves it. moduleLoadList records a loaded builtin as "NativeModule http",
|
|
751
|
+
// so the filter must match that form (not only "node:http").
|
|
752
|
+
function testVerifyLoadableWithoutHttpClient() {
|
|
753
|
+
var cp = require("node:child_process");
|
|
754
|
+
var nodePath = require("node:path");
|
|
755
|
+
var repoRoot = nodePath.resolve(__dirname, "..", "..");
|
|
756
|
+
var webhookPath = nodePath.join(repoRoot, "lib", "webhook.js");
|
|
757
|
+
var script =
|
|
758
|
+
"var crypto = require('node:crypto');" +
|
|
759
|
+
"var webhook = require(" + JSON.stringify(webhookPath) + ");" +
|
|
760
|
+
"var secret = 'whsec_edgeruntime';" +
|
|
761
|
+
"var body = '{\"id\":\"evt_edge\"}';" +
|
|
762
|
+
"var t = Math.floor(Date.now() / 1000);" +
|
|
763
|
+
"var sig = crypto.createHmac('sha256', secret).update(t + '.' + body).digest('hex');" +
|
|
764
|
+
"webhook.verify({ alg: 'hmac-sha256-stripe', secret: secret, header: 't=' + t + ',v1=' + sig, body: body })" +
|
|
765
|
+
" .then(function (r) {" +
|
|
766
|
+
" if (!r || r.ok !== true) { process.stderr.write('verify-not-ok'); process.exit(3); }" +
|
|
767
|
+
// process.moduleLoadList records a loaded networking builtin as
|
|
768
|
+
// "NativeModule http" (Node 20+) or "node:http" (older) — match both forms.
|
|
769
|
+
" var net = process.moduleLoadList.filter(function (m) { return /^(?:NativeModule |node:)(net|tls|http|https|http2)$/.test(m); });" +
|
|
770
|
+
" if (net.length) { process.stderr.write('net-loaded:' + net.join(',')); }" +
|
|
771
|
+
" process.exit(net.length ? 1 : 0);" +
|
|
772
|
+
" })" +
|
|
773
|
+
" .catch(function (e) { process.stderr.write(String(e && e.message)); process.exit(2); });";
|
|
774
|
+
var code = 0;
|
|
775
|
+
var stderr = "";
|
|
776
|
+
try {
|
|
777
|
+
cp.execFileSync(process.execPath, ["-e", script], { stdio: ["ignore", "ignore", "pipe"], timeout: 30000 });
|
|
778
|
+
} catch (e) {
|
|
779
|
+
code = (e && typeof e.status === "number") ? e.status : -1;
|
|
780
|
+
stderr = (e && e.stderr) ? e.stderr.toString() : "";
|
|
781
|
+
}
|
|
782
|
+
check("webhook.verify composes with no node networking builtin loaded (edge-runtime safe) [exit=" +
|
|
783
|
+
code + (stderr ? " stderr=" + stderr.slice(0, 120).replace(/\n/g, " ") : "") + "]",
|
|
784
|
+
code === 0);
|
|
785
|
+
}
|
|
786
|
+
|
|
739
787
|
// signer.send dispatches through the shared httpClient keep-alive transport
|
|
740
788
|
// pool; a cached client socket finalizes its destroy on a later event-loop
|
|
741
789
|
// turn, past the forked worker's grace window. Reset the pool, then poll until
|
|
@@ -801,6 +849,8 @@ async function _runTests() {
|
|
|
801
849
|
await testStripeNonceStoreFromPrimitive();
|
|
802
850
|
await testStripeNonceStoreBadShapeRefused();
|
|
803
851
|
await testStripeNonceStoreConcurrentAtomic();
|
|
852
|
+
// #378 — verify path must stay free of the Node networking chain.
|
|
853
|
+
testVerifyLoadableWithoutHttpClient();
|
|
804
854
|
}
|
|
805
855
|
|
|
806
856
|
module.exports = { run: run };
|
package/package.json
CHANGED