@blamejs/blamejs-shop 0.2.15 → 0.2.18

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 (52) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +2 -2
  3. package/lib/admin.js +81 -3
  4. package/lib/asset-manifest.json +5 -1
  5. package/lib/customers.js +71 -4
  6. package/lib/storefront.js +462 -0
  7. package/lib/vendor/MANIFEST.json +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +20 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  10. package/lib/vendor/blamejs/examples/wiki/docker-compose.prod.yml +29 -0
  11. package/lib/vendor/blamejs/examples/wiki/docker-compose.yml +7 -0
  12. package/lib/vendor/blamejs/lib/agent-idempotency.js +15 -3
  13. package/lib/vendor/blamejs/lib/agent-orchestrator.js +39 -0
  14. package/lib/vendor/blamejs/lib/app-shutdown.js +32 -0
  15. package/lib/vendor/blamejs/lib/bounded-map.js +102 -0
  16. package/lib/vendor/blamejs/lib/cache.js +184 -23
  17. package/lib/vendor/blamejs/lib/cert.js +68 -11
  18. package/lib/vendor/blamejs/lib/cluster-storage.js +114 -10
  19. package/lib/vendor/blamejs/lib/db.js +205 -15
  20. package/lib/vendor/blamejs/lib/dual-control.js +139 -143
  21. package/lib/vendor/blamejs/lib/i18n.js +10 -1
  22. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +43 -9
  23. package/lib/vendor/blamejs/lib/network-dns.js +10 -2
  24. package/lib/vendor/blamejs/lib/nonce-store.js +39 -12
  25. package/lib/vendor/blamejs/lib/queue-local.js +2 -2
  26. package/lib/vendor/blamejs/lib/redis-client.js +14 -1
  27. package/lib/vendor/blamejs/lib/subject.js +8 -1
  28. package/lib/vendor/blamejs/package.json +1 -1
  29. package/lib/vendor/blamejs/release-notes/v0.13.33.json +26 -0
  30. package/lib/vendor/blamejs/release-notes/v0.13.34.json +48 -0
  31. package/lib/vendor/blamejs/release-notes/v0.13.35.json +35 -0
  32. package/lib/vendor/blamejs/release-notes/v0.13.36.json +27 -0
  33. package/lib/vendor/blamejs/release-notes/v0.13.37.json +18 -0
  34. package/lib/vendor/blamejs/release-notes/v0.13.38.json +27 -0
  35. package/lib/vendor/blamejs/release-notes/v0.13.39.json +27 -0
  36. package/lib/vendor/blamejs/release-notes/v0.13.40.json +22 -0
  37. package/lib/vendor/blamejs/release-notes/v0.13.41.json +27 -0
  38. package/lib/vendor/blamejs/release-notes/v0.13.42.json +18 -0
  39. package/lib/vendor/blamejs/test/20-db.js +191 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +20 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +33 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +64 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/bounded-map.test.js +87 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +48 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +170 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/cluster-storage.test.js +125 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +92 -1
  49. package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +32 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +31 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +14 -0
  52. package/package.json +1 -1
@@ -6871,8 +6871,17 @@ var KNOWN_ANTIPATTERNS = [
6871
6871
  // shared runSql; re-routing through runInTransaction would change
6872
6872
  // semantics (passing module.exports vs database). Keep as-is.
6873
6873
  "lib/db.js",
6874
+ // clusterStorage.transaction(fn) is the cluster-aware ASYNC
6875
+ // transaction primitive (v0.13.38). dbSchema.runInTransaction is
6876
+ // synchronous (BEGIN -> fn() -> COMMIT) and cannot wrap the async fn
6877
+ // the atomic-RMW callers (cache, dual-control) need. The cluster path
6878
+ // delegates to externalDb.transaction; the single-node path issues
6879
+ // BEGIN/COMMIT/ROLLBACK around an awaited fn with shared-connection
6880
+ // serialization (no other statement may interleave). Same legitimate-
6881
+ // primitive justification as db.js.
6882
+ "lib/cluster-storage.js",
6874
6883
  ],
