@blamejs/blamejs-shop 0.3.39 → 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 (62) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +331 -0
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/storefront.js +244 -4
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/CHANGELOG.md +8 -0
  7. package/lib/vendor/blamejs/README.md +8 -5
  8. package/lib/vendor/blamejs/SECURITY.md +7 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +266 -2
  10. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  11. package/lib/vendor/blamejs/index.js +7 -1
  12. package/lib/vendor/blamejs/lib/agent-idempotency.js +113 -0
  13. package/lib/vendor/blamejs/lib/agent-orchestrator.js +108 -0
  14. package/lib/vendor/blamejs/lib/agent-snapshot.js +137 -0
  15. package/lib/vendor/blamejs/lib/agent-tenant.js +193 -17
  16. package/lib/vendor/blamejs/lib/ai-input.js +167 -3
  17. package/lib/vendor/blamejs/lib/ai-output.js +463 -0
  18. package/lib/vendor/blamejs/lib/ai-prompt.js +304 -0
  19. package/lib/vendor/blamejs/lib/archive-wrap.js +234 -1
  20. package/lib/vendor/blamejs/lib/archive.js +1 -0
  21. package/lib/vendor/blamejs/lib/audit.js +3 -0
  22. package/lib/vendor/blamejs/lib/auth/oid4vp.js +47 -28
  23. package/lib/vendor/blamejs/lib/cluster.js +186 -14
  24. package/lib/vendor/blamejs/lib/codepoint-class.js +18 -0
  25. package/lib/vendor/blamejs/lib/compliance-ai-act.js +446 -0
  26. package/lib/vendor/blamejs/lib/consent.js +104 -8
  27. package/lib/vendor/blamejs/lib/content-credentials.js +851 -41
  28. package/lib/vendor/blamejs/lib/crypto-field.js +5 -0
  29. package/lib/vendor/blamejs/lib/db.js +15 -0
  30. package/lib/vendor/blamejs/lib/framework-error.js +21 -0
  31. package/lib/vendor/blamejs/lib/mail-srs.js +122 -19
  32. package/lib/vendor/blamejs/lib/privacy.js +168 -0
  33. package/lib/vendor/blamejs/lib/safe-archive.js +196 -136
  34. package/lib/vendor/blamejs/lib/validate-opts.js +24 -0
  35. package/lib/vendor/blamejs/lib/vault/rotate.js +175 -15
  36. package/lib/vendor/blamejs/lib/vault-aad.js +84 -33
  37. package/lib/vendor/blamejs/package.json +1 -1
  38. package/lib/vendor/blamejs/release-notes/v0.14.11.json +72 -0
  39. package/lib/vendor/blamejs/release-notes/v0.14.12.json +95 -0
  40. package/lib/vendor/blamejs/release-notes/v0.14.13.json +52 -0
  41. package/lib/vendor/blamejs/release-notes/v0.14.14.json +31 -0
  42. package/lib/vendor/blamejs/test/00-primitives.js +9 -1
  43. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +103 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +91 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +186 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +140 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +59 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +125 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +133 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +6 -1
  51. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +94 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +176 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +243 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +130 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +70 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +289 -2
  58. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +22 -0
  59. package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +61 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +69 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +158 -0
  62. package/package.json +1 -1
@@ -1,11 +1,122 @@
1
1
  "use strict";
2
2
  /**
3
- * b.contentCredentials — SB-942 / AB-853 / C2PA manifest builder.
3
+ * b.contentCredentials — SB-942 / AB-853 / C2PA manifest builder, the
4
+ * COSE_Sign1 interop wrapper with its RFC 3161 sigTst2 countersignature,
5
+ * and the CAWG identity assertion.
4
6
  */
5
7
 
6
8
  var helpers = require("../helpers");
7
9
  var b = helpers.b;
8
10
  var check = helpers.check;
