@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.
- package/CHANGELOG.md +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/cart.js +75 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/storefront.js +8 -1
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- 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
|
-
//
|
|
544
|
-
//
|
|
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
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
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