@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
@@ -58,6 +58,8 @@ var { defineClass } = require("./framework-error");
58
58
  var guardAgentRegistry = require("./guard-agent-registry");
59
59
  var bCrypto = require("./crypto");
60
60
  var agentAudit = require("./agent-audit");
61
+ var vaultAad = require("./vault-aad");
62
+ var validateOpts = require("./validate-opts");
61
63
 
62
64
  var audit = lazyRequire(function () { return require("./audit"); });
63
65
  var cluster = lazyRequire(function () { return require("./cluster"); });
@@ -796,13 +798,119 @@ function _safeAudit(ctx, action, actor, metadata) {
796
798
  agentAudit.safeAudit(ctx.audit, action, actor, metadata);
797
799
  }
798
800
 
801
+ // ---- Vault-key rotation: out-of-band reseal hook -------------------------
802
+ //
803
+ // Registry rows are AAD-sealed on an OPERATOR-SUPPLIED backend (opts.backend /
804
+ // the in-memory default), NOT in the framework's db.enc. The vault-key
805
+ // rotation pipeline (b.vaultRotate.rotate) only walks tables inside db.enc,
806
+ // so it cannot reach this backend — sealed tenantId + metadata cells would be
807
+ // ORPHANED under the old vault root after a rotation (CWE-320 cryptographic-
808
+ // key-management failure: ciphertext stranded under a retired key, then
809
+ // unreadable once the old keypair is destroyed). This reseal hook rotates the
810
+ // backend out-of-band, composing the SAME explicit-root primitive the in-tree
811
+ // pipeline uses (vaultAad.resealRoot) and the SAME AAD builder the seal path
812
+ // used (cryptoField._aadParts) so the re-sealed AAD tuple is byte-identical —
813
+ // one source of truth, no drift.
814
+ //
815
+ // Reseal store contract: the durable backend the operator wired for
816
+ // opts.backend already exposes list() (enumerate every row) + set(name, row)
817
+ // (write by name). The row identity column `name` is the AAD anchor and is
818
+ // never sealed, so it is always present in plaintext for the write-back.
819
+ // `tenantId` is a plain sealed string; `metadata` is a sealed JSON string —
820
+ // both are AAD-sealed cells, so each is re-sealed in place under the same AAD
821
+ // without unwrapping the metadata JSON.
822
+ /**
823
+ * @primitive b.agent.orchestrator.reseal
824
+ * @signature b.agent.orchestrator.reseal(opts)
825
+ * @since 0.14.12
826
+ * @status stable
827
+ * @compliance gdpr, soc2
828
+ * @related b.vault.getKeysJson, b.cryptoField.sealRow
829
+ *
830
+ * Re-seals every AAD-bound registry cell (tenantId / metadata) on an
831
+ * operator-supplied backend from the OLD vault keypair to the NEW one,
832
+ * out-of-band. The in-tree vault-key rotation pipeline only walks tables
833
+ * inside `db.enc`, so an operator-supplied orchestrator backend is
834
+ * unreachable to it — after a keypair rotation its cells would otherwise be
835
+ * orphaned under the retired root (CWE-320). Rebuilds each cell's AAD from
836
+ * the registered schema (one source of truth); only AAD-sealed cells are
837
+ * touched. The `name` row-identity column is the AAD anchor and is never
838
+ * sealed, so it is always present for the write-back.
839
+ *
840
+ * @opts
841
+ * store: Object, // { list(): rows[], set(name, row) } (the create() backend contract)
842
+ * oldRootJson: string, // b.vault.getKeysJson() of the retired keypair
843
+ * newRootJson: string, // b.vault.getKeysJson() of the new keypair
844
+ *
845
+ * @example
846
+ * await b.agent.orchestrator.reseal({ store: backend, oldRootJson: oldKeys, newRootJson: newKeys });
847
+ * // → { table: "agent_orchestrator_registry", resealed: 4 }
848
+ */
849
+ function reseal(args) {
850
+ args = args || {};
851
+ validateOpts.requireNonEmptyString(args.oldRootJson,
852
+ "reseal: oldRootJson (b.vault.getKeysJson() of the OLD keypair)",
853
+ AgentOrchestratorError, "agent-orchestrator/bad-root");
854
+ validateOpts.requireNonEmptyString(args.newRootJson,
855
+ "reseal: newRootJson (b.vault.getKeysJson() of the NEW keypair)",
856
+ AgentOrchestratorError, "agent-orchestrator/bad-root");
857
+ var store = args.store;
858
+ validateOpts.requireMethods(store, ["list", "set"],
859
+ "reseal: operator store (same backend contract as create({ backend }))",
860
+ AgentOrchestratorError, "agent-orchestrator/bad-reseal-store");
861
+ _ensureSealTable();
862
+ var schema = cryptoField().getSchema(SEAL_TABLE);
863
+ return Promise.resolve(store.list()).then(function (rows) {
864
+ if (!Array.isArray(rows)) {
865
+ throw new AgentOrchestratorError("agent-orchestrator/bad-reseal-store",
866
+ "reseal: store.list() must resolve to an array of rows");
867
+ }
868
+ var chain = Promise.resolve();
869
+ var resealed = 0;
870
+ rows.forEach(function (row) {
871
+ if (!row || typeof row !== "object") return;
872
+ var changed = false;
873
+ for (var f = 0; f < schema.sealedFields.length; f += 1) {
874
+ var column = schema.sealedFields[f];
875
+ var value = row[column];
876
+ // Only AAD-sealed cells need rotating. Vault-less / pre-sealing rows
877
+ // carry plain values (sealRow leaves them untouched when vault-less);
878
+ // resealRoot would throw not-sealed on a plain value, so skip.
879
+ if (typeof value !== "string" || !vaultAad.isAadSealed(value)) continue;
880
+ var aadParts = cryptoField()._aadParts(schema, SEAL_TABLE, column, row);
881
+ row[column] = vaultAad.resealRoot(value, aadParts, args.oldRootJson, args.newRootJson);
882
+ changed = true;
883
+ }
884
+ if (changed) {
885
+ resealed += 1;
886
+ chain = chain.then(function () { return store.set(row.name, row); });
887
+ }
888
+ });
889
+ return chain.then(function () { return { table: SEAL_TABLE, resealed: resealed }; });
890
+ });
891
+ }
892
+
799
893
  module.exports = {
800
894
  create: create,
801
895
  shardFor: shardFor,
896
+ reseal: reseal,
802
897
  AgentOrchestratorError: AgentOrchestratorError,
803
898
  guards: {
804
899
  registry: guardAgentRegistry,
805
900
  },
901
+ // AAD_ROTATION — the vault-key rotation descriptor every framework module
902
+ // that seals an {aad:true} table on an OPERATOR-SUPPLIED backend (outside
903
+ // db.enc) exports, so an operator can register it with a rotation eager-
904
+ // sweep and the codebase-patterns detect-and-refuse gate can confirm no
905
+ // such external-store table is silently orphaned. `backend: "external"`
906
+ // flags that the in-tree b.vaultRotate.rotate pipeline cannot reach it.
907
+ AAD_ROTATION: {
908
+ table: SEAL_TABLE,
909
+ rowIdField: "name",
910
+ schemaVersion: "1",
911
+ backend: "external",
912
+ reseal: reseal,
913
+ },
806
914
  // Test-only — flush the salted FNV basis cache so a vault reset
807
915
  // between tests forces re-derivation.
808
916
  _resetForTest: function () { _saltedFnvBasisCache = null; },
@@ -53,6 +53,8 @@ var bCrypto = require("./crypto");
53
53
  var guardSnapshotEnvelope = require("./guard-snapshot-envelope");
54
54
  var agentAudit = require("./agent-audit");
55
55
  var safeJson = require("./safe-json");
56
+ var vaultAad = require("./vault-aad");
57
+ var validateOpts = require("./validate-opts");
56
58
 
57
59
  var audit = lazyRequire(function () { return require("./audit"); });
58
60
  var auditSign = lazyRequire(function () { return require("./audit-sign"); });
@@ -163,6 +165,117 @@ function create(opts) {
163
165
  };
164
166
  }