11
+ var asn1 = require("../../lib/asn1-der");
12
+ var nodeCrypto = require("node:crypto");
13
+
14
+ // ---- RFC 3161 token-mint helpers (mock TSA, in-tree ASN.1 DER) ----
15
+ // Mirrors test/layer-0-primitives/tsa.test.js so the sigTst2
16
+ // countersignature round-trip exercises a real-shaped timestamp token
17
+ // (CMS SignedData over a TSTInfo) with full control over the imprint.
18
+ var ID_CT_TST_INFO = "1.2.840.113549.1.9.16.1.4";
19
+ var ID_KP_TIMESTAMPING = "1.3.6.1.5.5.7.3.8";
20
+ var OID_CONTENT_TYPE_ATTR = "1.2.840.113549.1.9.3";
21
+ var OID_MESSAGE_DIGEST_ATTR = "1.2.840.113549.1.9.4";
22
+ var OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
23
+ var OID_SHA512 = "2.16.840.1.101.3.4.2.3";
24
+ var OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
25
+
26
+ function _utcTime(d) { var s = d.toISOString().replace(/[-:T]/g, "").slice(2, 14) + "Z"; return asn1.writeNode(0x17, Buffer.from(s, "ascii")); }
27
+ function _genTime(d) { var s = d.toISOString().replace(/[-:T]/g, "").slice(0, 14) + "Z"; return asn1.writeNode(0x18, Buffer.from(s, "ascii")); }
28
+ function _algId(oid, n) { return n ? asn1.writeSequence([asn1.writeOid(oid), asn1.writeNull()]) : asn1.writeSequence([asn1.writeOid(oid)]); }
29
+ function _certName(cn) { return asn1.writeSequence([asn1.writeSet([asn1.writeSequence([asn1.writeOid("2.5.4.3"), asn1.writeUtf8String(cn)])])]); }
30
+
31
+ function _makeTsaCert() {
32
+ var kp = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
33
+ var spki = kp.publicKey.export({ type: "spki", format: "der" });
34
+ var version = asn1.writeContextExplicit(0, asn1.writeInteger(Buffer.from([2])));
35
+ var serial = asn1.writeInteger(Buffer.from([0x2a]));
36
+ var sigAlgId = _algId("1.2.840.113549.1.1.11", true);
37
+ var name = _certName("Test TSA");
38
+ var now = Date.now();
39
+ var validity = asn1.writeSequence([_utcTime(new Date(now - 86400000)), _utcTime(new Date(now + 86400000 * 3650))]);
40
+ var ekuChildren = [asn1.writeOid("2.5.29.37"), asn1.writeBoolean(true),
41
+ asn1.writeOctetString(asn1.writeSequence([asn1.writeOid(ID_KP_TIMESTAMPING)]))];
42
+ var exts = [asn1.writeSequence(ekuChildren)];
43
+ var children = [version, serial, sigAlgId, name, validity, name, spki, asn1.writeContextExplicit(3, asn1.writeSequence(exts))];
44
+ var tbs = asn1.writeSequence(children);
45
+ var tbsSig = nodeCrypto.sign("sha256", tbs, kp.privateKey);
46
+ var certDer = asn1.writeSequence([tbs, sigAlgId, asn1.writeBitString(tbsSig, 0)]);
47
+ return { certDer: certDer, key: kp.privateKey, serial: Buffer.from([0x2a]), issuer: name };
48
+ }
49
+
50
+ // Build an X.509 cert for `cn`, signed by `issuer` ({ name, key }) or
51
+ // self-signed (issuer null). Used to assemble a real [leaf, intermediate,
52
+ // root] chain so the identity-assertion chain walk is exercised through
53
+ // an intermediate CA, not only a direct-root / self-signed leaf.
54
+ function _makeCert(cn, issuer) {
55
+ var kp = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
56
+ var spki = kp.publicKey.export({ type: "spki", format: "der" });
57
+ var subjName = _certName(cn);
58
+ var issuerName = issuer ? issuer.name : subjName;
59
+ var issuerKey = issuer ? issuer.key : kp.privateKey;
60
+ var sigAlgId = _algId("1.2.840.113549.1.1.11", true);
61
+ var version = asn1.writeContextExplicit(0, asn1.writeInteger(Buffer.from([2])));
62
+ var serial = asn1.writeInteger(Buffer.from([0x2b]));
63
+ var now = Date.now();
64
+ var validity = asn1.writeSequence([_utcTime(new Date(now - 86400000)), _utcTime(new Date(now + 86400000 * 3650))]);
65
+ var tbs = asn1.writeSequence([version, serial, sigAlgId, issuerName, validity, subjName, spki]);
66
+ var tbsSig = nodeCrypto.sign("sha256", tbs, issuerKey);
67
+ var certDer = asn1.writeSequence([tbs, sigAlgId, asn1.writeBitString(tbsSig, 0)]);
68
+ return { name: subjName, key: kp.privateKey, pem: new nodeCrypto.X509Certificate(certDer).toString() };
69
+ }
70
+
71
+ function _makeTsToken(o) {
72
+ var tstChildren = [
73
+ asn1.writeInteger(Buffer.from([1])),
74
+ asn1.writeOid("1.2.3.4.1"),
75
+ asn1.writeSequence([_algId(OID_SHA512, true), asn1.writeOctetString(o.imprintHash)]),
76
+ asn1.writeInteger(Buffer.from([0x05])),
77
+ _genTime(new Date()),
78
+ ];
79
+ if (o.nonce) tstChildren.push(asn1.writeInteger(o.nonce));
80
+ var tstInfo = asn1.writeSequence(tstChildren);
81
+ var msgDigest = nodeCrypto.createHash("sha512").update(tstInfo).digest();
82
+ var ctAttr = asn1.writeSequence([asn1.writeOid(OID_CONTENT_TYPE_ATTR), asn1.writeSet([asn1.writeOid(ID_CT_TST_INFO)])]);
83
+ var mdAttr = asn1.writeSequence([asn1.writeOid(OID_MESSAGE_DIGEST_ATTR), asn1.writeSet([asn1.writeOctetString(msgDigest)])]);
84
+ var signedSet = asn1.writeSet([ctAttr, mdAttr]);
85
+ var sig = nodeCrypto.sign("sha512", signedSet, o.key);
86
+ var signedAttrsImplicit = Buffer.concat([Buffer.from([0xa0]), signedSet.slice(1)]);
87
+ var sid = asn1.writeSequence([o.issuer, asn1.writeInteger(o.serial)]);
88
+ var signerInfo = asn1.writeSequence([
89
+ asn1.writeInteger(Buffer.from([1])), sid, _algId(OID_SHA512, true), signedAttrsImplicit,
90
+ _algId(OID_RSA_ENCRYPTION, true), asn1.writeOctetString(sig),
91
+ ]);
92
+ var encap = asn1.writeSequence([asn1.writeOid(ID_CT_TST_INFO), asn1.writeContextExplicit(0, asn1.writeOctetString(tstInfo))]);
93
+ var certsField = asn1.writeContextImplicit(0, o.certDer, { constructed: true });
94
+ var signedData = asn1.writeSequence([
95
+ asn1.writeInteger(Buffer.from([3])), asn1.writeSet([_algId(OID_SHA512, true)]),
96
+ encap, certsField, asn1.writeSet([signerInfo]),
97
+ ]);
98
+ return asn1.writeSequence([asn1.writeOid(OID_SIGNED_DATA), asn1.writeContextExplicit(0, signedData)]);
99
+ }
100
+
101
+ // Drive a full signCose request→TSA→attach→verify round trip and return
102
+ // { attached, verifyResult, anchorPem, nonce }.
103
+ function _timestampedRoundTrip(manifest, pair) {
104
+ var req = b.contentCredentials.signCose(manifest, {
105
+ privateKeyPem: pair.privateKey, alg: "ml-dsa-87", timestamp: {}, audit: false,
106
+ });
107
+ var digest = nodeCrypto.createHash("sha512").update(req.timestampRequest.toBeSigned).digest();
108
+ var cert = _makeTsaCert();
109
+ var token = _makeTsToken({
110
+ certDer: cert.certDer, key: cert.key, issuer: cert.issuer, serial: cert.serial,
111
+ imprintHash: digest, nonce: req.timestampRequest.nonce,
112
+ });
113
+ var attached = b.contentCredentials.signCose(manifest, {
114
+ privateKeyPem: pair.privateKey, alg: "ml-dsa-87",
115
+ timestamp: { token: token, signature: req.timestampRequest.signature }, audit: false,
116
+ });
117
+ var anchorPem = new nodeCrypto.X509Certificate(cert.certDer).toString();
118
+ return { req: req, attached: attached, cert: cert, token: token, anchorPem: anchorPem };
119
+ }
9
120
 
