@blamejs/blamejs-shop 0.4.87 → 0.4.89

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 (46) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/SECURITY.md +22 -9
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/gift-card-ledger.js +72 -15
  5. package/lib/store-credit.js +340 -89
  6. package/lib/vendor/MANIFEST.json +53 -31
  7. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +7 -2
  8. package/lib/vendor/blamejs/.github/dependabot.yml +12 -0
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +16 -12
  10. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +3 -1
  11. package/lib/vendor/blamejs/.github/workflows/release-container.yml +23 -0
  12. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  13. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  14. package/lib/vendor/blamejs/examples/wiki/Dockerfile +19 -2
  15. package/lib/vendor/blamejs/lib/atomic-file.js +32 -9
  16. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +105 -40
  17. package/lib/vendor/blamejs/lib/middleware/dpop.js +54 -31
  18. package/lib/vendor/blamejs/lib/middleware/span-http-server.js +7 -0
  19. package/lib/vendor/blamejs/lib/network-tls.js +12 -2
  20. package/lib/vendor/blamejs/lib/queue-local.js +123 -46
  21. package/lib/vendor/blamejs/lib/queue.js +13 -9
  22. package/lib/vendor/blamejs/lib/request-helpers.js +90 -0
  23. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +69 -7
  24. package/lib/vendor/blamejs/lib/self-update.js +11 -2
  25. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
  26. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +6 -2
  27. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +689 -688
  28. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +5 -0
  29. package/lib/vendor/blamejs/package.json +1 -1
  30. package/lib/vendor/blamejs/release-notes/v0.15.17.json +52 -0
  31. package/lib/vendor/blamejs/release-notes/v0.15.18.json +49 -0
  32. package/lib/vendor/blamejs/release-notes/v0.15.19.json +18 -0
  33. package/lib/vendor/blamejs/scripts/check-vendor-currency.js +24 -0
  34. package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +419 -0
  35. package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +471 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +309 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -8
  38. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +138 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +26 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +227 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +83 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +90 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +274 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +192 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +36 -0
  46. package/package.json +1 -1
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ /**
3
+ * DPoP htu reconstruction must peer-gate X-Forwarded-Proto / X-Forwarded-Host
4
+ * (RFC 9449 §4.3). The htu (scheme + authority + path) is cryptographically
5
+ * bound in the proof, so if the server builds it from a forgeable forwarded
6
+ * header trusted from ANY caller, a direct attacker can forge
7
+ * X-Forwarded-Proto: https / a victim X-Forwarded-Host and make a proof signed
8
+ * for one origin validate against another (htu confusion).
9
+ *
10
+ * dpop previously honored both via a bare boolean opts.trustForwardedHeaders,
11
+ * which trusted the headers with no immediate-peer check — the same forgeable
12
+ * class csrf-protect / security-headers / cors already closed via the
13
+ * peer-gated requestHelpers resolvers. This brings dpop onto the same
14
+ * fail-closed model: trustForwardedHeaders is refused on its own, and proto +
15
+ * host resolve through b.requestHelpers.trustedProtocol / trustedHost, honoring
16
+ * the forwarded headers only from a declared trusted-proxy peer.
17
+ *
18
+ * Covers the two new peer-gated primitives (requestHost / trustedHost), the
19
+ * dpop create()-time refusal, and the end-to-end htu decision.
20
+ *
21
+ * Run standalone: node test/layer-0-primitives/dpop-htu-peergating.test.js
22
+ */
23
+
24
+ var nodeCrypto = require("node:crypto");
25
+ var helpers = require("../helpers");
26
+ var b = helpers.b;
27
+ var check = helpers.check;
28
+
29
+ function _ns() { return b.nonceStore.create({ backend: "memory" }); }
30
+
31
+ function _mkRes() {
32
+ var res = {
33
+ statusCode: 200, headersSent: false, _status: null, _body: null, _ended: false,
34
+ writeHead: function (s) { res._status = s; res.statusCode = s; res.headersSent = true; return res; },
35
+ setHeader: function () {},
36
+ getHeader: function () { return undefined; },
37
+ end: function (bdy) { res._ended = true; if (bdy != null) res._body = bdy; },
38
+ json: function (o) { res._status = res._status || res.statusCode; res._body = JSON.stringify(o); res._ended = true; },
39
+ };
40
+ return res;
41
+ }
42
+
43
+ // ---- requestHost / trustedHost primitives ----
44
+
45
+ function testRequestHostPrimitive() {
46
+ var base = {
47
+ headers: { host: "real.example.com", "x-forwarded-host": "forged.evil.test" },
48
+ socket: { remoteAddress: "203.0.113.9", encrypted: false },
49
+ };
50
+
51
+ // default (no trustProxy): the forwarded header is ignored, the request's own
52
+ // Host is returned.
53
+ check("requestHost: default ignores X-Forwarded-Host, uses Host",
54
+ b.requestHelpers.requestHost(base) === "real.example.com");
55
+ check("requestHost: default with trustProxy:false ignores forwarded",
56
+ b.requestHelpers.requestHost(base, { trustProxy: false }) === "real.example.com");
57
+
58
+ // peer-gated predicate: peer NOT trusted → forged header ignored.
59
+ var notTrusted = function (addr) { return addr === "10.0.0.1"; };
60
+ check("requestHost: predicate ignores X-Forwarded-Host from an untrusted peer",
61
+ b.requestHelpers.requestHost(base, { trustProxy: notTrusted }) === "real.example.com");
62
+
63
+ // peer-gated predicate: peer trusted → forwarded header honored.
64
+ var trusted = function (addr) { return addr === "203.0.113.9"; };
65
+ check("requestHost: predicate honors X-Forwarded-Host from a trusted peer",
66
+ b.requestHelpers.requestHost(base, { trustProxy: trusted }) === "forged.evil.test");
67
+
68
+ // legacy true: honored unconditionally (spoofable — documented).
69
+ check("requestHost: legacy true honors forwarded (leftmost hop)",
70
+ b.requestHelpers.requestHost(base, { trustProxy: true }) === "forged.evil.test");
71
+
72
+ // absent host → null.
73
+ check("requestHost: returns null when no host present",
74
+ b.requestHelpers.requestHost({ headers: {} }) === null);
75
+ }
76
+
77
+ function testTrustedHostPrimitive() {
78
+ var base = {
79
+ headers: { host: "real.example.com", "x-forwarded-host": "forged.evil.test" },
80
+ socket: { remoteAddress: "203.0.113.9", encrypted: false },
81
+ };
82
+
83
+ // No options → not peer-gated, forwarded ignored.
84
+ var plain = b.requestHelpers.trustedHost();
85
+ check("trustedHost: no opts is not peerGated", plain.peerGated === false);
86
+ check("trustedHost: no opts ignores X-Forwarded-Host", plain.resolve(base) === "real.example.com");
87
+
88
+ // trustedProxies covering the peer → peer-gated, forwarded honored.
89
+ var gated = b.requestHelpers.trustedHost({ trustedProxies: ["203.0.113.0/24"] });
90
+ check("trustedHost: trustedProxies is peerGated", gated.peerGated === true);
91
+ check("trustedHost: honors X-Forwarded-Host from a trusted-proxy peer",
92
+ gated.resolve(base) === "forged.evil.test");
93
+
94
+ // trustedProxies NOT covering the peer → forged header ignored.
95
+ var gatedMiss = b.requestHelpers.trustedHost({ trustedProxies: ["10.0.0.0/8"] });
96
+ check("trustedHost: ignores X-Forwarded-Host from a non-trusted peer",
97
+ gatedMiss.resolve(base) === "real.example.com");
98
+
99
+ // hostResolver lets the operator own it.
100
+ var owned = b.requestHelpers.trustedHost({ hostResolver: function () { return "owned.example.com"; } });
101
+ check("trustedHost: hostResolver is peerGated", owned.peerGated === true);
102
+ check("trustedHost: hostResolver owns the value", owned.resolve(base) === "owned.example.com");
103
+
104
+ // bad hostResolver → TypeError at construction.
105
+ var threw = null;
106
+ try { b.requestHelpers.trustedHost({ hostResolver: 123 }); } catch (e) { threw = e; }
107
+ check("trustedHost: non-function hostResolver throws", threw instanceof TypeError);
108
+ }
109
+
110
+ // ---- dpop create()-time refusal of the spoofable legacy boolean ----
111
+
112
+ function testDpopRefusesBareTrustForwardedHeaders() {
113
+ var threw = null;
114
+ try {
115
+ b.middleware.dpop({ replayStore: _ns(), trustForwardedHeaders: true });
116
+ } catch (e) { threw = e; }
117
+ check("dpop: bare trustForwardedHeaders:true is refused at create()", threw !== null);
118
+ check("dpop: refusal names the spoofable risk + trustedProxies migration",
119
+ threw && /spoofable|trustedProxies/i.test(String(threw.message || threw)));
120
+
121
+ // WITH trustedProxies it mounts (the operator declared the trust edge).
122
+ var ok = null;
123
+ try {
124
+ ok = b.middleware.dpop({ replayStore: _ns(), trustForwardedHeaders: true, trustedProxies: ["10.0.0.0/8"] });
125
+ } catch (e) { ok = e; }
126
+ check("dpop: trustForwardedHeaders:true WITH trustedProxies mounts", typeof ok === "function");
127
+
128
+ // Default (no forwarded trust) mounts cleanly.
129
+ var def = null;
130
+ try { def = b.middleware.dpop({ replayStore: _ns() }); } catch (e) { def = e; }
131
+ check("dpop: default (no forwarded trust) mounts", typeof def === "function");
132
+
133
+ // getHtu owns the entire URI → _reconstructHtu (and the forwarded headers) is
134
+ // never consulted, so a leftover trustForwardedHeaders:true is moot and must
135
+ // NOT fail construction (the refusal only matters when htu is reconstructed).
136
+ var withGetHtu = null;
137
+ try {
138
+ withGetHtu = b.middleware.dpop({
139
+ replayStore: _ns(),
140
+ trustForwardedHeaders: true,
141
+ getHtu: function () { return "https://api.example.com/resource"; },
142
+ });
143
+ } catch (e) { withGetHtu = e; }
144
+ check("dpop: trustForwardedHeaders:true WITH getHtu mounts (reconstruction bypassed)",
145
+ typeof withGetHtu === "function");
146
+ }
147
+
148
+ // ---- end-to-end: a forged X-Forwarded-Proto/Host from a non-trusted peer
149
+ // must NOT be used to reconstruct the htu (the bound proof is rejected);
150
+ // from a trusted-proxy peer it IS honored. ----
151
+
152
+ async function _proofForHttps(key) {
153
+ return b.auth.dpop.buildProof({
154
+ htm: "POST",
155
+ htu: "https://api.example.com/resource",
156
+ privateKey: key,
157
+ algorithm: "ES256",
158
+ });
159
+ }
160
+
161
+ function _forgedReq(proof) {
162
+ return {
163
+ method: "POST",
164
+ url: "/resource",
165
+ headers: {
166
+ host: "api.example.com",
167
+ "x-forwarded-proto": "https",
168
+ "x-forwarded-host": "api.example.com",
169
+ dpop: proof,
170
+ },
171
+ socket: { remoteAddress: "203.0.113.9", encrypted: false }, // plain HTTP from a direct caller
172
+ };
173
+ }
174
+
175
+ async function testForgedForwardedNotHonored() {
176
+ var kp = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" });
177
+ var keyPem = kp.privateKey.export({ type: "pkcs8", format: "pem" });
178
+
179
+ // Peer 203.0.113.9 is NOT in trustedProxies (10.0.0.0/8) → forged
180
+ // X-Forwarded-Proto:https is ignored → htu reconstructed as http://… →
181
+ // the proof signed for https://api.example.com/resource MUST be rejected.
182
+ var mw = b.middleware.dpop({ replayStore: _ns(), trustedProxies: ["10.0.0.0/8"] });
183
+ var proof = await _proofForHttps(keyPem);
184
+ var req = _forgedReq(proof);
185
+ var res = _mkRes();
186
+ var nextCalled = false;
187
+ await mw(req, res, function () { nextCalled = true; });
188
+ check("forged XFP from untrusted peer: htu stays http → proof rejected (401)",
189
+ res._status === 401 && nextCalled === false);
190
+ check("forged XFP from untrusted peer: handler not reached", req.dpop === undefined);
191
+ }
192
+
193
+ async function testTrustedPeerForwardedHonored() {
194
+ var kp = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" });
195
+ var keyPem = kp.privateKey.export({ type: "pkcs8", format: "pem" });
196
+
197
+ // Peer 203.0.113.9 IS in trustedProxies (203.0.113.0/24) → X-Forwarded-Proto
198
+ // :https + X-Forwarded-Host honored → htu = https://api.example.com/resource
199
+ // matches the proof → it verifies and the request proceeds.
200
+ var mw = b.middleware.dpop({ replayStore: _ns(), trustedProxies: ["203.0.113.0/24"] });
201
+ var proof = await _proofForHttps(keyPem);
202
+ var req = _forgedReq(proof);
203
+ var res = _mkRes();
204
+ var nextCalled = false;
205
+ await mw(req, res, function () { nextCalled = true; });
206
+ check("trusted-proxy peer: XFP honored → htu https → proof verifies (next called)",
207
+ nextCalled === true && res._status !== 401);
208
+ check("trusted-proxy peer: req.dpop attached for downstream binding",
209
+ req.dpop && typeof req.dpop.jkt === "string");
210
+ }
211
+
212
+ async function run() {
213
+ testRequestHostPrimitive();
214
+ testTrustedHostPrimitive();
215
+ testDpopRefusesBareTrustForwardedHeaders();
216
+ await testForgedForwardedNotHonored();
217
+ await testTrustedPeerForwardedHonored();
218
+ }
219
+
220
+ module.exports = { run: run };
221
+
222
+ if (require.main === module) {
223
+ run().then(
224
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
225
+ function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
226
+ );
227
+ }
@@ -300,7 +300,90 @@ async function testEnqueueRoundTripsAvailableAt() {
300
300
  }