165
167
 
168
+ // ---- Wrapped-AAD root re-seal (vault-key rotation pipeline) ----------------
169
+
170
+ /**
171
+ * @primitive b.agent.snapshot.reseal
172
+ * @signature b.agent.snapshot.reseal(opts)
173
+ * @since 0.14.12
174
+ * @status stable
175
+ * @related b.agent.snapshot.create, b.vault.getKeysJson
176
+ *
177
+ * Re-seal every persisted snapshot envelope from the OLD vault root to
178
+ * the NEW vault root under the SAME column-shaped AAD, for a vault-key
179
+ * rotation. The snapshot seal is a `vault.aad:` ciphertext hidden behind
180
+ * the `snap-sealed-v1:` wrapper prefix and written to an operator
181
+ * backend, so a `db.enc` scan for the bare `vault.aad:` prefix can
182
+ * neither detect nor reach it — the rotation pipeline drives the re-key
183
+ * through this explicit backend walk. Each row is unsealed under the old
184
+ * root and re-sealed under the new root in memory (composing
185
+ * `b.vault.aad.resealRoot`); the plaintext envelope is never written to
186
+ * operator-readable storage. The decorative wrapper fields the backend's
187
+ * `list()` filters on (`snapshotId` / `takenAt` / `tenantId`) are
188
+ * preserved, so the index is untouched.
189
+ *
190
+ * `allowPlaintext` envelopes (no `sealed` wrapper) carry no AAD-sealed
191
+ * blob to re-key and are skipped; the returned `resealed` count reflects
192
+ * only re-sealed rows. A row sealed by a non-default KMS sealer (the
193
+ * inner blob is not a `vault.aad:` value) is refused — re-key it through
194
+ * the operator's own KMS, not this path.
195
+ *
196
+ * @opts
197
+ * backend: { put, get, list }, // the same backend create() was wired with
198
+ * oldRootJson: string, // b.vault.getKeysJson() of the OLD keypair
199
+ * newRootJson: string, // b.vault.getKeysJson() of the NEW keypair
200
+ *
201
+ * @example
202
+ * var result = await b.agent.snapshot.reseal({
203
+ * backend: operatorBackend,
204
+ * oldRootJson: oldKeysJson,
205
+ * newRootJson: newKeysJson,
206
+ * });
207
+ * result.table; // → "agent.snapshot"
208
+ * result.resealed; // → <count of re-keyed snapshots>
209
+ */
210
+ async function reseal(opts) {
211
+ opts = opts || {};
212
+ var backend = opts.backend;
213
+ validateOpts.requireMethods(backend, ["put", "get", "list"],
214
+ "reseal: opts.backend (same backend create() was wired with)",
215
+ AgentSnapshotError, "agent-snapshot/bad-backend");
216
+ validateOpts.requireNonEmptyString(opts.oldRootJson,
217
+ "reseal: opts.oldRootJson (b.vault.getKeysJson() of the OLD keypair)",
218
+ AgentSnapshotError, "agent-snapshot/bad-root");
219
+ validateOpts.requireNonEmptyString(opts.newRootJson,
220
+ "reseal: opts.newRootJson (b.vault.getKeysJson() of the NEW keypair)",
221
+ AgentSnapshotError, "agent-snapshot/bad-root");
222
+
223
+ var entries = await backend.list();
224
+ if (!Array.isArray(entries)) return { table: SNAPSHOT_TABLE, resealed: 0 };
225
+ var resealed = 0;
226
+ for (var i = 0; i < entries.length; i += 1) {
227
+ var snapshotId = entries[i] && entries[i].snapshotId;
228
+ if (typeof snapshotId !== "string" || snapshotId.length === 0) continue;
229
+ var raw = await backend.get(snapshotId);
230
+ if (!raw) continue;
231
+ // Only the sealed-wrapper shape carries a re-keyable blob. The
232
+ // allowPlaintext path stores the bare envelope (no `sealed`) — skip.
233
+ if (!raw.sealed || typeof raw.sealed !== "string" ||
234
+ raw.sealed.indexOf(SEALED_PREFIX) !== 0) {
235
+ continue;
236
+ }
237
+ var innerBlob = raw.sealed.slice(SEALED_PREFIX.length);
238
+ // The inner blob is a vault.aad: ciphertext (when sealed by the
239
+ // default b.vault.aad sealer — the only sealer resealRoot can
240
+ // re-key). A custom KMS sealer's blob isn't a vault.aad: value, so
241
+ // refuse rather than silently no-op: the operator must drive the
242
+ // re-key through their own KMS.
243
+ if (!vaultAad.isAadSealed(innerBlob)) {
244
+ throw new AgentSnapshotError("agent-snapshot/not-vault-sealed",
245
+ "reseal: snapshot " + snapshotId + " was sealed by a non-vault sealer " +
246
+ "(no " + JSON.stringify(vaultAad.AAD_PREFIX) + " prefix on the inner blob); " +
247
+ "re-key it through the KMS the operator wired as opts.sealer at create() time");
248
+ }
249
+ // Rebuild the EXACT AAD the envelope was sealed under via the
250
+ // module's own _snapshotAad builder — single source of truth with
251
+ // the seal (_persist) + unseal (_unwrapAndVerify) paths. The wrapper
252
+ // carries snapshotId; schemaVersion mirrors the unseal path's
253
+ // `raw.schemaVersion || SCHEMA_VERSION` fallback so an envelope
254
+ // written under an older SCHEMA_VERSION re-keys under its original
255
+ // AAD, not the current one.
256
+ var aad = _snapshotAad({
257
+ snapshotId: snapshotId,
258
+ schemaVersion: raw.schemaVersion != null ? raw.schemaVersion : SCHEMA_VERSION,
259
+ });
260
+ var rekeyed;
261
+ try {
262
+ rekeyed = vaultAad.resealRoot(innerBlob, aad, opts.oldRootJson, opts.newRootJson);
263
+ } catch (e) {
264
+ throw new AgentSnapshotError("agent-snapshot/reseal-failed",
265
+ "reseal: snapshot " + snapshotId + " failed to re-key — the value may not have " +
266
+ "been sealed under oldRootJson + this AAD, or the bytes are tampered (" +
267
+ ((e && e.message) || String(e)) + ")");
268
+ }
269
+ // Re-apply the prefix + preserve every decorative wrapper field
270
+ // (snapshotId / takenAt / tenantId the backend's list() filters on)
271
+ // so the rotation leaves the index untouched.
272
+ var rewritten = Object.assign({}, raw, { sealed: SEALED_PREFIX + rekeyed });
273
+ await backend.put(snapshotId, rewritten);
274
+ resealed += 1;
275
+ }
276
+ return { table: SNAPSHOT_TABLE, resealed: resealed };
277
+ }
278
+
166
279
  // ---- Signer + sealer resolution -------------------------------------------
