@blamejs/blamejs-shop 0.4.85 → 0.4.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/cart.js +75 -0
  4. package/lib/order.js +4 -2
  5. package/lib/security-middleware.js +1 -0
  6. package/lib/storefront.js +8 -1
  7. package/lib/vendor/MANIFEST.json +71 -39
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  10. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  11. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  12. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  13. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  14. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  15. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  16. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  17. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  19. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  20. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  21. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  22. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  23. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  24. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  25. package/lib/vendor/blamejs/lib/mail.js +39 -0
  26. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  27. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  28. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  29. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  30. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  31. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  32. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  33. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  34. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  35. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  36. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  37. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  38. package/lib/vendor/blamejs/lib/session.js +85 -28
  39. package/lib/vendor/blamejs/package.json +1 -1
  40. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  59. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  60. package/lib/webhooks.js +38 -9
  61. package/package.json +1 -1
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * b.webhook.verify — replay nonceStore is atomic, not check-then-set (#328).
4
+ *
5
+ * The Stripe-shape inbound verifier must consult its replay store via the
6
+ * atomic { checkAndInsert(nonce, expireAt) → bool } contract — the SAME
7
+ * contract b.nonceStore.create exposes and b.webhook.verifier already uses.
8
+ * A non-atomic check-then-set ({ has, set }) races two concurrent
9
+ * redeliveries of the same event: both observe "unseen", both proceed
10
+ * (CWE-367 TOCTOU). These tests drive the real b.webhook.verify consumer
11
+ * path with the framework's own b.nonceStore.create so the store drops in
12
+ * with no adapter, and assert that of two byte-identical concurrent
13
+ * deliveries exactly one is accepted.
14
+ *
15
+ * RED proof (old buggy non-atomic await-has → if-seen-throw → await-set):
16
+ * two concurrent verifies of the same signature BOTH observe "unseen" and
17
+ * BOTH resolve ok — "concurrent: exactly one accepted" would fail (2 ok,
18
+ * 0 replay). The atomic checkAndInsert collapses that to 1 ok / 1 replay.
19
+ *
20
+ * Run standalone: `node test/layer-0-primitives/webhook-verify-nonce-atomic.test.js`
21
+ * Or via smoke: `node test/smoke.js`
22
+ */
23
+
24
+ var helpers = require("../helpers");
25
+ var b = helpers.b;
26
+ var check = helpers.check;
27
+
28
+ var STRIPE_ALG = "hmac-sha256-stripe";
29
+
30
+ // Build a valid Stripe-Signature header for (body, secret, ts) through the
31
+ // public round-trip companion — no dependence on another test file's helper.
32
+ function _header(body, secret, ts) {
33
+ return b.webhook.sign({ alg: STRIPE_ALG, secret: secret, body: body, timestamp: ts });
34
+ }
35
+
36
+ // The framework's own replay store satisfies the verifier's contract with
37
+ // no adapter: a first delivery is accepted, a second (sequential) replay of
38
+ // the same signature is refused with the stable webhook/replay code.
39
+ async function testFrameworkStoreDropsIn() {
40
+ var body = '{"event":"checkout.completed","id":"evt_drop_in"}';
41
+ var secret = "whsec_drop_in_secret";
42
+ var ts = Math.floor(Date.now() / 1000);
43
+ var header = _header(body, secret, ts);
44
+ var ns = b.nonceStore.create({ backend: "memory" });
45
+
46
+ var r1 = await b.webhook.verify({
47
+ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: ns,
48
+ });
49
+ check("framework store: first delivery accepted", r1.ok === true);
50
+ check("framework store: scheme v1", r1.scheme === "v1");
51
+ check("framework store: timestamp echoed", r1.timestamp === ts);
52
+
53
+ var replayed = false;
54
+ try {
55
+ await b.webhook.verify({
56
+ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: ns,
57
+ });
58
+ } catch (e) {
59
+ replayed = true;
60
+ check("framework store: replay → webhook/replay", e.code === "webhook/replay");
61
+ }
62
+ check("framework store: sequential replay refused", replayed);
63
+
64
+ if (ns.close) await ns.close();
65
+ }
66
+
67
+ // The core of #328: of two concurrent verifies of the SAME signature with a
68
+ // SHARED framework store, exactly one is accepted and the other is refused as
69
+ // a replay. A non-atomic check-then-set would let both through (the TOCTOU
70
+ // the atomic checkAndInsert closes).
71
+ async function testConcurrentExactlyOneAccepted() {
72
+ var body = '{"event":"invoice.paid","id":"evt_concurrent"}';
73
+ var secret = "whsec_concurrent_secret";
74
+ var ts = Math.floor(Date.now() / 1000);
75
+ var header = _header(body, secret, ts);
76
+ var ns = b.nonceStore.create({ backend: "memory" });
77
+
78
+ var settled = await Promise.allSettled([
79
+ b.webhook.verify({ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: ns }),
80
+ b.webhook.verify({ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: ns }),
81
+ ]);
82
+
83
+ var accepted = settled.filter(function (s) {
84
+ return s.status === "fulfilled" && s.value && s.value.ok === true;
85
+ }).length;
86
+ var replays = settled.filter(function (s) {
87
+ return s.status === "rejected" && s.reason && s.reason.code === "webhook/replay";
88
+ }).length;
89
+
90
+ check("concurrent: exactly one accepted", accepted === 1);
91
+ check("concurrent: exactly one replay", replays === 1);
92
+
93
+ if (ns.close) await ns.close();
94
+ }
95
+
96
+ // A higher-concurrency fan-out hardens the assertion: N byte-identical
97
+ // deliveries land at once and STILL exactly one is accepted — the atomic
98
+ // gate admits a single winner regardless of contention depth.
99
+ async function testConcurrentFanoutSingleWinner() {
100
+ var body = '{"event":"charge.refunded","id":"evt_fanout"}';
101
+ var secret = "whsec_fanout_secret";
102
+ var ts = Math.floor(Date.now() / 1000);
103
+ var header = _header(body, secret, ts);
104
+ var ns = b.nonceStore.create({ backend: "memory" });
105
+
106
+ var N = 8;
107
+ var calls = [];
108
+ for (var i = 0; i < N; i += 1) {
109
+ calls.push(b.webhook.verify({
110
+ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: ns,
111
+ }));
112
+ }
113
+ var settled = await Promise.allSettled(calls);
114
+ var accepted = settled.filter(function (s) {
115
+ return s.status === "fulfilled" && s.value && s.value.ok === true;
116
+ }).length;
117
+ var replays = settled.filter(function (s) {
118
+ return s.status === "rejected" && s.reason && s.reason.code === "webhook/replay";
119
+ }).length;
120
+
121
+ check("fanout: exactly one accepted of " + N, accepted === 1);
122
+ check("fanout: remaining N-1 refused as replay", replays === N - 1);
123
+
124
+ if (ns.close) await ns.close();
125
+ }
126
+
127
+ // The contract migration: a store exposing only the legacy racy { has, set }
128
+ // shape (no checkAndInsert) is refused — the verifier fails closed on a
129
+ // store that cannot give it an atomic gate, rather than silently falling
130
+ // back to a check-then-set race.
131
+ async function testLegacyHasSetShapeRefused() {
132
+ var body = '{"event":"x","id":"evt_legacy"}';
133
+ var secret = "whsec_legacy_shape";
134
+ var ts = Math.floor(Date.now() / 1000);
135
+ var header = _header(body, secret, ts);
136
+ var legacyStore = {
137
+ has: function () { return Promise.resolve(false); },
138
+ set: function () { return Promise.resolve(); },
139
+ };
140
+
141
+ var refused = false;
142
+ try {
143
+ await b.webhook.verify({
144
+ alg: STRIPE_ALG, secret: secret, header: header, body: body, nonceStore: legacyStore,
145
+ });
146
+ } catch (e) {
147
+ refused = true;
148
+ check("legacy { has, set }: webhook/bad-nonce-store code", e.code === "webhook/bad-nonce-store");
149
+ }
150
+ check("legacy { has, set } store refused (no checkAndInsert)", refused);
151
+ }
152
+
153
+ async function run() {
154
+ check("b.webhook.verify is a function", typeof b.webhook.verify === "function");
155
+ check("b.nonceStore.create is a function", typeof b.nonceStore.create === "function");
156
+ await testFrameworkStoreDropsIn();
157
+ await testConcurrentExactlyOneAccepted();
158
+ await testConcurrentFanoutSingleWinner();
159
+ await testLegacyHasSetShapeRefused();
160
+ }
161
+
162
+ module.exports = { run: run };
163
+
164
+ if (require.main === module) {
165
+ run().then(
166
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
167
+ function (e) { console.error("FAIL:", e.message); process.exit(1); }
168
+ );
169
+ }
package/lib/webhooks.js CHANGED
@@ -96,6 +96,14 @@ var SECRET_BYTES = 32;
96
96
  var BACKOFF_SCHEDULE_S = Object.freeze([60, 300, 1800, 14400, 86400]); // allow:raw-time-literal — seconds values; C.TIME returns ms
97
97
  var MAX_ATTEMPTS = 5;
98
98
 
99
+ // processRetries claims each due delivery by pushing its next_retry_at out by
100
+ // this lease before attempting it, so a second tick that overlaps a slow pass
101
+ // skips the in-flight row instead of double-attempting it. _attempt overwrites
102
+ // next_retry_at with the real backoff (or stamps delivered_at), so the lease
103
+ // only persists if the process dies mid-attempt — in which case the row is
104
+ // retried after the lease expires. Comfortably longer than a bounded pass.
105
+ var RETRY_CLAIM_LEASE_MS = C.TIME.minutes(5);
106
+
99
107
  // Window used by the per-endpoint rate-limit sliding count. One
100
108
  // minute is short enough that a wedged receiver doesn't permanently
101
109
  // brick its own delivery feed (the window naturally slides forward),
@@ -539,15 +547,17 @@ function create(opts) {
539
547
  return await _deliver(endpoint, row.event_type, row.payload_json);
540
548
  },
541
549
 
542
- // Polled scheduler entry point — picks deliveries whose
543
- // next_retry_at has come due and re-attempts each. Operators
544
- // wire this to a cron / Durable Object alarm / queue worker.
545
- // Returns the list of deliveries the call attempted.
550
+ // Polled scheduler entry point — picks deliveries whose next_retry_at has
551
+ // come due and re-attempts each. The worker's scheduled() handler drives
552
+ // this once a minute via the container's /_/webhook-retry-tick route.
553
+ // Returns the list of deliveries the call actually attempted (the ones it
554
+ // won the claim on).
546
555
  //
547
- // No claim/lock here the framework's single-writer assumption
548
- // for the deliveries table holds (the worker is the only writer).
549
- // A multi-writer deployment composes external locking before
550
- // calling processRetries.
556
+ // Each row is claimed with an atomic conditional UPDATE before it is
557
+ // attempted, so two overlapping ticks (a slow pass still running when the
558
+ // next minute fires) can never double-attempt the same delivery the
559
+ // claim moves next_retry_at out of the due window, and only the tick whose
560
+ // UPDATE matched (rowCount === 1) calls _attempt.
551
561
  processRetries: async function (procOpts) {
552
562
  var now = (procOpts && typeof procOpts.now === "number") ? procOpts.now : nowFn();
553
563
  var max = (procOpts && Number.isInteger(procOpts.max) && procOpts.max > 0) ? procOpts.max : 100;
@@ -561,8 +571,27 @@ function create(opts) {
561
571
  var attempted = [];
562
572
  for (var i = 0; i < r.rows.length; i += 1) {
563
573
  var row = r.rows[i];
574
+ // Skip a delivery whose endpoint has been deleted or PAUSED. Normal
575
+ // fan-out only sends to active endpoints, so the automatic cron must
576
+ // not keep POSTing to one an operator explicitly disabled. Checked
577
+ // BEFORE the claim, so a paused endpoint's pending retries keep their
578
+ // schedule position (next_retry_at untouched) and resume when it is
579
+ // re-enabled, rather than the lease churning forward every tick. A
580
+ // manual admin retry remains an explicit operator override.
564
581
  var endpoint = await _getEndpoint(row.endpoint_id);
565
- if (!endpoint) continue;
582
+ if (!endpoint || !(endpoint.active === 1 || endpoint.active === true)) continue;
583
+ // Atomic claim: push next_retry_at out by the lease so a concurrent
584
+ // tick's identical WHERE no longer matches this row. Only the winner
585
+ // (rowCount === 1) attempts; _attempt then overwrites next_retry_at
586
+ // with the real backoff or stamps delivered_at.
587
+ var claim = await query(
588
+ "UPDATE webhook_deliveries SET next_retry_at = ?1 " +
589
+ "WHERE id = ?2 AND next_retry_at IS NOT NULL AND next_retry_at <= ?3 AND delivered_at IS NULL",
590
+ [now + RETRY_CLAIM_LEASE_MS, row.id, now],
591
+ );
592
+ var claimed = claim && (claim.rowCount != null ? claim.rowCount
593
+ : (claim.meta && claim.meta.changes != null ? claim.meta.changes : claim.changes));
594
+ if (Number(claimed || 0) !== 1) continue; // lost the claim to a concurrent tick
566
595
  var d = await _attempt(row.id, endpoint, row.event_type, row.payload_json);
567
596
  attempted.push(d);
568
597
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.85",
3
+ "version": "0.4.87",
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": {