@blamejs/blamejs-shop 0.3.40 → 0.3.41

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 +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/storefront.js +14 -3
  4. package/lib/vendor/MANIFEST.json +2 -2
  5. package/lib/vendor/blamejs/CHANGELOG.md +8 -0
  6. package/lib/vendor/blamejs/README.md +8 -5
  7. package/lib/vendor/blamejs/SECURITY.md +7 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +266 -2
  9. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  10. package/lib/vendor/blamejs/index.js +7 -1
  11. package/lib/vendor/blamejs/lib/agent-idempotency.js +113 -0
  12. package/lib/vendor/blamejs/lib/agent-orchestrator.js +108 -0
  13. package/lib/vendor/blamejs/lib/agent-snapshot.js +137 -0
  14. package/lib/vendor/blamejs/lib/agent-tenant.js +193 -17
  15. package/lib/vendor/blamejs/lib/ai-input.js +167 -3
  16. package/lib/vendor/blamejs/lib/ai-output.js +463 -0
  17. package/lib/vendor/blamejs/lib/ai-prompt.js +304 -0
  18. package/lib/vendor/blamejs/lib/archive-wrap.js +234 -1
  19. package/lib/vendor/blamejs/lib/archive.js +1 -0
  20. package/lib/vendor/blamejs/lib/audit.js +3 -0
  21. package/lib/vendor/blamejs/lib/auth/oid4vp.js +47 -28
  22. package/lib/vendor/blamejs/lib/cluster.js +186 -14
  23. package/lib/vendor/blamejs/lib/codepoint-class.js +18 -0
  24. package/lib/vendor/blamejs/lib/compliance-ai-act.js +446 -0
  25. package/lib/vendor/blamejs/lib/consent.js +104 -8
  26. package/lib/vendor/blamejs/lib/content-credentials.js +851 -41
  27. package/lib/vendor/blamejs/lib/crypto-field.js +5 -0
  28. package/lib/vendor/blamejs/lib/db.js +15 -0
  29. package/lib/vendor/blamejs/lib/framework-error.js +21 -0
  30. package/lib/vendor/blamejs/lib/mail-srs.js +122 -19
  31. package/lib/vendor/blamejs/lib/privacy.js +168 -0
  32. package/lib/vendor/blamejs/lib/safe-archive.js +196 -136
  33. package/lib/vendor/blamejs/lib/validate-opts.js +24 -0
  34. package/lib/vendor/blamejs/lib/vault/rotate.js +175 -15
  35. package/lib/vendor/blamejs/lib/vault-aad.js +84 -33
  36. package/lib/vendor/blamejs/package.json +1 -1
  37. package/lib/vendor/blamejs/release-notes/v0.14.11.json +72 -0
  38. package/lib/vendor/blamejs/release-notes/v0.14.12.json +95 -0
  39. package/lib/vendor/blamejs/release-notes/v0.14.13.json +52 -0
  40. package/lib/vendor/blamejs/release-notes/v0.14.14.json +31 -0
  41. package/lib/vendor/blamejs/test/00-primitives.js +9 -1
  42. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +103 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +91 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +186 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +140 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +59 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +125 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +133 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +6 -1
  50. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +94 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +176 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +243 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +130 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +70 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +289 -2
  57. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +22 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +61 -0
  59. package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +69 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +158 -0
  61. package/package.json +1 -1
@@ -134,6 +134,179 @@ async function testTenantStrategyRoundTrip() {
134
134
  }
135
135
  }
136
136
 