6875
- reason: "Extracted to dbSchema.runInTransaction. Replaces the inline BEGIN / COMMIT / ROLLBACK try/catch boilerplate in migrations / seeders / db-schema. Handles both raw better-sqlite3 and b.db framework wrapper handles via runSqlOnHandle.",
6884
+ reason: "Extracted to dbSchema.runInTransaction. Replaces the inline BEGIN / COMMIT / ROLLBACK try/catch boilerplate in migrations / seeders / db-schema. Handles both raw better-sqlite3 and b.db framework wrapper handles via runSqlOnHandle. db.js + cluster-storage.js are allowlisted transaction primitives (sync public + async cluster-aware) that can't route through the sync helper.",
6876
6885
  },
6877
6886
  {
6878
6887
  id: "inline-numeric-bounds-cascade",
@@ -9787,6 +9796,86 @@ function testWikiPortAgreesAcrossArtifacts() {
9787
9796
  bad);
9788
9797
  }
9789
9798
 
9799
+ // v0.13.34 — the wiki compose stop_grace_period MUST exceed the app
9800
+ // shutdown orchestrator's total grace budget (graceMs) plus the forced-
9801
+ // exit watchdog margin. Otherwise `docker stop` / a rolling redeploy
9802
+ // SIGKILLs the container before the DB re-encrypt phase finishes, losing
9803
+ // every write since the last periodic flush — the encrypted-DB data-loss
9804
+ // class. Cross-artifact guard so raising graceMs in lib/app-shutdown.js
9805
+ // without bumping the compose (or dropping the setting) can't silently
9806
+ // reopen the hole.
9807
+ function testWikiStopGraceExceedsShutdownBudget() {
9808
+ var bad = [];
9809
+ var shutdownSrc;
9810
+ try { shutdownSrc = fs.readFileSync("lib/app-shutdown.js", "utf8"); }
9811
+ catch (_e) { return; }
9812
+ var graceM = /DEFAULT_GRACE_MS\s*=\s*C\.TIME\.seconds\((\d+)\)/.exec(shutdownSrc);
9813
+ if (!graceM) return;
9814
+ var marginM = /FORCE_EXIT_MARGIN_MS\s*=\s*C\.TIME\.seconds\((\d+)\)/.exec(shutdownSrc);
9815
+ var graceS = parseInt(graceM[1], 10);
9816
+ var marginS = marginM ? parseInt(marginM[1], 10) : 0;
9817
+ var minGrace = graceS + marginS;
9818
+ var composeFiles = ["examples/wiki/docker-compose.yml", "examples/wiki/docker-compose.prod.yml"];
9819
+ for (var i = 0; i < composeFiles.length; i += 1) {
9820
+ var cf = composeFiles[i];
9821
+ var text;
9822
+ try { text = fs.readFileSync(cf, "utf8"); }
9823
+ catch (_e) { continue; }
9824
+ var m = /stop_grace_period:\s*'?(\d+)\s*s'?/.exec(text);
9825
+ if (!m) {
9826
+ bad.push({ file: cf, line: 1,
9827
+ content: cf + " declares no stop_grace_period — Docker's 10s default SIGKILLs before " +
9828
+ "the " + graceS + "s shutdown budget finishes the DB re-encrypt. Set " +
9829
+ "stop_grace_period to at least " + minGrace + "s." });
9830
+ continue;
9831
+ }
9832
+ var graceSet = parseInt(m[1], 10);
9833
+ if (graceSet < minGrace) {
9834
+ bad.push({ file: cf, line: 1,
9835
+ content: cf + " stop_grace_period is " + graceSet + "s but the shutdown budget (graceMs " +
9836
+ graceS + "s + watchdog margin " + marginS + "s) needs at least " + minGrace +
9837
+ "s, or the DB re-encrypt is SIGKILLed mid-flush." });
9838
+ }
9839
+ }
9840
+ bad = _filterMarkers(bad, "wiki-stop-grace-below-shutdown-budget");
9841
+ _report("wiki compose stop_grace_period exceeds the app shutdown grace budget " +
9842
+ "(v0.13.34 — no SIGKILL-before-DB-re-encrypt data loss on docker stop / redeploy)",
9843
+ bad);
9844
+ }
9845
+
9846
+ // v0.13.41 — the agent-orchestrator registry-read paths (_list / _lookup)
9847
+ // MUST consult the tenant gate (_tenantAllows) so an actor can't enumerate
9848
+ // or acquire a handle to another tenant's agent when tenant scoping is on.
9849
+ // agent-event-bus enforces this on subscribe/delivery; the orchestrator now
9850
+ // mirrors it. Encoded so a refactor can't silently drop the gate from
9851
+ // either read path.
9852
+ function testOrchestratorRegistryReadsTenantScoped() {
9853
+ var bad = [];
9854
+ var src;
9855
+ try { src = fs.readFileSync("lib/agent-orchestrator.js", "utf8"); }
9856
+ catch (_e) { return; }
9857
+ ["_list", "_lookup"].forEach(function (fn) {
9858
+ var start = src.indexOf("function " + fn + "(");
9859
+ if (start === -1) {
9860
+ bad.push({ file: "lib/agent-orchestrator.js", line: 1,
9861
+ content: fn + " not found — tenant-scope detector can't verify it" });
9862
+ return;
9863
+ }
9864
+ // Body = from this function to the next top-level function declaration.
9865
+ var rest = src.slice(start + 1);
9866
+ var next = rest.search(/\nasync function |\nfunction /);
9867
+ var body = next === -1 ? rest : rest.slice(0, next);
9868
+ if (body.indexOf("_tenantAllows") === -1) {
9869
+ bad.push({ file: "lib/agent-orchestrator.js", line: 1,
9870
+ content: fn + " does not consult _tenantAllows — registry reads must be tenant-scoped " +
9871
+ "(cross-tenant enumeration / handle acquisition leak)" });
9872
+ }
9873
+ });
9874
+ bad = _filterMarkers(bad, "orchestrator-registry-tenant-scope");
9875
+ _report("agent-orchestrator _list/_lookup consult the tenant gate " +
9876
+ "(v0.13.41 — no cross-tenant registry enumeration / handle acquisition)", bad);
9877
+ }
9878
+
9790
9879
  // v0.13.19 — a CI job that runs the long test suites (smoke / wiki