301
301
  }
302
302
 
303
+ // R7-4: a deps-bearing flow child must be PARKED (non-leaseable) from the
304
+ // instant it is enqueued. enqueueFlow's first pass now passes dependsOn so
305
+ // queue-local parks the child at FLOW_BLOCKED immediately, instead of leaving
306
+ // it leaseable until the second pass — a window in which a concurrent consumer
307
+ // could lease it before its dependencies have run. Driven on the real
308
+ // queue-local consumer (enqueue → lease), distinguishing jobs by returned id.
309
+ async function testFlowChildParkedAtEnqueue() {
310
+ var tmpDir = _tmp();
311
+ await setupTestDb(tmpDir);
312
+ try {
313
+ var queueLocal = require("../../lib/queue-local");
314
+ var ql = queueLocal.create(); // default store (framework db, single-node)
315
+
316
+ var childEnq = await ql.enqueue("park-q", { n: "child" },
317
+ { dependsOn: ["dep-a"], flowId: "f1", flowChildName: "child" });
318
+ var rootEnq = await ql.enqueue("park-q", { n: "root" },
319
+ { flowId: "f1", flowChildName: "root" });
320
+ var leased = await ql.lease("park-q", 30000, 10);
321
+ var leasedIds = leased.map(function (j) { return j.jobId; });
322
+ check("R7-4: a root (no-deps) child is immediately leaseable",
323
+ leasedIds.indexOf(rootEnq.jobId) !== -1);
324
+ check("R7-4: a deps-bearing child is PARKED (not leased) at enqueue time",
325
+ leasedIds.indexOf(childEnq.jobId) === -1);
326
+
327
+ // Contrast: WITHOUT dependsOn at enqueue (the pre-fix first-pass shape) the
328
+ // same child WOULD be leaseable — proving the parking is what closes the
329
+ // race, not some other gate.
330
+ var unparkedEnq = await ql.enqueue("park-q2", { n: "unparked" },
331
+ { flowId: "f2", flowChildName: "unparked" });
332
+ var leased2 = await ql.lease("park-q2", 30000, 10);
333
+ check("R7-4 contrast: a child enqueued WITHOUT dependsOn is leaseable",
334
+ leased2.map(function (j) { return j.jobId; }).indexOf(unparkedEnq.jobId) !== -1);
335
+ } finally {
336
+ await teardownTestDb(tmpDir);
337
+ }
338
+ }
339
+
340
+ // P1: if a dependency completes in the WINDOW between enqueueFlow's first
341
+ // pass (which enqueues + parks the deps-bearing child) and its second pass
342
+ // (patchFlowDeps, which resolves dependency NAMES → sibling jobIds), the
343
+ // completion has already released the child (complete() →
344
+ // _maybeReleaseFlowChildren bumps availableAt to now). patchFlowDeps must
345
+ // NOT re-park it: the dependency is done and never completes again, so a
346
+ // re-park would strand the child pending-but-unleaseable forever. Driven on
347
+ // the real queue-local consumer: enqueue (parked) → complete the dep (the
348
+ // in-window release) → patchFlowDeps → the child MUST still be leaseable.
349
+ async function testPatchFlowDepsDoesNotReparkReleasedChild() {
350
+ var tmpDir = _tmp();
351
+ await setupTestDb(tmpDir);
352
+ try {
353
+ var queueLocal = require("../../lib/queue-local");
354
+ var ql = queueLocal.create();
355
+
356
+ // First-pass shape: a no-deps dependency + a deps-bearing child parked by
357
+ // its dependsOn (the dependency NAMES the first pass writes).
358
+ var depEnq = await ql.enqueue("win-q", { n: "dep" },
359
+ { flowId: "fw", flowChildName: "dep-a" });
360
+ var childEnq = await ql.enqueue("win-q", { n: "child" },
361
+ { dependsOn: ["dep-a"], flowId: "fw", flowChildName: "child" });
362
+
363
+ // The dependency completes BEFORE the second pass — releasing the child.
364
+ var leasedDep = await ql.lease("win-q", 30000, 10);
365
+ var leasedDepIds = leasedDep.map(function (j) { return j.jobId; });
366
+ check("window: the dependency leased while the child is still parked",
367
+ leasedDepIds.indexOf(depEnq.jobId) !== -1 &&
368
+ leasedDepIds.indexOf(childEnq.jobId) === -1);
369
+ await ql.complete(depEnq.jobId); // → _maybeReleaseFlowChildren releases the child
370
+
371
+ // Second pass runs AFTER the in-window release.
372
+ await ql.patchFlowDeps(childEnq.jobId, [depEnq.jobId]);
373
+
374
+ // The child must still be leaseable. On the pre-fix tree patchFlowDeps
375
+ // re-parked availableAt at MAX_SAFE_INTEGER, stranding it forever.
376
+ var leasedChild = await ql.lease("win-q", 30000, 10);
377
+ check("P1: a child released in the enqueue window is NOT re-parked by patchFlowDeps",
378
+ leasedChild.map(function (j) { return j.jobId; }).indexOf(childEnq.jobId) !== -1);
379
+ } finally {
380
+ await teardownTestDb(tmpDir);
381
+ }
382
+ }
383
+
303
384
  async function run() {
385
+ await testFlowChildParkedAtEnqueue();
386
+ await testPatchFlowDepsDoesNotReparkReleasedChild();
304
387
  await testFlowSurface();
305
388
  await testRepeatCronReEnqueuesAfterComplete();
306
389
  await testRepeatStopsOnFinalFailure();
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * b.selfUpdate.poll — the GitHub release asset `digest` field must reach
4
+ * the returned asset object.
5
+ *
6
+ * The GitHub releases API includes `assets[].digest` (e.g.
7
+ * "sha256:<hex>") on each asset. poll() returned each matched asset as
8
+ * { name, url, size } and dropped digest, so a consumer could not do a
9
+ * defense-in-depth in-flight integrity check against the release-
10
+ * published digest. This drives the real b.selfUpdate.poll consumer
11
+ * path against a local http.Server fixture (no live GitHub) and asserts
12
+ * the returned asset / signature objects expose `digest`.
13
+ *
14
+ * Run standalone: `node test/layer-0-primitives/self-update-poll-asset-digest.test.js`
15
+ */
16
+
17
+ var http = require("http");
18
+ var helpers = require("../helpers");
19
+ var b = helpers.b;
20
+ var check = helpers.check;
21
+
22
+ function _serveJson(payload) {
23
+ return http.createServer(function (req, res) {
24
+ res.writeHead(200, { "Content-Type": "application/json" });
25
+ res.end(JSON.stringify(payload));
26
+ });
27
+ }
28
+
29
+ var ASSET_DIGEST = "sha256:1111111111111111111111111111111111111111111111111111111111111111";
30
+ var SIG_DIGEST = "sha256:2222222222222222222222222222222222222222222222222222222222222222";
31
+
32
+ async function testPollExposesAssetDigest() {
33
+ var server = _serveJson({
34
+ tag_name: "v2.0.0",
35
+ assets: [
36
+ { name: "blamejs-2.0.0.tar.gz", browser_download_url: "https://example.invalid/asset.tgz", size: 1024, digest: ASSET_DIGEST },
37
+ { name: "blamejs-2.0.0.tar.gz.sig", browser_download_url: "https://example.invalid/asset.sig", size: 64, digest: SIG_DIGEST },
38
+ ],
39
+ });
40
+ var port = await b.testing.listenOnRandomPort(server);
41
+ try {
42
+ var r = await b.selfUpdate.poll({
43
+ releasesUrl: "http://127.0.0.1:" + port + "/releases/latest",
44
+ currentVersion: "v1.0.0",
45
+ allowedProtocols: ["http:"],
46
+ allowInternal: true,
47
+ });
48
+ check("poll: available=true", r.available === true);
49
+ check("poll: asset selected", r.asset && r.asset.name === "blamejs-2.0.0.tar.gz");
50
+ check("poll: asset.digest exposed", r.asset && r.asset.digest === ASSET_DIGEST);
51
+ check("poll: signature selected", r.signature && r.signature.name === "blamejs-2.0.0.tar.gz.sig");
52
+ check("poll: signature.digest exposed", r.signature && r.signature.digest === SIG_DIGEST);
53
+ } finally { server.close(); }
54
+ }
55
+
56
+ async function testPollDigestNullWhenAbsent() {
57
+ // Upstream omits digest — the returned object must expose digest: null
58
+ // (the field is present, defense-in-depth check is skipped).
59
+ var server = _serveJson({
60
+ tag_name: "v2.0.0",
61
+ assets: [
62
+ { name: "blamejs-2.0.0.tar.gz", browser_download_url: "https://example.invalid/asset.tgz", size: 1024 },
63
+ ],
64
+ });
65
+ var port = await b.testing.listenOnRandomPort(server);
66
+ try {
67
+ var r = await b.selfUpdate.poll({
68
+ releasesUrl: "http://127.0.0.1:" + port + "/releases/latest",
69
+ currentVersion: "v1.0.0",
70
+ allowedProtocols: ["http:"],
71
+ allowInternal: true,
72
+ });
73
+ check("poll: asset selected (no digest upstream)", r.asset && r.asset.name === "blamejs-2.0.0.tar.gz");
74
+ check("poll: asset.digest is null when absent", r.asset && r.asset.digest === null);
75
+ } finally { server.close(); }
76
+ }
77
+
78
+ async function run() {
79
+ await testPollExposesAssetDigest();
80
+ await testPollDigestNullWhenAbsent();
81
+ }
82
+
83
+ module.exports = { run: run };
84
+
85
+ if (require.main === module) {
86
+ run().then(
87
+ function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
88
+ function (e) { console.error("FAIL:", e.stack || e.message); process.exit(1); }
89
+ );
90
+ }
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ /**
3
+ * b.selfUpdate.standaloneVerifier — ECDSA signature-encoding dispatch.
4
+ *
5
+ * The standalone verifier must choose the ECDSA signature encoding
6
+ * (DER ASN.1 SEQUENCE vs IEEE-P1363 raw r||s) by STRUCTURE, not by
7
+ * byte length. A P-384 DER signature whose r and s both encode short
8
+ * can total exactly 96 bytes — the same length as a raw P-384 sig — so
9
+ * a length-only heuristic mis-decodes the valid DER signature as raw
10
+ * and spuriously rejects an otherwise-valid update.
11
+ *
12
+ * RED before the fix: a forged-but-genuine 96-byte DER P-384 signature
13
+ * verifies as DER yet is rejected by the length-based dispatch.
14
+ */
15
+
16
+ var helpers = require("../helpers");
17
+ var b = helpers.b;
18
+ var check = helpers.check;
19
+
20
+ var fs = require("node:fs");
21
+ var os = require("node:os");
22
+ var path = require("node:path");
23
+ var nc = require("node:crypto");
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Minimal P-384 (secp384r1) math, BigInt-only (no deps), used solely to
27
+ // FORGE a valid signature with chosen short coordinates so its DER form is
28
+ // exactly 96 bytes. A genuine 96-byte DER P-384 signature is astronomically
29
+ // rare from random signing (coordinates are ~48 bytes), so we construct one:
30
+ // given chosen (r, s) and the message hash e, we solve for a public key Q
31
+ // such that (r, s) is a valid ECDSA signature — Q = r^-1 (s*R - e*G), where
32
+ // R is a curve point with x-coordinate r. The resulting (asset, sig, pubkey)
33
+ // triple verifies as DER under node:crypto.
34
+ // ---------------------------------------------------------------------------
35
+ var P_P = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff");
36
+ var P_A = P_P - 3n;
37
+ var P_B = BigInt("0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef");
38
+ var P_N = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973");
39
+ var P_GX = BigInt("0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7");
40
+ var P_GY = BigInt("0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f");
41
+ var P_G = [P_GX, P_GY];
42
+
43
+ function _mod(x, m) { var r = x % m; return r < 0n ? r + m : r; }
44
+ function _modpow(base, exp, m) {
45
+ base = _mod(base, m);
46
+ var r = 1n;
47
+ while (exp > 0n) { if (exp & 1n) r = _mod(r * base, m); base = _mod(base * base, m); exp >>= 1n; }
48
+ return r;
49
+ }
50
+ function _inv(x, m) { return _modpow(_mod(x, m), m - 2n, m); }
51
+ function _double(pt) {
52
+ if (pt === null) return null;
53
+ var x = pt[0], y = pt[1];
54
+ if (y === 0n) return null;
55
+ var s = _mod((3n * x * x + P_A) * _inv(2n * y, P_P), P_P);
56
+ var xr = _mod(s * s - 2n * x, P_P);
57
+ var yr = _mod(s * (x - xr) - y, P_P);
58
+ return [xr, yr];
59
+ }
60
+ function _add(pt1, pt2) {
61
+ if (pt1 === null) return pt2;
62
+ if (pt2 === null) return pt1;
63
+ var x1 = pt1[0], y1 = pt1[1], x2 = pt2[0], y2 = pt2[1];
64
+ if (x1 === x2) { if (_mod(y1 + y2, P_P) === 0n) return null; return _double(pt1); }
65
+ var s = _mod((y2 - y1) * _inv(x2 - x1, P_P), P_P);
66
+ var xr = _mod(s * s - x1 - x2, P_P);
67
+ var yr = _mod(s * (x1 - xr) - y1, P_P);
68
+ return [xr, yr];
69
+ }
70
+ function _neg(pt) { return pt === null ? null : [pt[0], _mod(-pt[1], P_P)]; }
71
+ function _scalarMul(k, pt) {
72
+ k = _mod(k, P_N);
73
+ var r = null, q = pt;
74
+ while (k > 0n) { if (k & 1n) r = _add(r, q); q = _double(q); k >>= 1n; }
75
+ return r;
76
+ }
77
+ function _liftX(xr) {
78
+ var rhs = _mod(xr * xr * xr + P_A * xr + P_B, P_P);
79
+ var y = _modpow(rhs, (P_P + 1n) / 4n, P_P); // p % 4 === 3
80
+ if (_mod(y * y, P_P) !== rhs) return null;
81
+ return [xr, y];
82
+ }
83
+ function _beToBig(buf) { var x = 0n; for (var i = 0; i < buf.length; i++) x = (x << 8n) | BigInt(buf[i]); return x; }
84
+ function _bigToFixed(x, len) {
85
+ var h = x.toString(16);
86
+ if (h.length < len * 2) h = "0".repeat(len * 2 - h.length) + h;
87
+ return Buffer.from(h, "hex");
88
+ }
89
+ function _derInt(x) {
90
+ var h = x.toString(16);
91
+ if (h.length % 2) h = "0" + h;
92
+ var bts = Buffer.from(h, "hex");
93
+ var i = 0;
94
+ while (i < bts.length - 1 && bts[i] === 0) i++;
95
+ bts = bts.subarray(i);
96
+ if (bts[0] & 0x80) bts = Buffer.concat([Buffer.from([0]), bts]);
97
+ return Buffer.concat([Buffer.from([0x02, bts.length]), bts]);
98
+ }
99
+ function _derSig(r, s) {
100
+ var body = Buffer.concat([_derInt(r), _derInt(s)]);
101
+ return Buffer.concat([Buffer.from([0x30, body.length]), body]);
102
+ }
103
+ // SPKI DER prefix for an EC P-384 public key (the fixed ASN.1 header that
104
+ // precedes the 97-byte uncompressed point 0x04 || X || Y).
105
+ var _EC_P384_SPKI_PREFIX = Buffer.from("3076301006072a8648ce3d020106052b81040022036200", "hex");
106
+ function _pubKeyFromPoint(pt) {
107
+ var point = Buffer.concat([Buffer.from([0x04]), _bigToFixed(pt[0], 48), _bigToFixed(pt[1], 48)]);
108
+ var spki = Buffer.concat([_EC_P384_SPKI_PREFIX, point]);
109
+ return nc.createPublicKey({ key: spki, format: "der", type: "spki" });
110
+ }
111
+ // Leftmost 384 bits of the SHA3-512 digest (the field ECDSA-P384 verifies on).
112
+ function _hashToE(asset) {
113
+ var d = nc.createHash("sha3-512").update(asset).digest();
114
+ return _beToBig(d) >> (8n * (BigInt(d.length) - 48n));
115
+ }
116
+
117
+ // Forge a verifying 96-byte DER P-384 signature over `asset`.
118
+ // Returns { pubPem, sig (DER, 96 bytes) }.
119
+ function _forge96ByteDerSig(asset) {
120
+ var e = _hashToE(asset);
121
+ // Find a curve point R whose x-coordinate r is exactly 45 bytes long with
122
+ // its top bit clear (so its DER INTEGER needs no sign-pad: 2 + 45 bytes).
123
+ var base = 1n << BigInt(8 * 44); // smallest 45-byte value, top byte 0x01
124
+ var r = null, R = null;
125
+ for (var d = 0n; d < 200000n; d++) {
126
+ var cand = base + d;
127
+ if (cand <= 0n || cand >= P_N) continue;
128
+ var lifted = _liftX(_mod(cand, P_P));
129
+ if (lifted) { r = cand; R = lifted; break; }
130
+ }
131
+ if (r === null) throw new Error("test setup: could not find a 45-byte r on the curve");
132
+ // s: a fixed 45-byte value (top byte 0x01, top bit clear) -> DER INTEGER 2 + 45.
133
+ var s = (1n << (8n * 44n)) + 12345n;
134
+ // Q = r^-1 (s*R - e*G)
135
+ var Q = _scalarMul(_inv(r, P_N), _add(_scalarMul(s, R), _neg(_scalarMul(e, P_G))));
136
+ var pub = _pubKeyFromPoint(Q);
137
+ var sig = _derSig(r, s);
138
+ return { pubPem: pub.export({ type: "spki", format: "pem" }), key: pub, sig: sig };
139
+ }
140
+
141
+ function _scratch(label) {
142
+ return fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-sv-enc-" + label + "-"));
143
+ }
144
+ function _write(dir, name, data) {
145
+ var p = path.join(dir, name);
146
+ fs.writeFileSync(p, data);
147
+ return p;
148
+ }
149
+
150
+ // The core RED case: a valid 96-byte DER P-384 signature must verify.
151
+ function testEcdsaP384Der96Bytes() {
152
+ var dir = _scratch("der96");
153
+ try {
154
+ var asset = Buffer.from("blamejs release artifact — 96-byte DER P-384 case");
155
+ var forged = _forge96ByteDerSig(asset);
156
+
157
+ // Sanity on the crafted artifact (the test's own preconditions).
158
+ check("setup: forged sig is exactly 96 bytes", forged.sig.length === 96);
159
+ check("setup: forged sig is a DER SEQUENCE (0x30)", forged.sig[0] === 0x30);
160
+ var sanity = nc.createVerify("sha3-512");
161
+ sanity.update(asset);
162
+ check("setup: forged sig verifies as DER under node:crypto",
163
+ sanity.verify({ key: forged.key, dsaEncoding: "der" }, forged.sig) === true);
164
+
165
+ var assetPath = _write(dir, "asset", asset);
166
+ var sigPath = _write(dir, "asset.sig", forged.sig);
167
+
168
+ var r = b.selfUpdate.standaloneVerifier.verify(assetPath, sigPath, forged.pubPem);
169
+ check("96-byte DER P-384: verify SUCCEEDS (encoding detected by structure, not length)",
170
+ r.ok === true);
171
+ check("96-byte DER P-384: alg detected", r.alg === "ecdsa-p384");
172
+ } finally {
173
+ fs.rmSync(dir, { recursive: true });
174
+ }
175
+ }
176
+
177
+ // A genuine raw IEEE-P1363 96-byte signature must still verify (no regression).
178
+ function testEcdsaP384RawStillVerifies() {
179
+ var dir = _scratch("raw96");
180
+ try {
181
+ var kp = nc.generateKeyPairSync("ec", { namedCurve: "P-384" });
182
+ var pub = kp.publicKey.export({ type: "spki", format: "pem" });
183
+ var asset = Buffer.from("blamejs raw ieee-p1363 P-384 case");
184
+ var assetPath = _write(dir, "asset", asset);
185
+ var sign = nc.createSign("sha3-512");
186
+ sign.update(asset);
187
+ var sigBytes = sign.sign({ key: kp.privateKey, dsaEncoding: "ieee-p1363" });
188
+ check("setup: raw sig is exactly 96 bytes", sigBytes.length === 96);
189
+ check("setup: raw sig is NOT a DER SEQUENCE", sigBytes[0] !== 0x30);
190
+ var sigPath = _write(dir, "asset.sig", sigBytes);
191
+ var r = b.selfUpdate.standaloneVerifier.verify(assetPath, sigPath, pub);
192
+ check("raw 96-byte IEEE-P1363 P-384: verify SUCCEEDS", r.ok === true);
193
+ check("raw 96-byte IEEE-P1363 P-384: alg detected", r.alg === "ecdsa-p384");
194
+ } finally {
195
+ fs.rmSync(dir, { recursive: true });
196
+ }
197
+ }
198
+
199
+ // A standard (>96 byte) DER signature must still verify (no regression).
200
+ function testEcdsaP384DerDefaultStillVerifies() {
201
+ var dir = _scratch("derdefault");
202
+ try {
203
+ var kp = nc.generateKeyPairSync("ec", { namedCurve: "P-384" });
204
+ var pub = kp.publicKey.export({ type: "spki", format: "pem" });
205
+ var asset = Buffer.from("blamejs default DER P-384 case");
206
+ var assetPath = _write(dir, "asset", asset);
207
+ var sign = nc.createSign("sha3-512");
208
+ sign.update(asset);
209
+ var sigBytes = sign.sign(kp.privateKey); // default DER
210
+ check("setup: default DER sig length != 96", sigBytes.length !== 96);
211
+ check("setup: default DER sig is a SEQUENCE", sigBytes[0] === 0x30);
212
+ var sigPath = _write(dir, "asset.sig", sigBytes);
213
+ var r = b.selfUpdate.standaloneVerifier.verify(assetPath, sigPath, pub);
214
+ check("default DER P-384: verify SUCCEEDS", r.ok === true);
215
+ } finally {
216
+ fs.rmSync(dir, { recursive: true });
217
+ }
218
+ }
219
+
220
+ // A forged/tampered signature is still rejected (fail-closed preserved).
221
+ function testForgedSigRejected() {
222
+ var dir = _scratch("forged");
223
+ try {
224
+ var asset = Buffer.from("blamejs release — forged-sig rejection case");
225
+ var forged = _forge96ByteDerSig(asset);
226
+ // Flip a byte inside the DER signature body (corrupt s) — must NOT verify.
227
+ var bad = Buffer.from(forged.sig);
228
+ bad[bad.length - 1] ^= 0xff;
229
+ var assetPath = _write(dir, "asset", asset);
230
+ var sigPath = _write(dir, "asset.sig", bad);
231
+ var threw = null;
232
+ try { b.selfUpdate.standaloneVerifier.verify(assetPath, sigPath, forged.pubPem); }
233
+ catch (e) { threw = e; }
234
+ check("forged 96-byte DER sig: rejected (fail-closed)",
235
+ threw && /signature INVALID/.test(threw.message));
236
+ } finally {
237
+ fs.rmSync(dir, { recursive: true });
238
+ }
239
+ }
240
+
241
+ // A 96-byte buffer that is neither a valid DER SEQUENCE nor a verifying raw
242
+ // sig is rejected (no acceptance leak from the structural dispatch).
243
+ function testGarbage96BytesRejected() {
244
+ var dir = _scratch("garbage");
245
+ try {
246
+ var kp = nc.generateKeyPairSync("ec", { namedCurve: "P-384" });
247
+ var pub = kp.publicKey.export({ type: "spki", format: "pem" });
248
+ var asset = Buffer.from("blamejs garbage-96 case");
249
+ var assetPath = _write(dir, "asset", asset);
250
+ var garbage = Buffer.alloc(96, 0x41); // not 0x30, not a real raw sig
251
+ var sigPath = _write(dir, "asset.sig", garbage);
252
+ var threw = null;
253
+ try { b.selfUpdate.standaloneVerifier.verify(assetPath, sigPath, pub); }
254
+ catch (e) { threw = e; }
255
+ check("garbage 96-byte sig: rejected (fail-closed)", threw !== null);
256
+ } finally {
257
+ fs.rmSync(dir, { recursive: true });
258
+ }
259
+ }
260
+
261
+ async function run() {
262
+ testEcdsaP384Der96Bytes();
263
+ testEcdsaP384RawStillVerifies();
264
+ testEcdsaP384DerDefaultStillVerifies();
265
+ testForgedSigRejected();
266
+ testGarbage96BytesRejected();
267
+ }
268
+
269
+ module.exports = { run: run };
270
+
271
+ if (require.main === module) {
272
+ run().then(function () { console.log("OK"); })
273
+ .catch(function (e) { console.error(e); process.exit(1); });
274
+ }