10
121
  async function run() {
11
122
  check("build is fn", typeof b.contentCredentials.build === "function");
@@ -79,16 +190,192 @@ async function run() {
79
190
  var cose = b.contentCredentials.signCose(manifest2, {
80
191
  privateKeyPem: pair.privateKey,
81
192
  alg: "ml-dsa-87",
193
+ timestamp: false,
194
+ timestampOptOutReason: "interop fixture — no TSA in this test",
82
195
  });
83
196
  check("signCose: returns coseSign1 Buffer", Buffer.isBuffer(cose.coseSign1));
84
197
  check("signCose: alg echoed", cose.alg === "ml-dsa-87");
85
198
  check("signCose: CBOR tag 18 (COSE_Sign1)", cose.coseSign1[0] === 0xD2);
86
199
  check("signCose: array of 4 elements", cose.coseSign1[1] === 0x84);
200
+ check("signCose: not timestamped on opt-out", cose.timestamped === false);
87
201
 
88
202
  threw = null;
89
- try { b.contentCredentials.signCose(manifest2, { privateKeyPem: pair.privateKey, alg: "unknown" }); }
203
+ try { b.contentCredentials.signCose(manifest2, { privateKeyPem: pair.privateKey, alg: "unknown", timestamp: false, timestampOptOutReason: "x" }); }
90
204
  catch (e) { threw = e; }
91
205
  check("signCose: unknown alg refused", threw && threw.code === "content-credentials/bad-alg");
206
+
207
+ // ---- v0.14.11: sigTst2 timestamp + verifyCose fail-closed ----
208
+ check("verifyCose is fn", typeof b.contentCredentials.verifyCose === "function");
209
+
210
+ // Timestamping is fail-closed: a missing timestamp opt THROWS.
211
+ threw = null;
212
+ try { b.contentCredentials.signCose(manifest2, { privateKeyPem: pair.privateKey, alg: "ml-dsa-87" }); }
213
+ catch (e) { threw = e; }
214
+ check("signCose: missing timestamp opt refused", threw && threw.code === "content-credentials/timestamp-required");
215
+
216
+ // Explicit opt-out requires a recorded reason.
217
+ threw = null;
218
+ try { b.contentCredentials.signCose(manifest2, { privateKeyPem: pair.privateKey, alg: "ml-dsa-87", timestamp: false }); }
219
+ catch (e) { threw = e; }
220
+ check("signCose: timestamp:false without reason refused", threw && threw.code === "TIMESTAMP_OPT_OUT_NO_REASON");
221
+
222
+ // Request-builder mode returns a TSA query + the signature to re-pin.
223
+ var reqOnly = b.contentCredentials.signCose(manifest2, {
224
+ privateKeyPem: pair.privateKey, alg: "ml-dsa-87", timestamp: {}, audit: false,
225
+ });
226
+ check("signCose request mode: not timestamped yet", reqOnly.timestamped === false);
227
+ check("signCose request mode: returns DER request", Buffer.isBuffer(reqOnly.timestampRequest.der));
228
+ check("signCose request mode: returns signature to re-pin", typeof reqOnly.timestampRequest.signature === "string");
229
+ check("signCose request mode: hashAlg default SHA-512", reqOnly.timestampRequest.hashAlg === "SHA-512");
230
+
231
+ // Bad hashAlg refused.
232
+ threw = null;
233
+ try { b.contentCredentials.signCose(manifest2, { privateKeyPem: pair.privateKey, alg: "ml-dsa-87", timestamp: { hashAlg: "MD5" } }); }
234
+ catch (e) { threw = e; }
235
+ check("signCose: bad timestamp hashAlg refused", threw && threw.code === "content-credentials/bad-tst-hash");
236
+
237
+ // Full round trip: request → mint token → attach → verify (timestamp valid).
238
+ var rt = _timestampedRoundTrip(manifest2, pair);
239
+ check("signCose attach mode: timestamped", rt.attached.timestamped === true);
240
+ check("signCose attach mode: sigTst2 present grows the COSE bytes", rt.attached.coseSign1.length > cose.coseSign1.length);
241
+
242
+ var v3 = b.contentCredentials.verifyCose(rt.attached.coseSign1, pair.publicKey, {
243
+ timestampNonce: rt.req.timestampRequest.nonce, timestampTrustAnchorsPem: [rt.anchorPem], audit: false,
244
+ });
245
+ check("verifyCose: signature valid", v3.valid === true);
246
+ check("verifyCose: timestamp valid (routed through tsa.verifyToken)", v3.timestamp && v3.timestamp.valid === true);
247
+ check("verifyCose: timestamp genTime is a Date", v3.timestamp.genTime instanceof Date);
248
+ check("verifyCose: alg surfaced", v3.alg === "ml-dsa-87");
249
+ check("verifyCose: claims recovered", v3.claims && v3.claims.content && v3.claims.content.id === "img-002");
250
+
251
+ // verifyCose NEVER throws — bad inputs return { valid:false }.
252
+ var vNoBuf = b.contentCredentials.verifyCose("not a buffer", pair.publicKey, { audit: false });
253
+ check("verifyCose: non-buffer fails closed (no throw)", vNoBuf.valid === false && vNoBuf.reason === "cose-not-buffer");
254
+ var vNoKey = b.contentCredentials.verifyCose(rt.attached.coseSign1, "", { audit: false });
255
+ check("verifyCose: missing key fails closed", vNoKey.valid === false && vNoKey.reason === "public-key-required");
256
+
257
+ // requireTimestamp default true refuses a token-less COSE_Sign1.
258
+ var vReq = b.contentCredentials.verifyCose(cose.coseSign1, pair.publicKey, { audit: false });
259
+ check("verifyCose: token-less refused by default", vReq.valid === false && vReq.reason === "timestamp-required");
260
+ var vReqOff = b.contentCredentials.verifyCose(cose.coseSign1, pair.publicKey, { requireTimestamp: false, audit: false });
261
+ check("verifyCose: requireTimestamp:false accepts token-less", vReqOff.valid === true);
262
+
263
+ // Tampered payload byte → signature mismatch, fail-closed.
264
+ var tamperedCose = Buffer.from(rt.attached.coseSign1); tamperedCose[tamperedCose.length - 3] ^= 0xff;
265
+ var vTamper = b.contentCredentials.verifyCose(tamperedCose, pair.publicKey, { audit: false });
266
+ check("verifyCose: tampered token fails closed", vTamper.valid === false);
267
+
268
+ // Wrong nonce on an otherwise-valid token → tsa-derived timestamp failure,
269
+ // surfaced as a fail-closed verdict (NOT a throw, NOT a chain-only pass).
270
+ var vNonce = b.contentCredentials.verifyCose(rt.attached.coseSign1, pair.publicKey, {
271
+ timestampNonce: nodeCrypto.randomBytes(8), audit: false,
272
+ });
273
+ check("verifyCose: wrong timestamp nonce fails closed via tsa.verifyToken",
274
+ vNonce.valid === false && /^timestamp-invalid:/.test(vNonce.reason));
275
+
276
+ // A cryptographically valid COSE_Sign1 over a payload that is NOT a
277
+ // complete SB-942 manifest must NOT verify as a content credential —
278
+ // verifyCose re-runs the required-field gate that verify() applies.
279
+ var partialCose = b.contentCredentials.signCose({ provider: { name: "x" }, content: { id: "y" } }, {
280
+ privateKeyPem: pair.privateKey, alg: "ml-dsa-87",
281
+ timestamp: false, timestampOptOutReason: "test fixture", audit: false,
282
+ });
283
+ var vPartial = b.contentCredentials.verifyCose(partialCose.coseSign1, pair.publicKey, {
284
+ requireTimestamp: false, audit: false,
285
+ });
286
+ check("verifyCose: incomplete manifest rejected (missing-required)",
287
+ vPartial.valid === false && vPartial.reason.indexOf("missing-required") === 0);
288
+
289
+ // ---- v0.14.11: CAWG identity assertion ----
290
+ check("attachIdentityAssertion is fn", typeof b.contentCredentials.attachIdentityAssertion === "function");
291
+ check("verifyIdentityAssertion is fn", typeof b.contentCredentials.verifyIdentityAssertion === "function");
292
+
293
+ var orgPair = b.crypto.generateSigningKeyPair("ml-dsa-87");
294
+ var refs = [{ label: "c2pa.actions", data: { action: "c2pa.created" } }];
295
+ var ia = b.contentCredentials.attachIdentityAssertion({
296
+ binding: "x509",
297
+ subject: { name: "Acme Newsroom", org: "Acme Media", id: "acme-001" },
298
+ referencedAssertions: refs,
299
+ privateKeyPem: orgPair.privateKey,
300
+ audit: false,
301
+ });
302
+ check("attachIdentityAssertion: v1.2 envelope", ia.type === "cawg.identity" && ia.version === "1.2");
303
+ check("attachIdentityAssertion: hash-binds referenced assertions", ia.signer_payload.referenced_assertions.length === 1);
304
+ check("attachIdentityAssertion: assertion is hash-bound (sha3-512)", ia.signer_payload.referenced_assertions[0].alg === "sha3-512");
305
+ check("attachIdentityAssertion: base64 signature", typeof ia.signature === "string" && ia.signature.length > 0);
306
+
307
+ // Self-asserted (no trust anchor) → valid signature, but verified:false.
308
+ var iaSelf = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, { referencedAssertions: refs, audit: false });
309
+ check("verifyIdentityAssertion: self-asserted valid", iaSelf.valid === true);
310
+ check("verifyIdentityAssertion: self-asserted NOT verified (no anchor)", iaSelf.verified === false && iaSelf.reason === "no-trust-anchor");
311
+
312
+ // x509 chained to a supplied trust anchor → verified:true. Reuse the
313
+ // mock TSA cert as a self-signed leaf==anchor.
314
+ var orgCert = _makeTsaCert();
315
+ var orgPem = new nodeCrypto.X509Certificate(orgCert.certDer).toString();
316
+ var iaTrusted = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
317
+ referencedAssertions: refs, identityCertChainPem: orgPem, identityTrustAnchorsPem: orgPem, audit: false,
318
+ });
319
+ check("verifyIdentityAssertion: x509 chained → verified", iaTrusted.valid === true && iaTrusted.verified === true);
320
+
321
+ // x509 chain through an INTERMEDIATE CA: chain [leaf, intermediate]
322
+ // with the root as the trust anchor must verify — the chain walk uses
323
+ // chain[1..], not only a direct leaf-vs-anchor test.
324
+ var caRoot = _makeCert("CAWG Test Root", null);
325
+ var caInter = _makeCert("CAWG Test Intermediate", { name: caRoot.name, key: caRoot.key });
326
+ var caLeaf = _makeCert("Acme Newsroom Leaf", { name: caInter.name, key: caInter.key });
327
+ var iaChain = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
328
+ referencedAssertions: refs,
329
+ identityCertChainPem: [caLeaf.pem, caInter.pem],
330
+ identityTrustAnchorsPem: caRoot.pem, audit: false,
331
+ });
332
+ check("verifyIdentityAssertion: [leaf,intermediate]→root anchor verified (walks intermediates)",
333
+ iaChain.valid === true && iaChain.verified === true);
334
+ // The same chain against an unrelated anchor must NOT verify.
335
+ var caRogue = _makeCert("Rogue Root", null);
336
+ var iaWrongAnchor = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
337
+ referencedAssertions: refs,
338
+ identityCertChainPem: [caLeaf.pem, caInter.pem],
339
+ identityTrustAnchorsPem: caRogue.pem, audit: false,
340
+ });
341
+ check("verifyIdentityAssertion: chain to unrelated anchor → not verified",
342
+ iaWrongAnchor.verified === false);
343
+
344
+ // Transplanted referenced assertions → fail closed.
345
+ var iaTransplant = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
346
+ referencedAssertions: [{ label: "x", data: { other: 1 } }], audit: false,
347
+ });
348
+ check("verifyIdentityAssertion: transplanted assertions fail closed",
349
+ iaTransplant.valid === false && iaTransplant.reason === "assertion-hash-mismatch");
350
+
351
+ // Aggregator path is never verified:true even with an anchor.
352
+ var iaAgg = b.contentCredentials.attachIdentityAssertion({
353
+ binding: "identity-claims-aggregator", subject: { name: "Jane Doe" },
354
+ referencedAssertions: refs, privateKeyPem: orgPair.privateKey, audit: false,
355
+ });
356
+ var iaAggV = b.contentCredentials.verifyIdentityAssertion(iaAgg, orgPair.publicKey, {
357
+ referencedAssertions: refs, identityTrustAnchorsPem: orgPem, audit: false,
358
+ });
359
+ check("verifyIdentityAssertion: aggregator path never verified:true",
360
+ iaAggV.valid === true && iaAggV.verified === false && iaAggV.reason === "aggregator-self-asserted");
361
+
362
+ // Wrong key → signature mismatch, fail closed (no throw).
363
+ var otherPair = b.crypto.generateSigningKeyPair("ml-dsa-87");
364
+ var iaWrongKey = b.contentCredentials.verifyIdentityAssertion(ia, otherPair.publicKey, { referencedAssertions: refs, audit: false });
365
+ check("verifyIdentityAssertion: wrong key fails closed", iaWrongKey.valid === false && iaWrongKey.reason === "signature-mismatch");
366
+
367
+ // Bad binding refused at config time.
368
+ threw = null;
369
+ try { b.contentCredentials.attachIdentityAssertion({ binding: "made-up", subject: { name: "x" }, referencedAssertions: refs, privateKeyPem: orgPair.privateKey }); }
370
+ catch (e) { threw = e; }
371
+ check("attachIdentityAssertion: bad binding refused", threw && threw.code === "content-credentials/bad-identity-binding");
92
372
  }