137
+ async function testRewrapTenantRotationRoundTrip() {
138
+ // A recipient: "tenant" blob is keyed off the vault root. Rotating
139
+ // the vault keypair changes the root, so the old envelope no longer
140
+ // opens — the operator must re-wrap each stored blob old-root ->
141
+ // new-root via b.archive.rewrapTenant before retiring the old keypair.
142
+ var oldDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aw-rewrap-old-"));
143
+ var newDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aw-rewrap-new-"));
144
+ try {
145
+ await helpers.setupVaultOnly(oldDir);
146
+ var oldRoot = b.vault.getKeysJson();
147
+ var src = Buffer.from("per-tenant sealed archive bytes ".repeat(40));
148
+ var sealedOld = b.archive.wrap(src, { recipient: "tenant", tenantId: "alpha" });
149
+ check("rewrapTenant: pre-rotation blob opens under the old root",
150
+ b.archive.unwrap(sealedOld, { tenantId: "alpha" }).equals(src));
151
+
152
+ // Simulate a vault rotation: a fresh keypair (new dir) is now live.
153
+ await helpers.setupVaultOnly(newDir);
154
+ var newRoot = b.vault.getKeysJson();
155
+ check("rewrapTenant: rotation produced a distinct vault root", oldRoot !== newRoot);
156
+
157
+ // The old blob is now stranded under the live (new) root — this is
158
+ // the data-loss class the primitive defends.
159
+ var stranded = null;
160
+ try { b.archive.unwrap(sealedOld, { tenantId: "alpha" }); } catch (e) { stranded = e; }
161
+ check("rewrapTenant: old blob no longer opens under the new live root",
162
+ stranded && /decrypt-failed/.test(stranded.code || stranded.message));
163
+
164
+ var rewrapped = b.archive.rewrapTenant({
165
+ blob: sealedOld,
166
+ oldRootJson: oldRoot,
167
+ newRootJson: newRoot,
168
+ tenantId: "alpha",
169
+ });
170
+ check("rewrapTenant: output carries BAWRP magic",
171
+ rewrapped.slice(0, 5).toString("ascii") === "BAWRP");
172
+ check("rewrapTenant: output keeps the tenant version byte 0x02",
173
+ rewrapped[5] === 0x02);
174
+ check("rewrapTenant: re-wrapped blob opens under the new live root via standard unwrap",
175
+ b.archive.unwrap(rewrapped, { tenantId: "alpha" }).equals(src));
176
+
177
+ // Cross-tenant isolation survives the re-wrap: another tenant's key
178
+ // (and AAD) cannot open the re-wrapped envelope.
179
+ var crossErr = null;
180
+ try { b.archive.unwrap(rewrapped, { tenantId: "beta" }); } catch (e) { crossErr = e; }
181
+ check("rewrapTenant: re-wrapped blob refuses another tenant",
182
+ crossErr && /decrypt-failed/.test(crossErr.code || crossErr.message));
183
+ } finally {
184
+ helpers.teardownVaultOnly(oldDir);
185
+ helpers.teardownVaultOnly(newDir);
186
+ }
187
+ }
188
+
189
+ async function testRewrapTenantRefusals() {
190
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aw-rewrap-refuse-"));
191
+ try {
192
+ await helpers.setupVaultOnly(tmpDir);
193
+ var root = b.vault.getKeysJson();
194
+ var src = Buffer.from("tenant bytes");
195
+
196
+ // Non-tenant envelope (a key-pair recipient, version 0x01) must be
197
+ // refused — rewrapTenant only handles root-keyed tenant blobs.
198
+ var pair = b.crypto.generateEncryptionKeyPair();
199
+ var recipientBlob = b.archive.wrap(src, { recipient: pair });
200
+ var notTenantErr = null;
201
+ try {
202
+ b.archive.rewrapTenant({ blob: recipientBlob, oldRootJson: root, newRootJson: root, tenantId: "alpha" });
203
+ } catch (e) { notTenantErr = e; }
204
+ check("rewrapTenant: refuses a recipient (key-pair) envelope",
205
+ notTenantErr && /not-tenant-envelope/.test(notTenantErr.code || notTenantErr.message));
206
+
207
+ // A passphrase envelope (different magic) is refused with bad-magic.
208
+ var passBlob = await b.archive.wrapWithPassphrase(src, {
209
+ passphrase: "operator-supplied-long-passphrase-2026",
210
+ minEntropyBits: 0,
211
+ });
212
+ var badMagicErr = null;
213
+ try {
214
+ b.archive.rewrapTenant({ blob: passBlob, oldRootJson: root, newRootJson: root, tenantId: "alpha" });
215
+ } catch (e) { badMagicErr = e; }
216
+ check("rewrapTenant: refuses a passphrase (BAWPP) envelope with bad-magic",
217
+ badMagicErr && /bad-magic/.test(badMagicErr.code || badMagicErr.message));
218
+
219
+ // Wrong old root → the blob does not open → decrypt-failed.
220
+ var sealed = b.archive.wrap(src, { recipient: "tenant", tenantId: "alpha" });
221
+ var wrongRootJson = b.vault.getKeysJson().replace(/[A-Za-z]/, function (c) {
222
+ return c === "A" ? "B" : "A";
223
+ });
224
+ var wrongRootErr = null;
225
+ try {
226
+ b.archive.rewrapTenant({ blob: sealed, oldRootJson: wrongRootJson, newRootJson: root, tenantId: "alpha" });
227
+ } catch (e) { wrongRootErr = e; }
228
+ check("rewrapTenant: wrong old root refused with decrypt-failed",
229
+ wrongRootErr && /decrypt-failed/.test(wrongRootErr.code || wrongRootErr.message));
230
+
231
+ // Missing roots / tenantId throw config errors.
232
+ var noRootErr = null;
233
+ try {
234
+ b.archive.rewrapTenant({ blob: sealed, newRootJson: root, tenantId: "alpha" });
235
+ } catch (e) { noRootErr = e; }
236
+ check("rewrapTenant: missing oldRootJson throws bad-root",
237
+ noRootErr && /bad-root/.test(noRootErr.code || noRootErr.message));
238
+
239
+ var noIdErr = null;
240
+ try {
241
+ b.archive.rewrapTenant({ blob: sealed, oldRootJson: root, newRootJson: root });
242
+ } catch (e) { noIdErr = e; }
243
+ check("rewrapTenant: missing tenantId throws no-tenant-id",
244
+ noIdErr && /no-tenant-id/.test(noIdErr.code || noIdErr.message));
245
+
246
+ var badBlobErr = null;
247
+ try {
248
+ b.archive.rewrapTenant({ blob: "not a buffer", oldRootJson: root, newRootJson: root, tenantId: "alpha" });
249
+ } catch (e) { badBlobErr = e; }
250
+ check("rewrapTenant: non-Buffer blob throws bad-input",
251
+ badBlobErr && /bad-input/.test(badBlobErr.code || badBlobErr.message));
252
+ } finally {
253
+ helpers.teardownVaultOnly(tmpDir);
254
+ }
255
+ }
256
+
257
+ async function testAadRotationDescriptor() {
258
+ var aw = require("../../lib/archive-wrap");
259
+ var desc = aw.AAD_ROTATION;
260
+ check("AAD_ROTATION: descriptor present", desc && typeof desc === "object");
261
+ check("AAD_ROTATION: backend is external (operator-placed blobs)", desc.backend === "external");
262
+ check("AAD_ROTATION: rowIdField + schemaVersion + table declared",
263
+ desc.table === "archive-wrap:tenant-blobs" && desc.rowIdField === "id" && desc.schemaVersion === "1");
264
+ check("AAD_ROTATION: reseal is a function", typeof desc.reseal === "function");
265
+
266
+ var oldDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aw-reseal-old-"));
267
+ var newDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aw-reseal-new-"));
268
+ try {
269
+ await helpers.setupVaultOnly(oldDir);
270
+ var oldRoot = b.vault.getKeysJson();
271
+ var srcA = Buffer.from("alpha archive ".repeat(20));
272
+ var srcB = Buffer.from("beta archive ".repeat(20));
273
+ var blobs = {
274
+ a1: b.archive.wrap(srcA, { recipient: "tenant", tenantId: "alpha" }),
275
+ b1: b.archive.wrap(srcB, { recipient: "tenant", tenantId: "beta" }),
276
+ };
277
+
278
+ await helpers.setupVaultOnly(newDir);
279
+ var newRoot = b.vault.getKeysJson();
280
+
281
+ // Operator-supplied backing store: list() enumerates the blobs the
282
+ // operator placed; put() writes the re-wrapped bytes back.
283
+ var store = {
284
+ list: function () {
285
+ return [
286
+ { id: "a1", blob: blobs.a1, tenantId: "alpha" },
287
+ { id: "b1", blob: blobs.b1, tenantId: "beta" },
288
+ ];
289
+ },
290
+ put: function (id, blob) { blobs[id] = blob; },
291
+ };
292
+ var result = desc.reseal({ store: store, oldRootJson: oldRoot, newRootJson: newRoot });
293
+ check("AAD_ROTATION.reseal: re-sealed every store entry", result.resealed === 2);
294
+ check("AAD_ROTATION.reseal: alpha blob opens under the new root",
295
+ b.archive.unwrap(blobs.a1, { tenantId: "alpha" }).equals(srcA));
296
+ check("AAD_ROTATION.reseal: beta blob opens under the new root",
297
+ b.archive.unwrap(blobs.b1, { tenantId: "beta" }).equals(srcB));
298
+
299
+ var badStoreErr = null;
300
+ try { desc.reseal({ store: {}, oldRootJson: oldRoot, newRootJson: newRoot }); }
301
+ catch (e) { badStoreErr = e; }
302
+ check("AAD_ROTATION.reseal: store without list/put refused",
303
+ badStoreErr && /bad-store/.test(badStoreErr.code || badStoreErr.message));
304
+ } finally {
305
+ helpers.teardownVaultOnly(oldDir);
306
+ helpers.teardownVaultOnly(newDir);
307
+ }
308
+ }
309
+
137
310
  async function testBackupRecipientRoundTrip() {
138
311
  var pair = b.crypto.generateEncryptionKeyPair();
139
312
  var src = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-wrap-src-"));
@@ -203,6 +376,9 @@ async function run() {
203
376
  await testWrapRefusesPartialStaticRecipient();
204
377
  await testWrapRequiresRecipient();
205
378
  await testTenantStrategyRoundTrip();
379
+ await testRewrapTenantRotationRoundTrip();
380
+ await testRewrapTenantRefusals();
381
+ await testAadRotationDescriptor();
206
382
  await testBackupRecipientRoundTrip();
207
383
  await testBackupRecipientStrategyRequiresKeys();
208
384
  await testBackupRecipientDirectoryRefused();
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * cluster vault-key rotation acceptance.
4
+ *
5
+ * After a vault-key rotation (b.vault.rotate) the SHA3-512 fingerprint of
6
+ * the vault PUBLIC keys changes on every node, but the rotation only
7
+ * re-seals the local dataDir — the external coordination row
8
+ * (_blamejs_cluster_state.vaultKeyFp) keeps the OLD fingerprint. Without
9
+ * a way to declare the change legitimate, cluster.init's
10
+ * _checkVaultKeyConsistency throws VAULT_KEY_DRIFT (FATAL) and every node
11
+ * refuses boot.
12
+ *
13
+ * acceptVaultKeyRotation: true is the operator's signed-off declaration
14
+ * "the fingerprint changed via rotation": the booting node advances the
15
+ * canonical row to its own fingerprint and bumps a monotonic
16
+ * rotationEpoch. expectedVaultKeyFp narrows the adoption to a single
17
+ * blessed fingerprint. The strict cross-node drift refusal stays in force
18
+ * whenever the rotation is NOT declared.
19
+ *
20
+ * These tests model "canonical is stale after a rotation" by booting once
21
+ * (which records the LIVE fingerprint), then overwriting the row with a
22
+ * different fingerprint — the same end-state a rotation produces, without
23
+ * hardcoding the fingerprint domain-separation literal here (the cluster
24
+ * module owns that; we read the live value back instead).
25
+ *
26
+ * Run standalone: `node test/layer-0-primitives/cluster-vault-rotation.test.js`
27
+ * Or via smoke: `node test/smoke.js`
28
+ */
29
+
30
+ var fs = require("node:fs");
31
+ var os = require("node:os");
32
+ var path = require("node:path");
33
+ var helpers = require("../helpers");
34
+ var b = helpers.b;
35
+ var check = helpers.check;
36
+ var setupTestDb = helpers.setupTestDb;
37
+ var teardownTestDb = helpers.teardownTestDb;
38
+ var _makeSqliteDriver = helpers._makeSqliteDriver;
39
+
40
+ var SECONDS = b.constants.TIME.seconds;
41
+ var BOGUS_FP = "abad1dea".repeat(16); // 128-char lowercase hex, ≠ any live key
42
+
43
+ function _initOpts(nodeId, extra) {
44
+ return Object.assign({
45
+ nodeId: nodeId,
46
+ externalDbBackend: "ops",
47
+ dialect: "sqlite",
48
+ leaseTtl: SECONDS(30),
49
+ heartbeatInterval: SECONDS(10),
50
+ }, extra || {});
51
+ }
52
+
53
+ async function _setup() {
54
+ b.cluster._resetForTest();
55
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-vk-rotate-"));
56
+ var dbPath = path.join(tmpDir, "ext.db");
57
+ var driver = _makeSqliteDriver(dbPath);
58
+ await setupTestDb(tmpDir); // initializes the vault
59
+ b.externalDb.init({
60
+ backends: { "ops": { connect: driver.connect, query: driver.query, close: driver.close } },
61
+ });
62
+ await b.frameworkSchema.ensureSchema({ externalDbBackend: "ops", dialect: "sqlite" });
63
+ return {
64
+ tmpDir: tmpDir,
65
+ driver: driver,
66
+ teardown: async function () {
67
+ try { await b.cluster.shutdown(); } catch (_e) {}
68
+ try { await b.externalDb.shutdown(); } catch (_e) {}
69
+ driver._close();
70
+ await teardownTestDb(tmpDir);
71
+ },
72
+ };
73
+ }
74
+
75
+ async function _readState() {
76
+ var r = await b.externalDb.query(
77
+ "SELECT vaultKeyFp, recordedByNode, rotationEpoch FROM _blamejs_cluster_state WHERE scope='state'",
78
+ [], { backend: "ops" }
79
+ );
80
+ return (r.rows && r.rows[0]) || null;
81
+ }
82
+
83
+ // Record the LIVE fingerprint (first boot), then stamp the row with a
84
+ // stale fingerprint — the post-rotation canonical state.
85
+ async function _recordLiveThenStale() {
86
+ await b.cluster.init(_initOpts("vk-seed"));
87
+ var live = (await _readState()).vaultKeyFp;
88
+ // shutdown() releases the DB lease so the next init can acquire; it
89
+ // also sets the terminated flag (post-graceful-exit, isLeader stays
90
+ // false). _resetForTest clears that flag so the SUBSEQUENT init boots
91
+ // as a fresh process would, with isLeader reflecting the new lease.
92
+ await b.cluster.shutdown();
93
+ b.cluster._resetForTest();
94
+ await b.externalDb.query(
95
+ "UPDATE _blamejs_cluster_state SET vaultKeyFp = ?, recordedByNode = 'old-node', rotationEpoch = 7 WHERE scope='state'",
96
+ [BOGUS_FP], { backend: "ops" }
97
+ );
98
+ return live;
99
+ }
100
+
101
+ function testSurface() {
102
+ check("cluster namespace", typeof b.cluster === "object");
103
+ check("cluster.init fn", typeof b.cluster.init === "function");
104
+ }
105
+
106
+ async function testRotationAccepted() {
107
+ var ctx = await _setup();
108
+ try {
109
+ var liveFp = await _recordLiveThenStale();
110
+ check("seeded fingerprint is stale (≠ live)", liveFp !== BOGUS_FP);
111
+
112
+ var threw = null;
113
+ try { await b.cluster.init(_initOpts("vk-rotated", { acceptVaultKeyRotation: true })); }
114
+ catch (e) { threw = e; }
115
+ check("rotation-accept boots without VAULT_KEY_DRIFT", threw === null);
116
+ check("rotated node holds the lease", b.cluster.isLeader() === true);
117
+
118
+ var post = await _readState();
119
+ check("canonical fingerprint advanced to live key", post.vaultKeyFp === liveFp);
120
+ check("canonical recorded by the rotated node", post.recordedByNode === "vk-rotated");
121
+ check("rotation epoch bumped past the stale 7", Number(post.rotationEpoch) === 8);
122
+ } finally { await ctx.teardown(); }
123
+ }
124
+
125
+ async function testNoDeclarationStillRefuses() {
126
+ // The cross-node drift case is untouched: an undeclared mismatch is
127
+ // still FATAL so genuine drift can't silently seal unreadable columns.
128
+ var ctx = await _setup();
129
+ try {
130
+ await _recordLiveThenStale();
131
+ var threw = null;
132
+ try { await b.cluster.init(_initOpts("vk-undeclared")); }
133
+ catch (e) { threw = e; }
134
+ check("undeclared mismatch throws", threw !== null);
135
+ check("undeclared mismatch is VAULT_KEY_DRIFT", threw && threw.code === "VAULT_KEY_DRIFT");
136
+ var post = await _readState();
137
+ check("undeclared mismatch left the canonical row untouched", post.vaultKeyFp === BOGUS_FP);
138
+ } finally { await ctx.teardown(); }
139
+ }
140
+
141
+ async function testExpectedFpMatchAdopts() {
142
+ var ctx = await _setup();
143
+ try {
144
+ var liveFp = await _recordLiveThenStale();
145
+ var threw = null;
146
+ try {
147
+ await b.cluster.init(_initOpts("vk-blessed", {
148
+ acceptVaultKeyRotation: true,
149
+ expectedVaultKeyFp: liveFp,
150
+ }));
151
+ } catch (e) { threw = e; }
152
+ check("blessed-fingerprint rotation adopts cleanly", threw === null);
153
+ var post = await _readState();
154
+ check("blessed-fingerprint canonical advanced", post.vaultKeyFp === liveFp);
155
+ } finally { await ctx.teardown(); }
156
+ }
157
+
158
+ async function testExpectedFpMismatchRefuses() {
159
+ // acceptVaultKeyRotation is set, but the blessed fingerprint is NOT the
160
+ // key this node actually holds — a stale / wrong key file. Refuse.
161
+ var ctx = await _setup();
162
+ try {
163
+ await _recordLiveThenStale();
164
+ var threw = null;
165
+ try {
166
+ await b.cluster.init(_initOpts("vk-wrongkey", {
167
+ acceptVaultKeyRotation: true,
168
+ expectedVaultKeyFp: "feedface".repeat(16), // ≠ live, ≠ stale
169
+ }));
170
+ } catch (e) { threw = e; }
171
+ check("blessed-but-wrong key throws", threw !== null);
172
+ check("blessed-but-wrong key is VAULT_KEY_ROTATION_MISMATCH",
173
+ threw && threw.code === "VAULT_KEY_ROTATION_MISMATCH");
174
+ var post = await _readState();
175
+ check("blessed-but-wrong key left the canonical row untouched", post.vaultKeyFp === BOGUS_FP);
176
+ } finally { await ctx.teardown(); }
177
+ }
178
+
179
+ async function testSameKeyReInitIsClean() {
180
+ // Once adopted, a fresh init with the SAME (now-canonical) key passes
181
+ // the consistency check without declaring a rotation and without
182
+ // bumping the epoch.
183
+ var ctx = await _setup();
184
+ try {
185
+ var liveFp = await _recordLiveThenStale();
186
+ await b.cluster.init(_initOpts("vk-adopt", { acceptVaultKeyRotation: true }));
187
+ var epochAfterAdopt = Number((await _readState()).rotationEpoch);
188
+ await b.cluster.shutdown();
189
+
190
+ var threw = null;
191
+ try { await b.cluster.init(_initOpts("vk-rejoin")); }
192
+ catch (e) { threw = e; }
193
+ check("re-init on the adopted key needs no rotation declaration", threw === null);
194
+ var post = await _readState();
195
+ check("re-init left fingerprint at the live key", post.vaultKeyFp === liveFp);
196
+ check("re-init did NOT bump the epoch", Number(post.rotationEpoch) === epochAfterAdopt);
197
+ } finally { await ctx.teardown(); }
198
+ }
199
+
200
+ async function testConfigValidation() {
201
+ var ctx = await _setup();
202
+ try {
203
+ var t1 = null;
204
+ try { await b.cluster.init(_initOpts("vk-cfg1", { acceptVaultKeyRotation: "yes" })); }
205
+ catch (e) { t1 = e; }
206
+ check("non-boolean acceptVaultKeyRotation throws", t1 && t1.code === "INVALID_CONFIG");
207
+
208
+ b.cluster._resetForTest();
209
+ var t2 = null;
210
+ try { await b.cluster.init(_initOpts("vk-cfg2", { expectedVaultKeyFp: "not-hex" })); }
211
+ catch (e) { t2 = e; }
212
+ check("non-hex expectedVaultKeyFp throws", t2 && t2.code === "INVALID_CONFIG");
213
+
214
+ b.cluster._resetForTest();
215
+ var t3 = null;
216
+ try { await b.cluster.init(_initOpts("vk-cfg3", { expectedVaultKeyFp: BOGUS_FP })); }
217
+ catch (e) { t3 = e; }
218
+ check("expectedVaultKeyFp without acceptVaultKeyRotation throws", t3 && t3.code === "INVALID_CONFIG");
219
+ } finally { await ctx.teardown(); }
220
+ }
221
+
222
+ async function run() {
223
+ testSurface();
224
+ await testRotationAccepted();
225
+ await testNoDeclarationStillRefuses();
226
+ await testExpectedFpMatchAdopts();
227
+ await testExpectedFpMismatchRefuses();
228
+ await testSameKeyReInitIsClean();
229
+ await testConfigValidation();
230
+ }
231
+
232
+ module.exports = { run: run };
233
+
234
+ if (require.main === module) {
235
+ run().then(
236
+ function () { console.log("[cluster-vault-rotation] OK — " + helpers.getChecks() + " checks passed"); },
237
+ // Rethrow rather than console.error(e): setupTestDb seeds a vault
238
+ // passphrase, and logging the error object trips CodeQL's clear-text-
239
+ // logging taint. Let Node print the uncaught error + stack and exit
240
+ // non-zero, with no logging sink for the taint to reach.
241
+ function (e) { process.exitCode = 1; throw e; }
242
+ );
243
+ }