9791
9880
  // e2e) MUST declare `timeout-minutes`. Without it a hung child (a
9792
9881
  // leaked timer / socket / fs.watch handle — the macOS smoke-hang
@@ -10372,6 +10461,8 @@ async function run() {
10372
10461
  // WIKI_PORT default must match the release-container.yml smoke
10373
10462
  // step's port mapping + curl host.
10374
10463
  testWikiPortAgreesAcrossArtifacts();
10464
+ testWikiStopGraceExceedsShutdownBudget();
10465
+ testOrchestratorRegistryReadsTenantScoped();
10375
10466
  // v0.13.19 CI hang backstop: every workflow job that runs the test
10376
10467
  // suite must declare timeout-minutes so a hung child can't ride
10377
10468
  // GitHub's 6-hour default.
@@ -83,6 +83,38 @@ async function run() {
83
83
  });
84
84
  check("approve: revoked grant rejected", revokedApprove.error === "grant-revoked");
85
85
 
86
+ // ---- Concurrency: the quorum-bypass + double-consume races ----
87
+ // The get/set version read a snapshot, mutated, and wrote back with an
88
+ // await in between, so two concurrent operations could each act on stale
89
+ // state. cache.update makes the read-modify-write atomic. These pin the
90
+ // invariants that broke before.
91
+ var reqC = await approvals.request({ action: "<test>.concurrent", requestedBy: { id: "alice" } });
92
+
93
+ // (a) The SAME approver firing two approvals in parallel must count ONCE.
94
+ // A stale-snapshot double-append would reach the 2-of-2 quorum with one
95
+ // human — the dual-control bypass.
96
+ var dupResults = await Promise.all([
97
+ approvals.approve({ grantId: reqC.grantId, approver: { id: "bob" } }),
98
+ approvals.approve({ grantId: reqC.grantId, approver: { id: "bob" } }),
99
+ ]);
100
+ var stC = await approvals.status(reqC.grantId);
101
+ check("concurrent same-approver: counted once (no quorum bypass)",
102
+ stC.approvedBy.length === 1 && stC.status === "pending");
103
+ var okN = dupResults.filter(function (r) { return !r.error; }).length;
104
+ var dupN = dupResults.filter(function (r) { return r.error === "already-approved-by-this-actor"; }).length;
105
+ check("concurrent same-approver: exactly one succeeds, one rejected duplicate",
106
+ okN === 1 && dupN === 1);
107
+
108
+ // (b) Two parallel consumes of a quorum-reached grant: exactly ONE wins
109
+ // ready:true — never two (single-use).
110
+ await approvals.approve({ grantId: reqC.grantId, approver: { id: "carol" } }); // 2/2 → approved
111
+ var consumeResults = await Promise.all([
112
+ approvals.consume(reqC.grantId),
113
+ approvals.consume(reqC.grantId),
114
+ ]);
115
+ var readyN = consumeResults.filter(function (r) { return r.ready === true; }).length;
116
+ check("concurrent consume: exactly one ready (no double-consume)", readyN === 1);
117
+
86
118
  // ---- Validation ----