167
280
 
168
281
  function _resolveSigner(ctx) {
@@ -666,11 +779,35 @@ function _frameworkVersion() {
666
779
  catch (_e) { return "unknown"; }
667
780
  }
668
781
 
782
+ // AAD_ROTATION — the eager-register descriptor the vault-key rotation
783
+ // pipeline consumes. backend "external" because snapshot envelopes live
784
+ // in an operator-supplied backend (not the framework db.enc store), so a
785
+ // `db.enc` scan for the bare "vault.aad:" prefix can't reach them — the
786
+ // pipeline drives the re-key through `reseal` against the same backend.
787
+ // The descriptor's `reseal({ store, oldRootJson, newRootJson })` maps the
788
+ // pipeline's generic `store` term onto this module's `backend` (the
789
+ // snapshot backing store), then defers to the module's own reseal.
669
790
  module.exports = {
670
791
  create: create,
792
+ reseal: reseal,
671
793
  SCHEMA_VERSION: SCHEMA_VERSION,
794
+ SEALED_PREFIX: SEALED_PREFIX,
672
795
  AgentSnapshotError: AgentSnapshotError,
673
796
  guards: {
674
797
  envelope: guardSnapshotEnvelope,
675
798
  },
799
+ AAD_ROTATION: {
800
+ table: SNAPSHOT_TABLE,
801
+ rowIdField: "snapshotId",
802
+ schemaVersion: String(SCHEMA_VERSION),
803
+ backend: "external",
804
+ reseal: function (rotationOpts) {
805
+ rotationOpts = rotationOpts || {};
806
+ return reseal({
807
+ backend: rotationOpts.store || rotationOpts.backend,
808
+ oldRootJson: rotationOpts.oldRootJson,
809
+ newRootJson: rotationOpts.newRootJson,
810
+ });
811
+ },
812
+ },
676
813
  };
@@ -58,6 +58,8 @@ var guardTenantId = require("./guard-tenant-id");
58
58
  var bCrypto = require("./crypto");
59
59
  var agentAudit = require("./agent-audit");
60
60
  var safeJson = require("./safe-json");
61
+ var vaultAad = require("./vault-aad");
62
+ var validateOpts = require("./validate-opts");
61
63
 
62
64
  var audit = lazyRequire(function () { return require("./audit"); });
63
65
  var cryptoField = lazyRequire(function () { return require("./crypto-field"); });
@@ -115,8 +117,12 @@ var CROSS_TENANT_ADMIN_SCOPE = "framework-cross-tenant-admin";
115
117
 
116
118
  // Per-tenant key derivation domain separators. NIST SP 800-108 r1 §5.1
117
119
  // KDF-in-Counter shape — fixed "label" + tenantId-as-salt + purpose-as-
118
- // info. Operator passphrase rotation produces a fresh master, breaking
119
- // every prior tenant ciphertext (operator intent: rotation = re-seal).
120
+ // info. The root secret is the vault master keypair (SHA3-512 of
121
+ // b.vault.getKeysJson()); rotating the vault keypair changes the root,
122
+ // so every tnt-v1: cell sealed under the old root must be re-sealed
123
+ // under the new root. That migration is NOT automatic — it runs via the
124
+ // AAD_ROTATION reseal hook (see `reseal` below), which the vault-key
125
+ // rotation pipeline composes per the explicit-root primitive contract.
120
126
  var TENANT_KDF_LABEL = "blamejs.agent.tenant/v1";
121
127
  // 32 bytes — XChaCha20-Poly1305 key length. Distinct from the audit
122
128
  // truncation buffer so future key-length bumps don't have to chase a
@@ -391,20 +397,32 @@ function _check(ctx, actor, agentTenantId) {
391
397
  // rootKey is SHA3-512(vault.getKeysJson()). Same derivation `b.vault.aad`
392
398
  // uses internally — the vault's master keypair PEM is the secret KDK.
393
399
  // Rotating the vault passphrase / keypair (b.vaultRotate.rotate)
394
- // changes rootKey, which changes every derived tenant key — operator
395
- // intent for rotation IS re-seal.
400
+ // changes rootKey, which changes every derived tenant key — so every
401
+ // prior tnt-v1: cell must be re-sealed old-root -> new-root. The
402
+ // `reseal` hook below (eager-registered via AAD_ROTATION) performs that
403
+ // migration; it uses the explicit-root variant of the tenant-key
404
+ // derivation (rootKeysJson arg) to decrypt under the old root and
405
+ // re-encrypt under the new one within one process.
396
406
  //
397
407
  // `derivedKey` returns a hex-encoded 32-byte key (64 chars) to keep
398
408
  // the wire shape compatible with prior callers. Internal callers that
399
409
  // need the raw key use `_tenantFieldKey` directly.
400
410
 
401
- function _vaultRootBytes() {
402
- // Vault.getKeysJson() throws when the vault hasn't been init'd. That
403
- // is the right secure-by-default posture: tenant-derived keys cannot
404
- // be produced before the operator has bootstrapped the vault. The
405
- // error reaches the caller (sealField / register) so an operator
406
- // mis-ordering boot (start agents before vault.init) sees a clear
407
- // refusal rather than getting weakened-but-deterministic keys.
411
+ function _vaultRootBytes(rootKeysJson) {
412
+ // rootKeysJson lets the vault-key rotation pipeline derive the per-
413
+ // tenant key under a SPECIFIC vault root (old or new keypair) within
414
+ // one process mirrors vault-aad._deriveKey's explicit-root arg. When
415
+ // omitted it reads the live singleton via vault().getKeysJson().
416
+ //
417
+ // getKeysJson() throws when the vault hasn't been init'd. That is the
418
+ // right secure-by-default posture: tenant-derived keys cannot be
419
+ // produced before the operator has bootstrapped the vault. The error
420
+ // reaches the caller (sealField / register) so an operator mis-ordering
421
+ // boot (start agents before vault.init) sees a clear refusal rather
422
+ // than getting weakened-but-deterministic keys.
423
+ if (typeof rootKeysJson === "string" && rootKeysJson.length > 0) {
424
+ return Buffer.from(bCrypto.sha3Hash(rootKeysJson), "hex");
425
+ }
408
426
  var keysJson;
409
427
  try { keysJson = vault().getKeysJson(); }
410
428
  catch (e) {
@@ -415,7 +433,7 @@ function _vaultRootBytes() {
415
433
  return Buffer.from(bCrypto.sha3Hash(keysJson), "hex");
416
434
  }
417
435
 
418
- function _deriveTenantKeyBytes(tenantId, purpose) {
436
+ function _deriveTenantKeyBytes(tenantId, purpose, rootKeysJson) {
419
437
  guardTenantId.validate(tenantId);
420
438
  if (typeof purpose !== "string" || purpose.length === 0) {
421
439
  throw new AgentTenantError("agent-tenant/bad-purpose",
@@ -424,8 +442,9 @@ function _deriveTenantKeyBytes(tenantId, purpose) {
424
442
  // Domain-separated KDF input. NUL separators between fields prevent
425
443
  // (label, tenantId="x\0y", purpose="z") colliding with
426
444
  // (label, tenantId="x", purpose="y\0z") — same byte concatenation,
427
- // different logical context.
428
- var rootBytes = _vaultRootBytes();
445
+ // different logical context. rootKeysJson (optional) pins the vault
446
+ // root for the rotation reseal path; default is the live singleton.
447
+ var rootBytes = _vaultRootBytes(rootKeysJson);
429
448
  var input = Buffer.concat([
430
449
  Buffer.from(TENANT_KDF_LABEL, "utf8"),
431
450
  Buffer.from([0x00]),
@@ -455,7 +474,11 @@ function _deriveTenantKeyBytes(tenantId, purpose) {
455
474
  * `derivedKey(t, "archive-wrap")` is recoverable later from the same
456
475
  * tenant + purpose with no key escrow. Rotating the vault
457
476
  * (`b.vaultRotate.rotate`) changes the root and therefore every
458
- * derived key by design, rotation intent is re-seal.
477
+ * derived key, so every cell sealed under the old root must be
478
+ * re-sealed under the new one. That migration runs through the
479
+ * module's `reseal` hook (eager-registered via `AAD_ROTATION`), not
480
+ * silently on the next read — a value sealed under the old root does
481
+ * not decrypt under the new root until the rotation pipeline walks it.
459
482
  *
460
483
  * Throws if the vault has not been initialized (keys cannot be derived
461
484
  * before bootstrap) or if `purpose` is empty. This is the same
@@ -552,12 +575,14 @@ function _auditFor(ctx, tenantId) {
552
575
 
553
576
  var TENANT_FIELD_PREFIX = "tnt-v1:";
554
577
 
555
- function _tenantFieldKey(tenantId, table) {
578
+ function _tenantFieldKey(tenantId, table, rootKeysJson) {
556
579
  // 32-byte symmetric key for XChaCha20-Poly1305. _deriveTenantKeyBytes
557
580
  // returns the raw key bound to the vault master + tenantId + purpose
558
581
  // — see the commentary above _derivedKey for the threat
559
582
  // model that drove this away from public-input-only derivation.
560
- return _deriveTenantKeyBytes(tenantId, "cryptoField:" + table);
583
+ // rootKeysJson (optional) pins a specific vault root for the rotation
584
+ // reseal path; default is the live singleton.
585
+ return _deriveTenantKeyBytes(tenantId, "cryptoField:" + table, rootKeysJson);
561
586
  }
562
587
 
563
588
  function _tenantFieldAad(tenantId, table, field) {
@@ -602,6 +627,25 @@ function _unsealField(tenantId, table, field, ciphertext) {
602
627
  return plain.toString("utf8");
603
628
  }
604
629
 
630
+ // Explicit-root re-seal of a single tnt-v1: cell, old root -> new root,
631
+ // under the SAME (tenantId, table, field) AAD. Decrypts with the key
632
+ // derived from oldRootJson and re-encrypts under newRootJson — the
633
+ // XChaCha20-Poly1305 tag refuses any cell that wasn't sealed under the
634
+ // old root + this AAD (CWE-345 / CWE-441). Values without the prefix
635
+ // (plaintext columns, already-rotated cells) pass through untouched.
636
+ function _resealTenantCell(tenantId, table, field, ciphertext, oldRootJson, newRootJson) {
637
+ if (typeof ciphertext !== "string" || ciphertext.indexOf(TENANT_FIELD_PREFIX) !== 0) {
638
+ return ciphertext;
639
+ }
640
+ var packed = Buffer.from(ciphertext.slice(TENANT_FIELD_PREFIX.length), "base64");
641
+ var aad = _tenantFieldAad(tenantId, table, field);
642
+ var oldKey = _tenantFieldKey(tenantId, table, oldRootJson);
643
+ var plain = bCrypto.decryptPacked(packed, oldKey, aad);
644
+ var newKey = _tenantFieldKey(tenantId, table, newRootJson);
645
+ var reSealed = bCrypto.encryptPacked(plain, newKey, aad);
646
+ return TENANT_FIELD_PREFIX + reSealed.toString("base64");
647
+ }
648
+
605
649
  function _sealRowForTenant(tenantId, table, row) {
606
650
  // Adopts the existing b.cryptoField table schema (sealedFields) but
607
651
  // routes each field through the per-tenant AEAD instead of the
@@ -663,6 +707,113 @@ function _unsealRowForTenant(ctx, tenantId, table, row) {
663
707
  return out;
664
708
  }
665
709
 
710
+ // ---- Vault-key rotation: re-seal hook -------------------------------------
711
+ //
712
+ // Two root-derived families live in this module, both keyed off the vault
713
+ // master keypair (SHA3-512 of b.vault.getKeysJson()):
714
+ //
715
+ // 1. The registry table "agent_tenant_registry" — a b.cryptoField
716
+ // {aad:true} table whose `metadata` column holds vault.aad: cells.
717
+ // Re-sealed old-root -> new-root via vaultAad.resealRoot, with the
718
+ // AAD tuple rebuilt by cryptoField._aadParts (the SAME builder the
719
+ // seal side uses — single source of truth, no drift).
720
+ //
721
+ // 2. The tnt-v1: per-tenant sealed cells written by sealField /
722
+ // sealRowForTenant. Re-sealed via _resealTenantCell, which derives
723
+ // the per-tenant XChaCha20-Poly1305 key under each root explicitly
724
+ // and re-binds the (tenantId|table|field) AAD.
725
+ //
726
+ // Both descriptors export a `reseal({ store, oldRootJson, newRootJson })`
727
+ // hook. The vault-key rotation pipeline eager-registers every module's
728
+ // AAD_ROTATION descriptor(s) and calls each reseal with the operator-
729
+ // supplied backing store; without this hook, a keypair rotation would
730
+ // orphan every prior cell (decryptable under neither root). oldRootJson /
731
+ // newRootJson are b.vault.getKeysJson() output for the two keypairs.
732
+
733
+ var REGISTRY_SCHEMA_VERSION = "1";
734
+
735
+ // Rebuild the registry's AAD tuple via the cryptoField seal-side builder
736
+ // so the rotate side can never drift from the seal side. The schema
737
+ // shape cryptoField._aadParts reads is { rowIdField, schemaVersion }.
738
+ function _registryAadFor(row) {
739
+ return cryptoField()._aadParts(
740
+ { rowIdField: "tenantId", schemaVersion: REGISTRY_SCHEMA_VERSION },
741
+ SEAL_TABLE, "metadata", row);
742
+ }
743
+
744
+ // Re-seal the registry table's vault.aad: metadata cells. `store` is the
745
+ // operator's backing store for SEAL_TABLE, exposing list() ->
746
+ // [{ tenantId, metadata, ... }] and set(tenantId, row). Each metadata
747
+ // cell that is vault.aad-sealed is re-sealed old-root -> new-root under
748
+ // the SAME (table, tenantId, "metadata", schemaVersion) AAD.
749
+ function _resealRegistry(args) {
750
+ var store = args && args.store;
751
+ validateOpts.requireMethods(store, ["list", "set"],
752
+ "reseal: store for the '" + SEAL_TABLE + "' table",
753
+ AgentTenantError, "agent-tenant/bad-reseal-store");
754
+ validateOpts.requireNonEmptyString(args.oldRootJson,
755
+ "reseal: oldRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
756
+ validateOpts.requireNonEmptyString(args.newRootJson,
757
+ "reseal: newRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
758
+ return Promise.resolve(store.list()).then(function (rows) {
759
+ rows = Array.isArray(rows) ? rows : [];
760
+ var resealed = 0;
761
+ var chain = Promise.resolve();
762
+ rows.forEach(function (row) {
763
+ if (!row || row.tenantId == null) return;
764
+ var cell = row.metadata;
765
+ if (!vaultAad.isAadSealed(cell)) return; // plaintext / already-migrated
766
+ var aad = _registryAadFor(row);
767
+ var next = vaultAad.resealRoot(cell, aad, args.oldRootJson, args.newRootJson);
768
+ var updated = Object.assign({}, row, { metadata: next });
769
+ resealed += 1;
770
+ chain = chain.then(function () { return store.set(row.tenantId, updated); });
771
+ });
772
+ return chain.then(function () {
773
+ return { table: SEAL_TABLE, resealed: resealed };
774
+ });
775
+ });
776
+ }
777
+
778
+ // Re-seal the tnt-v1: per-tenant sealed cells. `store` is the operator's
779
+ // backing store for every table that carries tnt-v1: columns, exposing
780
+ // list() -> [{ tenantId, table, field, value, _id? }] (one entry per
781
+ // sealed cell, carrying the context needed to rebuild AAD + key) and
782
+ // write(cell, newValue) to persist the re-sealed value. Plaintext /
783
+ // already-rotated values pass through untouched.
784
+ function _resealTenantCells(args) {
785
+ var store = args && args.store;
786
+ validateOpts.requireMethods(store, ["list", "write"],
787
+ "reseal: store for tnt-v1: cells",
788
+ AgentTenantError, "agent-tenant/bad-reseal-store");
789
+ validateOpts.requireNonEmptyString(args.oldRootJson,
790
+ "reseal: oldRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
791
+ validateOpts.requireNonEmptyString(args.newRootJson,
792
+ "reseal: newRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
793
+ return Promise.resolve(store.list()).then(function (cells) {
794
+ cells = Array.isArray(cells) ? cells : [];
795
+ var resealed = 0;
796
+ var chain = Promise.resolve();
797
+ cells.forEach(function (cell) {
798
+ if (!cell || cell.tenantId == null ||
799
+ typeof cell.table !== "string" || typeof cell.field !== "string") {
800
+ return;
801
+ }
802
+ var value = cell.value;
803
+ if (typeof value !== "string" || value.indexOf(TENANT_FIELD_PREFIX) !== 0) {
804
+ return; // not a tnt-v1: cell — leave untouched
805
+ }
806
+ var next = _resealTenantCell(cell.tenantId, cell.table, cell.field,
807
+ value, args.oldRootJson, args.newRootJson);
808
+ resealed += 1;
809
+ chain = chain.then(function () { return store.write(cell, next); });
810
+ });
811
+ return chain.then(function () {
812
+ return { table: TENANT_FIELD_PREFIX, resealed: resealed };
813
+ });
814
+ });
815
+ }
816
+
666
817
  // ---- Destroy preconditions ------------------------------------------------
667
818
 
668
819
  function _checkDestroyPreconditions(args, tenantId) {
@@ -703,11 +854,36 @@ function _inMemoryBackend() {
703
854
  };
704
855
  }
705
856
 
857
+ // Vault-key rotation descriptors — the rotation pipeline eager-registers
858
+ // these and calls each reseal({ store, oldRootJson, newRootJson }) to
859
+ // re-seal this module's two root-derived families old-root -> new-root.
860
+ // backend: "external" — the cells live in the operator's backing store
861
+ // (the registry backend + the operator's tenant data tables), not in the
862
+ // framework's at-rest SQLite, so the rotation pipeline cannot walk them
863
+ // from the data directory alone; the operator supplies the store.
864
+ var AAD_ROTATION = [
865
+ {
866
+ table: SEAL_TABLE,
867
+ rowIdField: "tenantId",
868
+ schemaVersion: REGISTRY_SCHEMA_VERSION,
869
+ backend: "external",
870
+ reseal: _resealRegistry,
871
+ },
872
+ {
873
+ table: TENANT_FIELD_PREFIX, // prefix family, not a single SQL table
874
+ rowIdField: "tenantId",
875
+ schemaVersion: "v1", // tnt-v1: ciphertext shape version
876
+ backend: "external",
877
+ reseal: _resealTenantCells,
878
+ },
879
+ ];
880
+
706
881
  module.exports = {
707
882
  create: create,
708
883
  derivedKey: _derivedKey,
709
884
  CROSS_TENANT_ADMIN_SCOPE: CROSS_TENANT_ADMIN_SCOPE,
710
885
  AgentTenantError: AgentTenantError,
886
+ AAD_ROTATION: AAD_ROTATION,
711
887
  guards: {
712
888
  tenantId: guardTenantId,
713
889
  },