@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,87 @@
1
+ "use strict";
2
+ // b.atomicFile.openAppendNoFollowSync — O_NOFOLLOW append open for long-lived
3
+ // append sinks (active log file kept open across appends + reopened on
4
+ // rotation). Creates the file if absent and appends if it is a regular file,
5
+ // but refuses a symlink at the final path component (ELOOP) instead of
6
+ // following it — so log writes cannot be redirected to an attacker-chosen
7
+ // file (CWE-59). RED before 0.15.16: log-stream-local opened the active log
8
+ // with a bare `openSync(path, "a")`, following a symlink planted at the path.
9
+
10
+ var fs = require("node:fs");
11
+ var os = require("node:os");
12
+ var path = require("node:path");
13
+ var helpers = require("../helpers");
14
+ var b = helpers.b;
15
+ var check = helpers.check;
16
+ var localProto = require("../../lib/log-stream-local");
17
+
18
+ function run() {
19
+ check("b.atomicFile.openAppendNoFollowSync is a function",
20
+ typeof b.atomicFile.openAppendNoFollowSync === "function");
21
+
22
+ var dir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-append-nofollow-"));
23
+ try {
24
+ // --- primitive: create-if-absent + append to a regular file ---
25
+ var p = path.join(dir, "active.log");
26
+ var fd = b.atomicFile.openAppendNoFollowSync(p, 0o600);
27
+ check("returns a numeric fd, creating the file if absent",
28
+ typeof fd === "number" && fd >= 0 && fs.existsSync(p));
29
+ fs.writeSync(fd, Buffer.from("line-1\n"));
30
+ fs.closeSync(fd);
31
+ var fd2 = b.atomicFile.openAppendNoFollowSync(p, 0o600);
32
+ fs.writeSync(fd2, Buffer.from("line-2\n"));
33
+ fs.closeSync(fd2);
34
+ check("appends rather than truncates an existing regular file",
35
+ fs.readFileSync(p, "utf8") === "line-1\nline-2\n");
36
+
37
+ // --- primitive: symlink at the final component is refused (POSIX) ---
38
+ if (process.platform !== "win32") {
39
+ var victim = path.join(dir, "victim.txt");
40
+ fs.writeFileSync(victim, "ORIGINAL");
41
+ var linkPath = path.join(dir, "evil.log");
42
+ var symlinkMade = false;
43
+ try { fs.symlinkSync(victim, linkPath); symlinkMade = true; } catch (_e) { /* no symlink priv */ }
44
+ if (symlinkMade) {
45
+ var loopCode = null;
46
+ try {
47
+ var bad = b.atomicFile.openAppendNoFollowSync(linkPath, 0o600);
48
+ fs.closeSync(bad);
49
+ } catch (e) { loopCode = e && e.code; }
50
+ check("symlink final component refused with ELOOP (not followed)", loopCode === "ELOOP");
51
+ check("the symlink target was never written through",
52
+ fs.readFileSync(victim, "utf8") === "ORIGINAL");
53
+ }
54
+
55
+ // --- consumer path (#356): log-stream-local refuses a symlinked active log ---
56
+ var logDir = path.join(dir, "logs");
57
+ fs.mkdirSync(logDir, { recursive: true });
58
+ var logVictim = path.join(dir, "log-victim.txt");
59
+ fs.writeFileSync(logVictim, "VICTIM-LOG");
60
+ var activePath = path.join(logDir, "blamejs.log"); // <prefix>.log
61
+ var planted = false;
62
+ try { fs.symlinkSync(logVictim, activePath); planted = true; } catch (_e) { /* no priv */ }
63
+ if (planted) {
64
+ var refusedCode = null;
65
+ var sink = null;
66
+ try { sink = localProto.create({ dir: logDir }); }
67
+ catch (e) { refusedCode = e && e.code; }
68
+ if (sink) { try { sink.close(); } catch (_c) { /* ignore */ } }
69
+ check("log-stream-local refuses to open a symlinked active log path",
70
+ refusedCode === "SYMLINK_REFUSED");
71
+ check("log-stream-local never writes through the symlink to the victim",
72
+ fs.readFileSync(logVictim, "utf8") === "VICTIM-LOG");
73
+ }
74
+ }
75
+ } finally {
76
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
77
+ }
78
+ return Promise.resolve();
79
+ }
80
+
81
+ module.exports = { run: run };
82
+ if (require.main === module) {
83
+ Promise.resolve(run()).then(
84
+ function () { console.log("OK — atomicFile.openAppendNoFollowSync + log-stream-local symlink refusal (" + helpers.getChecks() + " checks)"); process.exit(0); },
85
+ function (e) { console.error("FAIL: " + (e && e.message)); process.exit(1); }
86
+ );
87
+ }
@@ -8546,6 +8546,54 @@ var KNOWN_ANTIPATTERNS = [
8546
8546
  ],