87
119
  var threwBadCache = null;
88
120
  try { b.dualControl.create({ namespace: "x", cache: {} }); }
@@ -269,8 +269,39 @@ function testSmimeDocBlockCitations() {
269
269
 
270
270
  // ---- Run ----
271
271
 
272
+ // ---- trust-chain leaf is bound to the verified signer key ----
273
+ //
274
+ // _verifyTrustChain must select the chain leaf as the cert whose public
275
+ // key matches signerPublicKey (the key that actually verified the
276
+ // signature) — not chain[0] unconditionally. Otherwise a validly-chained
277
+ // cert for a DIFFERENT identity passes chain validation. _certKeyMatches
278
+ // is that binding decision; a mock { publicKey } is all it touches.
279
+ function testSmimeCertKeyBinding() {
280
+ var kpA = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
281
+ var kpB = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
282
+ var certA = { publicKey: kpA.publicKey };
283
+ var spkiA = Buffer.from(kpA.publicKey.export({ format: "der", type: "spki" }));
284
+ check("smime binding: cert matches its own full SPKI key",
285
+ smime._certKeyMatches(certA, spkiA) === true);
286
+ // Production form: signerPublicKey is the raw subjectPublicKey (the
287
+ // uncompressed EC point 0x04||x||y), which is the SPKI's suffix.
288
+ var jwkA = kpA.publicKey.export({ format: "jwk" });
289
+ var rawA = Buffer.concat([Buffer.from([0x04]),
290
+ Buffer.from(jwkA.x, "base64url"), Buffer.from(jwkA.y, "base64url")]);
291
+ check("smime binding: cert matches its raw subjectPublicKey (suffix)",
292
+ smime._certKeyMatches(certA, rawA) === true);
293
+ // The gap this closes: a DIFFERENT key must not match.
294
+ var spkiB = Buffer.from(kpB.publicKey.export({ format: "der", type: "spki" }));
295
+ check("smime binding: cert does NOT match a different cert's key",
296
+ smime._certKeyMatches(certA, spkiB) === false);
297
+ // Unextractable / absent key → no match (caller fails closed).
298
+ check("smime binding: unextractable key → no match",
299
+ smime._certKeyMatches({ publicKey: { export: function () { throw new Error("nope"); } } }, spkiA) === false);
300
+ }
301
+
272
302
  function run() {
273
303
  testSmimeSurface();
304
+ testSmimeCertKeyBinding();
274
305
  testSmimeSignVerifyRoundtrip();
275
306
  testSmimeVerifyTamperRefused();
276
307
  testSmimeVerifyWrongKeyRefused();
@@ -118,6 +118,20 @@ async function run() {
118
118
  var efv = redis._frameToValue(redis._parseFrame(_bytes("-ERR boom\r\n"), 0));
119
119
  check("frameToValue: error becomes _redisError marker",
120
120
  efv && efv._redisError === true && efv.message === "ERR boom");
121
+
122
+ // ---- close()/reconnect leak guard (v0.13.40) ----
123
+ // After close(), a reconnect timer scheduled during backoff must be
124
+ // cancelled and _connect() must refuse to re-open — otherwise a post-
125
+ // close reconnect leaks a fresh socket (and the un-unref'd backoff timer
126
+ // would hold the event loop alive). We test the closing guard directly:
127
+ // connect() after close() is a no-op, so no socket is opened. (The timer
128
+ // is also tracked + unref'd + cleared in close(), mirroring ws-client.)
129
+ var client = redis.create({ url: "redis://127.0.0.1:1/0" }); // port 1 — nothing listens
130
+ await client.close();
131
+ check("redis: closing flag set after close()", client._state().closing === true);
132
+ try { await client.connect(); } catch (_e) { /* closing guard should prevent any connect attempt */ }
133
+ check("redis: connect() after close is a no-op (closing guard — no socket opened)",
134
+ client._state().connected === false);
121
135
  }
122
136
 
123
137
  module.exports = { run: run };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.2.15",
3
+ "version": "0.2.18",
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": {