93
373
 
94
374
  module.exports = { run: run };
375
+
376
+ if (require.main === module) {
377
+ run().then(
378
+ function () { console.log("[content-credentials] OK — " + helpers.getChecks() + " checks passed"); },
379
+ function (e) { console.error("FAIL:", (e && e.stack) || e); process.exit(1); }
380
+ );
381
+ }
@@ -213,6 +213,28 @@ function testDcqlMatch() {
213
213
  try { b.auth.oid4vp.matchDcql([], { credentials: [] }); }
214
214
  catch (e) { threw = /non-empty array/.test(e.message); }
215
215
  check("DCQL: empty credentials array refused", threw);
216
+
217
+ // ---- null path-segment = array wildcard (OpenID4VP 1.0 §7.1.1) ----
218
+ function _dcqlValid(path, claimsObj, values) {
219
+ var q = { credentials: [{ id: "c", format: "vc+sd-jwt",
220
+ claims: [values ? { path: path, values: values } : { path: path }] }] };
221
+ return b.auth.oid4vp.matchDcql([{ id: "c", format: "vc+sd-jwt", claims: claimsObj }], q).valid;
222
+ }
223
+ var degrees = { degrees: [{ type: "BachelorDegree" }, { type: "MasterDegree" }] };
224
+ check("DCQL null: any-element type matches", _dcqlValid(["degrees", null, "type"], degrees));
225
+ check("DCQL null: value-match on any element (hit)", _dcqlValid(["degrees", null, "type"], degrees, ["MasterDegree"]));
226
+ check("DCQL null: value-match on any element (miss)", !_dcqlValid(["degrees", null, "type"], degrees, ["PhD"]));
227
+ // null on a non-array node → clean non-match, never throws (holder data, not config)
228
+ var threwNull = false; var nullNonArray = true;
229
+ try { nullNonArray = _dcqlValid(["degrees", null], { degrees: "not-an-array" }); }
230
+ catch (_e) { threwNull = true; }
231
+ check("DCQL null on non-array does not throw", !threwNull);
232
+ check("DCQL null on non-array is a non-match", !nullNonArray);
233
+ // null at the leaf → match iff the array holds any defined element
234
+ check("DCQL null at leaf matches non-empty array", _dcqlValid(["tags", null], { tags: ["a", "b"] }));
235
+ check("DCQL null at leaf misses empty array", !_dcqlValid(["tags", null], { tags: [] }));
236
+ // nested null wildcards recurse to arbitrary depth
237
+ check("DCQL nested null wildcards match", _dcqlValid(["a", null, "b", null], { a: [{ b: [1, 2] }] }));
216
238
  }