8547
8547
  reason: "Symlink-follow / torn-write class (CWE-59 / CWE-377). A raw nodeFs.writeFileSync / nodeFs.createWriteStream to a destination path follows a symlink an attacker pre-planted there and can leave a half-written file on a crash. The §1 sweep routed every hand-rolled write through an atomic-file primitive: writeSync (buffer payloads — cookie-jar flush, archive/tar extract, backup payloads, p12 export, api-snapshot, sealed-pem marker), writeStream (object-store streaming put — staged into a no-follow exclusive temp + atomic rename, capped by maxBytes), and writeExclSync (the vault seal/unseal/rotate staged write that must re-read + verify the bytes before the rename — clears any stale entry then O_EXCL|O_NOFOLLOW creates so a re-planted symlink fails closed). The three allowlisted files are the genuinely-safe forms: atomic-file.js owns the primitives (its createWriteStream binds an _openExclTemp fd); backup/index.js writeFileSync passes { flag: 'wx' } (O_EXCL refuses a pre-planted target); http-client.js opens its download temp with explicit O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW flags. Any new raw nodeFs.writeFileSync/createWriteStream elsewhere trips this — call the matching atomic-file primitive instead.",
8548
8548
  },
8549
+ {
8550
+ id: "raw-fs-append-open-without-nofollow",
8551
+ primitive: "b.atomicFile.openAppendNoFollowSync — O_WRONLY|O_APPEND|O_CREAT|O_NOFOLLOW append open",
8552
+ // The append-sink sibling of raw-fs-write-without-exclusive-nofollow. A
8553
+ // long-lived append target (an active log file kept open across appends and
8554
+ // reopened on rotation) can't use a one-shot atomic write, but a bare
8555
+ // nodeFs.openSync(path, "a") / appendFileSync(path, ...) still FOLLOWS a
8556
+ // symlink an attacker pre-planted at `path` (CWE-59) — redirecting the
8557
+ // append stream to a victim file. openAppendNoFollowSync adds O_NOFOLLOW so
8558
+ // the symlink is refused (ELOOP) atomically with the open, closing the
8559
+ // caller-pre-check-then-unlink race. The primitive itself opens with a
8560
+ // numeric flags variable (not the "a" string), so its own home does not
8561
+ // match and needs no allowlist.
8562
+ scanScope: "lib",
8563
+ skipCommentLines: true,
8564
+ regex: /\bnodeFs\.(?:openSync\s*\(\s*[^,]+,\s*"a"|appendFileSync\s*\(|appendFile\s*\()/,
8565
+ allowlist: [],
8566
+ reason: "Symlink-follow append class (CWE-59). A bare nodeFs.openSync(path, \"a\") or nodeFs.appendFileSync(path, ...) follows a symlink an attacker pre-planted at the append target and redirects the log/append stream to an attacker-chosen file; a caller's pre-check-then-unlink can't close the race because the sink's own open is what follows the link. The fix routes every long-lived append open through b.atomicFile.openAppendNoFollowSync (O_WRONLY|O_APPEND|O_CREAT|O_NOFOLLOW): the file is created/appended normally but a symlink at the final component fails the open closed with ELOOP. The v0.15.16 sweep converted log-stream-local.js (active log sink) and daemon.js (detached-process stdout/stderr log). Zero allowlist — the primitive opens with a numeric flags variable, so any new nodeFs.openSync(..., \"a\") / appendFileSync / appendFile in lib/ trips this; call b.atomicFile.openAppendNoFollowSync instead.",
8567
+ },
8568
+ {
8569
+ id: "httpclient-request-idle-timeout-without-wall-clock",
8570
+ primitive: "forward BOTH timeoutMs (overall wall-clock) and idleTimeoutMs to httpClient.request — never idle-only",
8571
+ // #355 class. httpClient distinguishes timeoutMs (overall wall-clock cap)
8572
+ // from idleTimeoutMs (zero-progress cap). A consumer that maps its single
8573
+ // configured timeout to idleTimeoutMs ONLY has no overall bound: a peer
8574
+ // trickling bytes within the idle window holds the request open forever
8575
+ // (slow-loris). Every httpClient request object that sets idleTimeoutMs must
8576
+ // also set timeoutMs on the line immediately before it. This detector flags
8577
+ // an `idleTimeoutMs:` property (identifier/member value) whose preceding line
8578
+ // is NOT a `timeoutMs:` line. The allowlist is the SERVER-side / pool idle
8579
+ // timeouts — inbound connection inactivity disconnect (RFC 5321 §4.5.3.2.7
8580
+ // for SMTP, IMAP/POP3/ManageSieve/submission/TLS) and the external-db
8581
+ // connection pool — a structurally different concept (a long-lived server
8582
+ // socket has no per-request wall-clock to cap), NOT an outbound httpClient
8583
+ // request.
8584
+ scanScope: "lib",
8585
+ skipCommentLines: true,
8586
+ regex: /\n[ \t]*(?!timeoutMs\b)[A-Za-z_$][\w$]*:[^\n]*\n[ \t]*idleTimeoutMs:[ \t]*[A-Za-z_$]/,
8587
+ allowlist: [
8588
+ "lib/mail-server-imap.js", // inbound IMAP server socket idle disconnect
8589
+ "lib/mail-server-managesieve.js", // inbound ManageSieve server socket idle disconnect
8590
+ "lib/mail-server-mx.js", // inbound SMTP/MX server socket idle (RFC 5321 §4.5.3.2.7)
8591
+ "lib/mail-server-pop3.js", // inbound POP3 server socket idle disconnect
8592
+ "lib/mail-server-submission.js", // inbound submission server socket idle disconnect
8593
+ "lib/mail-server-tls.js", // inbound TLS server socket idle (re-armed post-handshake)
8594
+ ],
8595
+ reason: "Slow-trickle hold-open class (#355, CWE-400). httpClient.request takes timeoutMs (overall wall-clock cap) AND idleTimeoutMs (zero-progress cap) as DISTINCT bounds. A consumer wrapping httpClient that forwards only idleTimeoutMs leaves the request unbounded in wall-clock time: a peer that emits one byte just under the idle window keeps resetting the idle timer forever, holding the call open indefinitely. The v0.15.16 sweep set timeoutMs alongside idleTimeoutMs in every httpClient consumer (api-encrypt httpClient, the object-store shared request wrapper + azure/gcs bucket-ops, queue-sqs, the cloudwatch/otlp/webhook log sinks, the HIBP password breach check). The detector requires timeoutMs on the line immediately before every idleTimeoutMs property; the only allowlisted files are inbound SERVER socket idle timeouts and the external-db pool idle — a long-lived server connection's inactivity disconnect, not an outbound request needing a per-request wall-clock cap. A new httpClient consumer forwarding idle-only trips this — set timeoutMs too.",
8596
+ },
8549
8597
  {
8550
8598
  id: "allowlist-map-indexed-by-untrusted-key-without-hasown",
8551
8599
  primitive: "Object.prototype.hasOwnProperty.call(MAP, key) — proto-shadow-safe allowlist membership",
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ // #343: externalDb.transaction (and the b.outbox built on it) silently
3
+ // non-atomic on a stateless / autocommit-per-statement adapter.
4
+ //
5
+ // transaction(fn) defaults its tx verbs to query(client,"BEGIN") /
6
+ // query(client,"COMMIT") when the adapter supplies no beginTx/commit/
7
+ // rollback. On a stateless adapter — connect() returns a sentinel and
8
+ // every query() is an independent round-trip (e.g. an HTTP bridge to
9
+ // Cloudflare D1) — BEGIN, the body statements, and COMMIT each land on a
10
+ // DIFFERENT session: no isolation, no rollback. Yet the call resolves, so
11
+ // the consumer believes the block was atomic. b.outbox is built entirely
12
+ // on this (enqueue requires the txClient), so its dual-write guarantee is
13
+ // void on such a backend.
14
+ //
15
+ // FIX: a backend declares supportsTransactions:false (the stateless
16
+ // declaration). transaction() and outbox.create() then REFUSE LOUDLY with
17
+ // a typed error (fail closed) instead of running BEGIN/COMMIT as separate
18
+ // no-ops and resolving.
19
+ //
20
+ // RED on the buggy tree: transaction(fn) runs (BEGIN/COMMIT as independent
21
+ // no-op round-trips) and RESOLVES; outbox.create() succeeds. GREEN after
22
+ // the fix: both throw NON_ATOMIC_BACKEND / outbox/non-atomic-backend.
23
+
24
+ var helpers = require("../helpers");
25
+ var b = helpers.b;
26
+ var check = helpers.check;
27
+
28
+ // A faithful STATELESS adapter: connect() returns a fresh sentinel each
29
+ // call, every query() is an independent round-trip against a shared store,
30
+ // and there is NO beginTx/commit/rollback hook. BEGIN/COMMIT/ROLLBACK are
31
+ // accepted as harmless no-ops (exactly what an autocommit bridge does).
32
+ function _makeStatelessAdapter() {
33
+ var store = {};
34
+ var connectCount = 0;
35
+ var sawBegin = false;
36
+ return {
37
+ sawBegin: function () { return sawBegin; },
38
+ connect: async function () { connectCount += 1; return { sentinel: connectCount }; },
39
+ query: async function (_client, sql, params) {
40
+ if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(sql)) { sawBegin = true; return { rows: [], rowCount: 0 }; }
41
+ if (/^INSERT INTO kv/i.test(sql)) { store[params[0]] = params[1]; return { rows: [], rowCount: 1 }; }
42
+ if (/^SELECT/i.test(sql)) {
43
+ var v = store[params && params[0]];
44
+ return v === undefined ? { rows: [], rowCount: 0 } : { rows: [{ value: v }], rowCount: 1 };
45
+ }
46
+ return { rows: [], rowCount: 0 };
47
+ },
48
+ close: async function () {},
49
+ };
50
+ }
51
+
52
+ async function run() {
53
+ // ---- transaction() refuses a stateless backend ----
54
+ b.externalDb._resetForTest();
55
+ var stateless = _makeStatelessAdapter();
56
+ b.externalDb.init({
57
+ backends: {
58
+ d1: {
59
+ dialect: "sqlite",
60
+ connect: stateless.connect,
61
+ query: stateless.query,
62
+ close: stateless.close,
63
+ supportsTransactions: false, // stateless / autocommit-per-statement declaration
64
+ },
65
+ },
66
+ });
67
+
68
+ var threw = null;
69
+ var bodyRan = false;
70
+ try {
71
+ await b.externalDb.transaction(async function (tx) {
72
+ bodyRan = true;
73
+ await tx.query("INSERT INTO kv (id, value) VALUES ($1, $2)", ["k", "v"]);
74
+ });
75
+ } catch (e) { threw = e; }
76
+ check("#343 transaction() refuses a stateless backend (typed NON_ATOMIC_BACKEND)",
77
+ threw && /NON_ATOMIC_BACKEND/.test(threw.code || ""));
78
+ check("#343 transaction() refuses BEFORE running the body / any BEGIN",
79
+ bodyRan === false && stateless.sawBegin() === false);
80
+
81
+ // write.transaction routes through transaction() — must refuse too.
82
+ var threwWrite = null;
83
+ try {
84
+ await b.externalDb.write.transaction(async function (tx) { await tx.query("SELECT 1", []); });
85
+ } catch (e) { threwWrite = e; }
86
+ check("#343 write.transaction() also refuses a stateless backend",
87
+ threwWrite && /NON_ATOMIC_BACKEND/.test(threwWrite.code || ""));
88
+
89
+ // supportsTransactions probe reflects the declaration.
90
+ check("#343 supportsTransactions() probe returns false for the stateless backend",
91
+ b.externalDb.supportsTransactions() === false);
92
+
93
+ // ---- outbox.create() refuses a stateless backend ----
94
+ var outboxThrew = null;
95
+ try {
96
+ b.outbox.create({
97
+ externalDb: b.externalDb,
98
+ table: "outbox_events",
99
+ publisher: async function () {},
100
+ audit: false,
101
+ });
102
+ } catch (e) { outboxThrew = e; }
103
+ check("#343 outbox.create() refuses a stateless backend (typed error)",
104
+ outboxThrew && /non-atomic-backend/.test(outboxThrew.code || ""));
105
+
106
+ // A custom externalDb object that declares supportsTransactions:false is
107
+ // refused identically (outbox accepts the namespace OR a faithful object).
108
+ var customOutboxThrew = null;
109
+ try {
110
+ b.outbox.create({
111
+ externalDb: {
112
+ dialect: "sqlite",
113
+ query: async function () { return { rows: [] }; },
114
+ transaction: async function (fn) { return fn({ query: async function () { return { rows: [] }; } }); },
115
+ supportsTransactions: false,
116
+ },
117
+ table: "outbox_events",
118
+ publisher: async function () {},
119
+ audit: false,
120
+ });
121
+ } catch (e) { customOutboxThrew = e; }
122
+ check("#343 outbox.create() refuses a custom externalDb declaring supportsTransactions:false",
123
+ customOutboxThrew && /non-atomic-backend/.test(customOutboxThrew.code || ""));
124
+
125
+ // ---- no regression: a STATEFUL backend (the historical default) still works ----
126
+ b.externalDb._resetForTest();
127
+ var stateful = helpers._makeFakeDriver();
128
+ b.externalDb.init({
129
+ backends: { main: { connect: stateful.connect, query: stateful.query, close: stateful.close, ping: stateful.ping } },
130
+ });
131
+ var statefulOk = false;
132
+ var got = await b.externalDb.transaction(async function (tx) {
133
+ await tx.query("INSERT INTO kv (id, value) VALUES ($1, $2)", ["a", "1"]);
134
+ var r = await tx.query("SELECT id, value FROM kv WHERE id = $1", ["a"]);
135
+ statefulOk = r.rowCount === 1;
136
+ return r.rows[0];
137
+ });
138
+ check("#343 a stateful backend (no flag, no hooks) still runs transactions",
139
+ statefulOk && got && got.value === "1");
140
+ check("#343 supportsTransactions() probe returns true for a stateful backend",
141
+ b.externalDb.supportsTransactions() === true);
142
+ check("#343 outbox.create() succeeds on a stateful backend",
143
+ (function () {
144
+ try {
145
+ b.outbox.create({ externalDb: b.externalDb, table: "outbox_events",
146
+ publisher: async function () {}, audit: false });
147
+ return true;
148
+ } catch (_e) { return false; }
149
+ })());
150
+
151
+ // ---- an explicit supportsTransactions:true (stateful operator assertion) is permitted ----
152
+ b.externalDb._resetForTest();
153
+ var asserted = helpers._makeFakeDriver();
154
+ b.externalDb.init({
155
+ backends: {
156
+ mainAsserted: {
157
+ connect: asserted.connect,
158
+ query: asserted.query,
159
+ close: asserted.close,
160
+ ping: asserted.ping,
161
+ supportsTransactions: true, // operator asserts the default BEGIN/COMMIT runs on a stateful client
162
+ },
163
+ },
164
+ });
165
+ check("#343 supportsTransactions:true is honored (transaction not refused)",
166
+ b.externalDb.supportsTransactions() === true);
167
+
168
+ // ---- a stateless adapter declaring supportsTransactions:false is refused even WITH a batch hook ----
169
+ // (batch is a separate static-statement atomic path; it cannot make an
170
+ // interactive transaction(fn) safe, so it does NOT flip the capability.)
171
+ b.externalDb._resetForTest();
172
+ var batchAdapter = _makeStatelessAdapter();
173
+ b.externalDb.init({
174
+ backends: {
175
+ d1batch: {
176
+ dialect: "sqlite",
177
+ connect: batchAdapter.connect,
178
+ query: batchAdapter.query,
179
+ close: batchAdapter.close,
180
+ supportsTransactions: false,
181
+ batch: async function () { return; },
182
+ },
183
+ },
184
+ });
185
+ var batchThrew = null;
186
+ try {
187
+ await b.externalDb.transaction(async function (tx) { await tx.query("SELECT 1", []); });
188
+ } catch (e) { batchThrew = e; }
189
+ check("#343 a stateless adapter with only a batch hook still refuses interactive transaction()",
190
+ batchThrew && /NON_ATOMIC_BACKEND/.test(batchThrew.code || ""));
191
+
192
+ b.externalDb._resetForTest();
193
+ console.log("OK — externalDb non-atomic backend refusal (#343)");
194
+ }
195
+
196
+ module.exports = { run: run };
197
+ if (require.main === module) {
198
+ run().then(function () { process.exit(0); })
199
+ .catch(function (err) { process.stderr.write(String(err && err.stack || err) + "\n"); process.exit(1); });
200
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * b.auth.fidoMds3 — x5c cert validity-window must fail closed when the
4
+ * notBefore / notAfter dates are present but unparseable.
5
+ *
6
+ * _validateChain reads chain[v].validFrom / .validTo and feeds them to
7
+ * Date.parse. A cert whose encoded validity dates parse to NaN (e.g. a
8
+ * malformed / hand-forged DER) makes isFinite(notBefore) /
9
+ * isFinite(notAfter) false, which — pre-fix — SKIPS both the
10
+ * not-yet-valid and the expired checks, so the cert sails through the
11
+ * validity window unchecked. The verifier must instead refuse with
12
+ * fido-mds3/cert-bad-validity.
13
+ *
14
+ * The test drives the real fetch() consumer path with a mocked
15
+ * httpClient serving a genuine RS256-signed BLOB whose self-signed leaf
16
+ * is also the operator-supplied caCertificate, so absent the validity
17
+ * bug the chain anchors and the JWS verifies — the validity-window
18
+ * check is the only gate left. node:crypto's X509Certificate is
19
+ * overridden so the parsed leaf reports an unparseable validFrom while
20
+ * every other cert behaviour (verify / checkIssued / publicKey /
21
+ * fingerprint256) stays real.
22
+ */
23
+
24
+ var nodeCrypto = require("node:crypto");
25
+ var helpers = require("../helpers");
26
+ var check = helpers.check;
27
+
28
+ // Mint an RSA self-signed cert + matching private-key PEM via the
29
+ // vendored pki bundle, mirroring the sibling fido-mds3 test fixture.
30
+ async function _makeSelfSignedRsaCert() {
31
+ var pki = require("../../lib/vendor/pki.cjs");
32
+ var x509 = pki.x509;
33
+ var keys = await nodeCrypto.webcrypto.subtle.generateKey(
34
+ { name: "RSASSA-PKCS1-v1_5",
35
+ modulusLength: 2048,
36
+ publicExponent: new Uint8Array([1, 0, 1]),
37
+ hash: "SHA-256" },
38
+ true, ["sign", "verify"]);
39
+ var now = new Date();
40
+ var notAfter = new Date(now.getTime() + 7 * 86400000);
41
+ var cert = await x509.X509CertificateGenerator.createSelfSigned({
42
+ serialNumber: "01",
43
+ name: "CN=blamejs-mds3-test-root",
44
+ notBefore: now,
45
+ notAfter: notAfter,
46
+ signingAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
47
+ keys: keys,
48
+ extensions: [
49
+ new x509.BasicConstraintsExtension(true, 0, true),
50
+ new x509.KeyUsagesExtension(
51
+ x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.digitalSignature,
52
+ true),
53
+ ],
54
+ });
55
+ var pkcs8 = await nodeCrypto.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
56
+ var pkB64 = Buffer.from(pkcs8).toString("base64").match(/.{1,64}/g).join("\n");
57
+ var keyPem = "-----BEGIN PRIVATE KEY-----\n" + pkB64 + "\n-----END PRIVATE KEY-----\n";
58
+ return { keyPem: keyPem, certPem: cert.toString("pem") };
59
+ }
60
+
61
+ function _b64url(buf) {
62
+ return Buffer.from(buf).toString("base64url");
63
+ }
64
+
65
+ function _makeBlob(payload, keyPem, certPem) {
66
+ var derB64 = certPem
67
+ .replace(/-----BEGIN CERTIFICATE-----/g, "")
68
+ .replace(/-----END CERTIFICATE-----/g, "")
69
+ .replace(/\s+/g, "");
70
+ var header = { alg: "RS256", typ: "JWT", x5c: [derB64] };
71
+ var headerB64 = _b64url(JSON.stringify(header));
72
+ var payloadB64 = _b64url(JSON.stringify(payload));
73
+ var signingInput = headerB64 + "." + payloadB64;
74
+ var sig = nodeCrypto.sign("sha256", Buffer.from(signingInput, "ascii"), {
75
+ key: keyPem,
76
+ padding: nodeCrypto.constants.RSA_PKCS1_PADDING,
77
+ });
78
+ return signingInput + "." + _b64url(sig);
79
+ }
80
+
81
+ function _futureDateString(daysFromNow) {
82
+ var d = new Date(Date.now() + daysFromNow * 86400000);
83
+ return d.toISOString().slice(0, 10);
84
+ }
85
+
86
+ // Make every freshly-parsed X509Certificate report an unparseable
87
+ // validFrom (Date.parse -> NaN) while keeping all real cert behaviour.
88
+ async function _withUnparseableValidFrom(fn) {
89
+ var realX509 = nodeCrypto.X509Certificate;
90
+ function Wrapped(arg) {
91
+ var c = new realX509(arg);
92
+ Object.defineProperty(c, "validFrom", {
93
+ configurable: true,
94
+ get: function () { return "GARBAGE-NOT-A-DATE"; },
95
+ });
96
+ return c;
97
+ }
98
+ nodeCrypto.X509Certificate = Wrapped;
99
+ try { return await fn(); }
100
+ finally { nodeCrypto.X509Certificate = realX509; }
101
+ }
102
+
103
+ async function run() {
104
+ var pair = await _makeSelfSignedRsaCert();
105
+ var payload = {
106
+ legalHeader: "Test BLOB",
107
+ no: 1,
108
+ nextUpdate: _futureDateString(7),
109
+ entries: [{
110
+ aaguid: "01234567-89ab-cdef-0123-456789abcdef",
111
+ metadataStatement: { description: "Test entry" },
112
+ statusReports: [{ status: "FIDO_CERTIFIED_L2" }],
113
+ }],
114
+ };
115
+ var token = _makeBlob(payload, pair.keyPem, pair.certPem);
116
+
117
+ // Mock httpClient.request via require-cache override (sibling pattern).
118
+ var hcPath = require.resolve("../../lib/http-client");
119
+ var origHc = require.cache[hcPath].exports;
120
+ require.cache[hcPath].exports = Object.assign({}, origHc, {
121
+ request: async function () {
122
+ return { statusCode: 200, headers: {}, body: Buffer.from(token, "ascii") };
123
+ },
124
+ });
125
+
126
+ // Re-require the verifier so it captures both the mocked httpClient and
127
+ // the wrapped X509Certificate through its module-level references.
128
+ var fmPath = require.resolve("../../lib/auth/fido-mds3");
129
+ delete require.cache[fmPath];
130
+ var fm = require(fmPath);
131
+
132
+ var threw = null;
133
+ try {
134
+ await _withUnparseableValidFrom(async function () {
135
+ await fm.fetch({
136
+ url: "https://test.invalid/mds3",
137
+ caCertificate: pair.certPem,
138
+ force: true,
139
+ });
140
+ });
141
+ } catch (e) {
142
+ threw = e;
143
+ } finally {
144
+ require.cache[hcPath].exports = origHc;
145
+ delete require.cache[fmPath];
146
+ }
147
+
148
+ check("fetch rejects a cert with present-but-unparseable validity dates",
149
+ threw !== null);
150
+ check("rejection is fido-mds3/cert-bad-validity (fail closed, not silently accepted)",
151
+ threw && /cert-bad-validity/.test(threw.code || ""));
152
+ }
153
+
154
+ module.exports = { run: run };
155
+
156
+ if (require.main === module) {
157
+ run().then(function () { console.log("OK"); })
158
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
159
+ }
@@ -38,14 +38,61 @@ async function run() {
38
38
 
39
39
  // nonceHeader override — the replay nonce can be read from a custom
40
40
  // request header (not just the Apollo-vendor default), for operators
41
- // fronting the gateway with a non-Apollo router.
41
+ // fronting the gateway with a non-Apollo router. The replay store speaks
42
+ // the framework's ATOMIC checkAndInsert(nonce, expireAt) contract.
42
43
  var withNonce = b.graphqlFederation.guardSdl({
43
44
  routerToken: "a".repeat(64),
44
- nonceStore: { has: function () { return false; }, remember: function () {} },
45
+ nonceStore: { checkAndInsert: function () { return true; } },
45
46
  nonceHeader: "x-my-nonce",
46
47
  });
47
48
  check("guardSdl: accepts custom nonceHeader", typeof withNonce === "function");
48
49
 
50
+ // #328 sibling — the replay nonceStore must speak the framework's ATOMIC
51
+ // checkAndInsert(nonce, expireAt) contract (was a racy non-atomic
52
+ // has()-then-remember() check-then-set). A store lacking checkAndInsert —
53
+ // including the legacy { has, remember } shape — is refused at config time.
54
+ var racyStoreThrew = null;
55
+ try {
56
+ b.graphqlFederation.guardSdl({
57
+ routerToken: "a".repeat(64),
58
+ nonceStore: { has: function () { return false; }, remember: function () {} },
59
+ });
60
+ } catch (e) { racyStoreThrew = e; }
61
+ check("guardSdl: refuses a nonceStore lacking checkAndInsert (legacy racy has/remember)",
62
+ racyStoreThrew && racyStoreThrew.code === "graphql-federation/bad-nonce-store");
63
+
64
+ // The framework's own b.nonceStore.create drops straight in, and a replay of
65
+ // the SAME nonce on a valid bearer _service probe is refused (401) — driving
66
+ // the REAL consumer path through the atomic store.
67
+ var nonceStore = b.nonceStore.create({ backend: "memory" });
68
+ var nguard = b.graphqlFederation.guardSdl({
69
+ routerToken: "a".repeat(64), nonceStore: nonceStore, audit: false,
70
+ });
71
+ function _driveNonce(nonce) {
72
+ return new Promise(function (resolve) {
73
+ var out = { status: null, nextCalled: false };
74
+ var res = {
75
+ statusCode: 200,
76
+ setHeader: function () {}, getHeader: function () { return undefined; },
77
+ writeHead: function (s) { out.status = s; },
78
+ end: function () { if (out.status === null) out.status = res.statusCode; resolve(out); },
79
+ };
80
+ nguard({
81
+ method: "POST",
82
+ headers: { authorization: "Bearer " + "a".repeat(64),
83
+ "x-apollographql-router-nonce": nonce },
84
+ body: { query: "{ _service { sdl } }" },
85
+ on: function () {},
86
+ }, res, function () { out.nextCalled = true; resolve(out); });
87
+ });
88
+ }
89
+ var freshNonce = await _driveNonce("nonce-aaaaaaaaaaaaaaaa");
90
+ check("guardSdl: fresh nonce on a valid bearer _service probe is forwarded",
91
+ freshNonce.nextCalled === true && freshNonce.status === null);
92
+ var replayNonce = await _driveNonce("nonce-aaaaaaaaaaaaaaaa");
93
+ check("guardSdl: a replayed nonce is refused (401), not forwarded",
94
+ replayNonce.status === 401 && replayNonce.nextCalled === false);
95
+
49
96
  // ---- guardSdl drives: a batched ARRAY body whose SECOND operation probes
50
97
  // _service must be refused (401) without a router token, not forwarded. ----
51
98
  function _drive(mw, body) {