217
239
 
218
240
  // ---- OID4VCI issuer config -------------------------------------------
@@ -133,6 +133,66 @@ function testLocalPartWithEquals() {
133
133
  srs.reverse(rw) === "foo=bar@bob.com");
134
134
  }
135
135
 
136
+ function testSrs1DoubleForward() {
137
+ // Two distinct forwarders with distinct secrets.
138
+ var srsA = b.mail.srs.create({ secret: b.crypto.generateToken(32), forwarderDomain: "fwd1.example" });
139
+ var srsB = b.mail.srs.create({ secret: b.crypto.generateToken(32), forwarderDomain: "fwd2.example" });
140
+
141
+ // Hop 1: alice@bob.com → fwd1 (SRS0).
142
+ var rw0 = srsA.rewrite("alice@bob.com");
143
+ check("srs1: first hop is SRS0 at fwd1", rw0.startsWith("SRS0=") && rw0.endsWith("@fwd1.example"));
144
+
145
+ // Hop 2: fwd2 chains the already-SRS0 address as SRS1.
146
+ var rw1 = srsB.srs1Rewrite(rw0);
147
+ check("srs1: produces SRS1=", rw1.startsWith("SRS1="));
148
+ check("srs1: ends at this forwarder (fwd2)", rw1.endsWith("@fwd2.example"));
149
+ check("srs1: embeds the SRS0 originator", rw1.indexOf("fwd1.example") !== -1);
150
+ check("srs1: has '==' separator", rw1.indexOf("==") > 0);
151
+
152
+ // reverse() at fwd2 unwraps exactly one hop → the originator's SRS0 (== rw0).
153
+ var back = srsB.reverse(rw1);
154
+ check("srs1: reverse unwraps one hop to the originator SRS0", back === rw0);
155
+ // fwd1 then reverses that SRS0 → original sender (full chain).
156
+ check("srs1: two-hop chain recovers the original sender", srsA.reverse(back) === "alice@bob.com");
157
+
158
+ // Tampered SRS1 tag → bad-tag.
159
+ var tampered = "SRS1=ZZZZ=" + rw1.slice(rw1.indexOf("=", 5) + 1);
160
+ var threwTag = null;
161
+ try { srsB.reverse(tampered); } catch (e) { threwTag = e; }
162
+ check("srs1: tampered tag refused", threwTag && /srs\/bad-tag/.test(threwTag.code || ""));
163
+
164
+ // rw1 is addressed to fwd2; fwd1 reversing it → wrong-forwarder.
165
+ var threwFwd = null;
166
+ try { srsA.reverse(rw1); } catch (e) { threwFwd = e; }
167
+ check("srs1: bounce at the wrong forwarder refused", threwFwd && /srs\/wrong-forwarder/.test(threwFwd.code || ""));
168
+
169
+ // srs1Rewrite on a plain (non-SRS) address → not-srs0.
170
+ var threwPlain = null;
171
+ try { srsB.srs1Rewrite("alice@bob.com"); } catch (e) { threwPlain = e; }
172
+ check("srs1Rewrite: plain address refused", threwPlain && /srs\/not-srs0/.test(threwPlain.code || ""));
173
+
174
+ // srs1Rewrite empty / no-@ → bad-address.
175
+ var threwEmpty = null;
176
+ try { srsB.srs1Rewrite(""); } catch (e) { threwEmpty = e; }
177
+ check("srs1Rewrite: empty refused", threwEmpty && /srs\/bad-address/.test(threwEmpty.code || ""));
178
+
179
+ // reverse of a malformed SRS1 (no '==' separator) → malformed.
180
+ var threwMal = null;
181
+ try { srsB.reverse("SRS1=abc@fwd2.example"); } catch (e) { threwMal = e; }
182
+ check("srs1: malformed (no '==') refused", threwMal && /srs\/malformed/.test(threwMal.code || ""));
183
+
184
+ // Third hop: fwd3 re-wraps fwd2's SRS1. The bounce target stays the
185
+ // SRS0 originator (fwd1), so it still routes straight back, never to
186
+ // the intermediate fwd2.
187
+ var srsC = b.mail.srs.create({ secret: b.crypto.generateToken(32), forwarderDomain: "fwd3.example" });
188
+ var rw2 = srsC.srs1Rewrite(rw1);
189
+ check("srs1: third hop is SRS1 at fwd3", rw2.startsWith("SRS1=") && rw2.endsWith("@fwd3.example"));
190
+ check("srs1: third hop keeps the originator (fwd1)", rw2.indexOf("fwd1.example") !== -1);
191
+ var back2 = srsC.reverse(rw2);
192
+ check("srs1: third hop unwraps straight back to the originator SRS0", back2 === rw0);
193
+ check("srs1: full three-hop chain recovers the original sender", srsA.reverse(back2) === "alice@bob.com");
194
+ }
195
+
136
196
  async function run() {
137
197
  testSurface();
138
198
  testRoundTrip();
@@ -142,6 +202,7 @@ async function run() {
142
202
  testBadShape();
143
203
  testForwarderDomainBinding();
144
204
  testLocalPartWithEquals();
205
+ testSrs1DoubleForward();
145
206
  }
146
207
 
147
208
  module.exports = { run: run };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ // b.privacy.vendorReview + b.privacy.listVendorReviewClauses — FERPA / SOPIPA
3
+ // EdTech vendor-review attestation builder (pure; no DB).
4
+
5
+ var helpers = require("../helpers");
6
+ var b = helpers.b;
7
+ var check = helpers.check;
8
+
9
+ var ALL_TRUE = {
10
+ educationalPurposeOnly: true, noTargetedAdvertising: true, noCommercialProfiling: true,
11
+ noSaleOfStudentData: true, securitySafeguards: true, deletionOnRequest: true,
12
+ subProcessorsCurrent: true, breachNotification: true, schoolOfficialDesignation: true,
13
+ };
14
+
15
+ function run() {
16
+ check("vendorReview is a function", typeof b.privacy.vendorReview === "function");
17
+ check("listVendorReviewClauses is a function", typeof b.privacy.listVendorReviewClauses === "function");
18
+ check("PrivacyError exposed on b.frameworkError", typeof b.frameworkError.PrivacyError === "function");
19
+ check("b.privacy.PrivacyError is the same constructor",
20
+ b.privacy.PrivacyError === b.frameworkError.PrivacyError);
21
+
22
+ var clauses = b.privacy.listVendorReviewClauses();
23
+ check("clauses: frozen array", Array.isArray(clauses) && Object.isFrozen(clauses));
24
+ check("clauses: >= 9 entries", clauses.length >= 9);
25
+ check("clauses: each has id + citation + required boolean",
26
+ clauses.every(function (c) { return typeof c.id === "string" && typeof c.citation === "string" && typeof c.required === "boolean"; }));
27
+ check("clauses: educationalPurposeOnly is required",
28
+ clauses.some(function (c) { return c.id === "educationalPurposeOnly" && c.required === true; }));
29
+
30
+ // All required clauses true → attested, no gaps, 365-day next review clock.
31
+ var reviewedAt = 1700000000000;
32
+ var ok = b.privacy.vendorReview({ vendorName: "Acme LMS", reviewedAt: reviewedAt, clauses: ALL_TRUE });
33
+ check("vendorReview: attested when all required true", ok.attested === true);
34
+ check("vendorReview: no gaps", ok.gaps.length === 0);
35
+ check("vendorReview: nextReviewDueAt = reviewedAt + 365 days",
36
+ ok.nextReviewDueAt === reviewedAt + b.constants.TIME.days(365));
37
+ check("vendorReview: report frozen", Object.isFrozen(ok));
38
+ check("vendorReview: vendorName carried", ok.vendorName === "Acme LMS");
39
+ check("vendorReview: coversPeriod is the prior year", ok.coversPeriod.to === reviewedAt && ok.coversPeriod.from === reviewedAt - b.constants.TIME.days(365));
40
+
41
+ // One required clause false → not attested + the gap is named.
42
+ var partial = Object.assign({}, ALL_TRUE, { noSaleOfStudentData: false });
43
+ var gap = b.privacy.vendorReview({ vendorName: "Acme", reviewedAt: reviewedAt, clauses: partial });
44
+ check("vendorReview: not attested on a required gap", gap.attested === false);
45
+ check("vendorReview: gap names the failed clause", gap.gaps.indexOf("noSaleOfStudentData") !== -1);
46
+
47
+ // The one optional clause omitted still attests.
48
+ check("vendorReview: optional clause omitted still attests",
49
+ b.privacy.vendorReview({ vendorName: "A", reviewedAt: reviewedAt, clauses: ALL_TRUE }).attested === true);
50
+
51
+ // Config-time throws (rule §5 — entry-point tier).
52
+ function expectCode(label, fn, code) {
53
+ var threw = null;
54
+ try { fn(); } catch (e) { threw = e; }
55
+ check(label, threw && (threw.code || "") === code);
56
+ }
57
+ expectCode("missing vendorName throws",
58
+ function () { b.privacy.vendorReview({ reviewedAt: reviewedAt, clauses: ALL_TRUE }); }, "privacy/bad-vendor");
59
+ expectCode("non-boolean clause value throws",
60
+ function () { b.privacy.vendorReview({ vendorName: "A", reviewedAt: reviewedAt, clauses: { noSaleOfStudentData: "yes" } }); }, "privacy/bad-clause-value");
61
+ expectCode("unknown clause key throws",
62
+ function () { b.privacy.vendorReview({ vendorName: "A", reviewedAt: reviewedAt, clauses: { bogusClause: true } }); }, "privacy/unknown-clause");
63
+ expectCode("missing reviewedAt throws",
64
+ function () { b.privacy.vendorReview({ vendorName: "A", clauses: ALL_TRUE }); }, "privacy/bad-reviewed-at");
65
+ expectCode("non-object opts throws",
66
+ function () { b.privacy.vendorReview("nope"); }, "privacy/bad-opts");
67
+ }
68
+
69
+ module.exports = { run: run };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /**
3
+ * b.vaultRotate.rotate — AAD round-trip.
4
+ *
5
+ * Regression for the vault-key rotation orphaning class: a keypair
6
+ * rotation must re-seal, old-root -> new-root,
7
+ * - vault.aad: data cells (registerTable({ aad: true })),
8
+ * - the AAD-bound db.key.enc (deployment-context sealed master key), and
9
+ * - the AAD-bound db.enc envelope (_dbEncAad(dataDir)),
10
+ * so the rotated data dir opens and decrypts under the NEW keypair and no
11
+ * longer under the old one. Before this fix rotate re-sealed only plain
12
+ * `vault:` cells, threw `bad-dbkey` on the AAD-sealed master key, and read
13
+ * db.enc with no AAD — silently orphaning AAD cells while reporting ok.
14
+ */
15
+
16
+ var helpers = require("../helpers");
17
+ var b = helpers.b;
18
+ var fs = helpers.fs;
19
+ var os = helpers.os;
20
+ var path = helpers.path;
21
+ var check = helpers.check;
22
+
23
+ var SECRETS_SCHEMA = [{
24
+ name: "secrets",
25
+ columns: { _id: "TEXT PRIMARY KEY", tenantId: "TEXT", secret: "TEXT" },
26
+ sealedFields: ["secret"],
27
+ aad: true,
28
+ rowIdField: "_id",
29
+ schemaVersion: "1",
30
+ }];
31
+
32
+ // Discover every lib module that declares an external AAD_ROTATION descriptor
33
+ // by reading its actual export, so the gate-coverage assertion can't drift as
34
+ // modules are added. backend:"external" means the store lives outside db.enc
35
+ // (an operator-supplied backend) and the rotation pipeline can only refuse and
36
+ // point at the module's reseal hook — so every such module MUST be reachable
37
+ // from rotate's EXTERNAL_AAD_MODULE_LOADERS or a rotation silently orphans it.
38
+ function _discoverExternalAadTables() {
39
+ var libDir = path.join(__dirname, "..", "..", "lib");
40
+ var files = [];
41
+ fs.readdirSync(libDir).forEach(function (name) {
42
+ var full = path.join(libDir, name);
43
+ if (fs.statSync(full).isDirectory()) {
44
+ fs.readdirSync(full).forEach(function (n2) { if (n2.slice(-3) === ".js") files.push(path.join(full, n2)); });
45
+ } else if (name.slice(-3) === ".js") { files.push(full); }
46
+ });
47
+ var tables = [];
48
+ files.forEach(function (f) {
49
+ if (f.replace(/\\/g, "/").indexOf("/vault/rotate.js") !== -1) return; // the gate consumes; it doesn't declare
50
+ var src = fs.readFileSync(f, "utf8");
51
+ if (src.indexOf("AAD_ROTATION") === -1 || src.indexOf("\"external\"") === -1) return;
52
+ var mod;
53
+ try { mod = require(f); } catch (_e) { return; }
54
+ var desc = mod && mod.AAD_ROTATION;
55
+ if (!desc) return;
56
+ (Array.isArray(desc) ? desc : [desc]).forEach(function (d) {
57
+ if (d && d.backend === "external" && d.table) tables.push(d.table);
58
+ });
59
+ });
60
+ return tables;
61
+ }
62
+
63
+ async function _reset() {
64
+ process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
65
+ b.cluster._resetForTest();
66
+ b.audit._resetForTest();
67
+ b.vault._resetForTest();
68
+ b.db._resetForTest();
69
+ }
70
+
71
+ async function run() {
72
+ // Gate-coverage invariant: rotate's external-AAD gate must name every
73
+ // external AAD_ROTATION table any lib module declares. A module that ships
74
+ // the descriptor but isn't wired into EXTERNAL_AAD_MODULE_LOADERS would let
75
+ // rotate() succeed with no acknowledgement, orphaning that store under the
76
+ // retired root (the archive-wrap:tenant-blobs gap caught on PR #293).
77
+ var declaredExternal = _discoverExternalAadTables();
78
+ var gatedExternal = b.vaultRotate._externalAadTables();
79
+ check("found the external AAD_ROTATION modules (idempotency/orchestrator/tenant/snapshot/archive-wrap)",
80
+ declaredExternal.length >= 6);
81
+ var ungated = declaredExternal.filter(function (t) { return gatedExternal.indexOf(t) === -1; });
82
+ check("rotate's external-AAD gate covers every declared external AAD_ROTATION table (ungated: " + ungated.join(", ") + ")",
83
+ ungated.length === 0);
84
+
85
+ var dirNew = fs.mkdtempSync(path.join(os.tmpdir(), "vr-new-"));
86
+ var dirA = fs.mkdtempSync(path.join(os.tmpdir(), "vr-a-"));
87
+ var staging = path.join(os.tmpdir(), "vr-stg-" + process.pid + "-" + Date.now());
88
+
89
+ try {
90
+ // 1. A fresh, distinct keypair to rotate INTO.
91
+ await _reset();
92
+ await b.vault.init({ dataDir: dirNew, mode: "plaintext" });
93
+ var newKeys = JSON.parse(b.vault.getKeysJson());
94
+ b.vault._resetForTest();
95
+
96
+ // 2. The live deployment: vault + encrypted db + an AAD-bound table
97
+ // carrying one sealed row.
98
+ await _reset();
99
+ await b.vault.init({ dataDir: dirA, mode: "plaintext" });
100
+ var oldKeys = JSON.parse(b.vault.getKeysJson());
101
+ check("old and new keypairs differ", JSON.stringify(oldKeys) !== JSON.stringify(newKeys));
102
+ await b.db.init({ dataDir: dirA, tmpDir: path.join(dirA, "tmpfs"), atRest: "encrypted", auditSigning: false, frameworkTables: false, schema: SECRETS_SCHEMA });
103
+
104
+ var sealed = b.cryptoField.sealRow("secrets", { _id: "sec-1", tenantId: "t1", secret: "top-secret-value" });
105
+ check("AAD seal produced a vault.aad: cell (db.init honored aad:true)",
106
+ typeof sealed.secret === "string" && sealed.secret.indexOf("vault.aad:") === 0);
107
+ b.db.from("secrets").insertOne(sealed);
108
+ await b.db.flushToDisk();
109
+ await b.db.close();
110
+
111
+ // Detect-and-refuse: without acknowledging the external-AAD agent
112
+ // stores, rotate must refuse rather than silently orphan them.
113
+ var refused = null;
114
+ try {
115
+ await b.vaultRotate.rotate({ dataDir: dirA, stagingDir: staging + "-x", oldKeys: oldKeys, newKeys: newKeys, mode: "plaintext" });
116
+ } catch (e) { refused = e; }
117
+ check("rotate refuses unacknowledged external-AAD stores",
118
+ !!refused && refused.code === "vault-rotate/external-aad-unresealed");
119
+
120
+ // 3. Rotate the keypair old -> new.
121
+ // externalAadResealed: true acknowledges the operator-supplied agent
122
+ // stores (idempotency/orchestrator/tenant/snapshot) are handled via
123
+ // their own reseal hooks — this deployment uses none of them.
124
+ var result = await b.vaultRotate.rotate({
125
+ dataDir: dirA, stagingDir: staging, oldKeys: oldKeys, newKeys: newKeys, mode: "plaintext",
126
+ externalAadResealed: true,
127
+ });
128
+ // rotate's own round-trip verify decrypts the staged db.enc with the
129
+ // new keypair and (with oldKeys supplied) regression-checks the old —
130
+ // so ok===true means the AAD cell + db.key.enc + db.enc all rotated.
131
+ check("rotate internal verify ok (AAD cells decrypt under new root, old rejected)",
132
+ !!result.verifyResult && result.verifyResult.ok === true);
133
+ check("rotate processed the sealed row", result.totalRowsProcessed >= 1);
134
+
135
+ // 4. Swap staging -> dataDir and re-open under the NEW keypair: proves
136
+ // db.init's AAD-first open of the rotated db.enc + db.key.enc works
137
+ // and the data cell decrypts end-to-end.
138
+ ["db.enc", "db.key.enc", "vault.key"].forEach(function (f) {
139
+ var s = path.join(staging, f);
140
+ if (fs.existsSync(s)) fs.copyFileSync(s, path.join(dirA, f));
141
+ });
142
+ try { fs.rmSync(path.join(dirA, "tmpfs"), { recursive: true, force: true }); } catch (_e) { /* fresh decrypt */ }
143
+
144
+ await _reset();
145
+ await b.vault.init({ dataDir: dirA, mode: "plaintext" });
146
+ check("vault now live under the NEW keypair", JSON.stringify(JSON.parse(b.vault.getKeysJson())) === JSON.stringify(newKeys));
147
+ await b.db.init({ dataDir: dirA, tmpDir: path.join(dirA, "tmpfs"), atRest: "encrypted", auditSigning: false, frameworkTables: false, schema: SECRETS_SCHEMA });
148
+
149
+ var got = b.cryptoField.unsealRow("secrets", b.db.from("secrets").where({ _id: "sec-1" }).first());
150
+ check("AAD cell decrypts after rotation under the new keypair", !!got && got.secret === "top-secret-value");
151
+ await b.db.close();
152
+ } finally {
153
+ await _reset();
154
+ [dirNew, dirA, staging].forEach(function (d) { try { fs.rmSync(d, { recursive: true, force: true }); } catch (_e) { /* cleanup */ } });
155
+ }
156
+ }
157
+
158
+ module.exports = { run: run };