@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
@@ -148,40 +148,52 @@ function _validateDcql(dcql) {
148
148
  }
149
149
 
150
150
  /**
151
- * Walk the path against the resolved-claim object the SD-JWT VC
152
- * verifier produced. Returns { found, value }.
153
- * path = ["address", "country"] → claims.address.country
154
- * path = ["array", 0] → claims.array[0]
155
- * null = "any element" (DCQL §6.4.2 array path semantics) — for
156
- * v1-defensible we don't dispatch on null; refuse with a clear
157
- * error so the operator knows the gap.
151
+ * Walk a DCQL claims path pointer (OpenID4VP 1.0 §7.1.1) against the
152
+ * resolved-claim object the SD-JWT VC verifier produced, applying
153
+ * `leafPredicate` at the terminal node and returning a boolean:
154
+ * string → object property (["address", "country"])
155
+ * integer array index (["array", 0])
156
+ * null → all elements of the array at this depth (recurse; the
157
+ * match is an existence check over the candidate leaves)
158
+ * A `null` segment on a non-array node — like an integer index into a
159
+ * non-array or a string key into an array — is a NON-MATCH, not an
160
+ * error: this walks holder credential data, not operator config, so a
161
+ * structural mismatch fails the match cleanly (rule §5 defensive
162
+ * request-shape reader tier) rather than throwing and crashing the
163
+ * verify request.
158
164
  */
159
- function _resolvePath(claims, path) {
160
- var node = claims;
161
- for (var i = 0; i < path.length; i++) {
162
- var seg = path[i];
163
- if (seg === null) {
164
- // DCQL §6.4.2: null means "any element of the array at this
165
- // depth". Not in v1 refuse loudly so it doesn't silently
166
- // match nothing.
167
- throw new AuthError("auth-oid4vp/null-path-segment-not-supported",
168
- "DCQL: null path segment (any-element) not supported in v1; supply a numeric index");
169
- }
170
- if (node === undefined || node === null) return { found: false, value: undefined };
171
- node = node[seg];
165
+ function _walkPath(node, path, idx, leafPredicate) {
166
+ if (idx === path.length) return leafPredicate(node);
167
+ if (node === undefined || node === null) return false;
168
+ var seg = path[idx];
169
+ if (seg === null) {
170
+ if (!Array.isArray(node)) return false;
171
+ return node.some(function (el) { return _walkPath(el, path, idx + 1, leafPredicate); });
172
+ }
173
+ if (typeof seg === "number") {
174
+ if (!Array.isArray(node)) return false;
175
+ return _walkPath(node[seg], path, idx + 1, leafPredicate);
172
176
  }
173
- return { found: node !== undefined, value: node };
177
+ // string segment selects an object property; a string key into an
178
+ // array is a non-match (use a null wildcard or integer index instead).
179
+ if (Array.isArray(node)) return false;
180
+ return _walkPath(node[seg], path, idx + 1, leafPredicate);
174
181
  }
175
182
 
176
183
  function _matchClaim(claims, claimQuery) {
177
- var resolved = _resolvePath(claims, claimQuery.path);
178
- if (!resolved.found) return false;
179
- if (claimQuery.values && claimQuery.values.length > 0) {
180
- return claimQuery.values.some(function (v) {
181
- return v === resolved.value || JSON.stringify(v) === JSON.stringify(resolved.value);
182
- });
184
+ var values = claimQuery.values;
185
+ var leafPredicate;
186
+ if (values && values.length > 0) {
187
+ leafPredicate = function (leaf) {
188
+ if (leaf === undefined) return false;
189
+ return values.some(function (v) {
190
+ return v === leaf || JSON.stringify(v) === JSON.stringify(leaf);
191
+ });
192
+ };
193
+ } else {
194
+ leafPredicate = function (leaf) { return leaf !== undefined; };
183
195
  }
184
- return true;
196
+ return _walkPath(claims, claimQuery.path, 0, leafPredicate);
185
197
  }
186
198
 
187
199
  function _matchCredentialQuery(presentation, query) {
@@ -219,6 +231,13 @@ function _matchCredentialQuery(presentation, query) {
219
231
  * implement their own verifier transport call this directly after
220
232
  * SD-JWT VC verification.
221
233
  *
234
+ * Claim path pointers follow OpenID4VP 1.0 §7.1.1: a string segment
235
+ * selects an object property, a non-negative integer indexes an array,
236
+ * and a `null` segment matches any element of the array at that depth
237
+ * (e.g. `["degrees", null, "type"]` matches the `type` claim of any
238
+ * element in the `degrees` array). A `null` segment applied to a
239
+ * non-array node is a non-match.
240
+ *
222
241
  * @example
223
242
  * var match = b.auth.oid4vp.matchDcql([
224
243
  * { id: "id-card", format: "vc+sd-jwt", claims: { vct: "...", given_name: "Alice" } }
@@ -52,6 +52,7 @@ var safeAsync = require("./safe-async");
52
52
  var safeJson = require("./safe-json");
53
53
  var safeSql = require("./safe-sql");
54
54
  var safeUrl = require("./safe-url");
55
+ var validateOpts = require("./validate-opts");
55
56
  var { FrameworkError, ClusterError } = require("./framework-error");
56
57
 
57
58
  // Lazy: vault → db → cluster forms a load-time chain, and external-db is
@@ -99,6 +100,15 @@ var configuredDialect = null;
99
100
  // (or external observer) can resolve "where is the current leader?"
100
101
  // via cluster.currentLeader() / cluster.discoveryHandler().
101
102
  var configuredEndpoint = null;
103
+ // Operator declaration that this node's vault keypair legitimately
104
+ // changed via a key rotation (b.vault.rotate). When set, a fingerprint
105
+ // that differs from the canonical cluster-state row is ADOPTED (the row
106
+ // advances to the new fingerprint + bumps the rotation epoch) instead
107
+ // of FATAL-refusing boot. Unset (the default) keeps the strict
108
+ // drift-refusal posture for the UNexpected mismatch. See
109
+ // _checkVaultKeyConsistency for the consistency model.
110
+ var configuredAcceptRotation = false;
111
+ var configuredExpectedVaultKeyFp = null;
102
112
 
103
113
  var log = boot("cluster");
104
114
 
@@ -142,6 +152,16 @@ function _emitTransition(kind, detail) {
142
152
  * outside `leader` / `follower`, and on a chain or vault-key mismatch
143
153
  * that would let this node corrupt cluster state.
144
154
  *
155
+ * After a vault-key rotation (`b.vault.rotate`) the public-key
156
+ * fingerprint changes, so the canonical cluster-state row no longer
157
+ * matches and every node would otherwise refuse boot with
158
+ * `VAULT_KEY_DRIFT`. Pass `acceptVaultKeyRotation: true` to declare the
159
+ * change legitimate: the node advances the canonical fingerprint and
160
+ * bumps a rotation epoch instead of refusing. `expectedVaultKeyFp`
161
+ * narrows the acceptance to a single blessed fingerprint so a typo'd /
162
+ * stale key file is still caught. The strict cross-node drift refusal
163
+ * stays in force whenever the rotation is NOT declared.
164
+ *
145
165
  * @opts
146
166
  * nodeId: string, // required; stable identity
147
167
  * role: "leader"|"follower",
@@ -152,6 +172,11 @@ function _emitTransition(kind, detail) {
152
172
  * provider: object, // custom election provider
153
173
  * externalDbBackend: object, // required when no custom provider
154
174
  * dialect: "postgres"|"sqlite"|"mysql",
175
+ * acceptVaultKeyRotation: boolean, // adopt a rotated vault-key
176
+ * // fingerprint instead of
177
+ * // refusing boot on mismatch
178
+ * expectedVaultKeyFp: string, // optional; bless ONLY this
179
+ * // post-rotation fingerprint
155
180
  * onTransition: function (event),
156
181
  *
157
182
  * @example
@@ -227,6 +252,31 @@ async function init(opts) {
227
252
  configuredEndpoint = null;
228
253
  }
229
254
 
255
+ // Vault-key rotation acceptance (config-time tier: THROW on bad
256
+ // input). acceptVaultKeyRotation is a boolean declaration; an
257
+ // expectedVaultKeyFp without it is a misconfiguration (the operator
258
+ // blessed a fingerprint but never enabled adoption).
259
+ validateOpts.optionalBoolean(opts.acceptVaultKeyRotation,
260
+ "cluster.init({ acceptVaultKeyRotation })", ClusterError, "INVALID_CONFIG");
261
+ configuredAcceptRotation = opts.acceptVaultKeyRotation === true;
262
+ if (opts.expectedVaultKeyFp !== undefined) {
263
+ if (typeof opts.expectedVaultKeyFp !== "string" ||
264
+ !/^[0-9a-f]{128}$/.test(opts.expectedVaultKeyFp)) {
265
+ throw _err("INVALID_CONFIG",
266
+ "cluster.init({ expectedVaultKeyFp }) must be a 128-char " +
267
+ "lowercase-hex SHA3-512 fingerprint (b.vault rotation output)", true);
268
+ }
269
+ if (!configuredAcceptRotation) {
270
+ throw _err("INVALID_CONFIG",
271
+ "cluster.init({ expectedVaultKeyFp }) requires " +
272
+ "acceptVaultKeyRotation: true — blessing a fingerprint without " +
273
+ "enabling adoption has no effect", true);
274
+ }
275
+ configuredExpectedVaultKeyFp = opts.expectedVaultKeyFp;
276
+ } else {
277
+ configuredExpectedVaultKeyFp = null;
278
+ }
279
+
230
280
  if (typeof opts.onTransition === "function") {
231
281
  transitionHandlers.push(opts.onTransition);
232
282
  }
@@ -433,6 +483,49 @@ function _vaultKeyFingerprint() {
433
483
  keys.ecPublicKey);
434
484
  }
435
485
 
486
+ // Idempotent migration for the rotationEpoch column. cluster-provider-db
487
+ // ensureSchema creates _blamejs_cluster_state without it (the column was
488
+ // added when rotation-epoch acceptance landed); ADD COLUMN here keeps the
489
+ // path version-agnostic the same way the provider migrates the leader
490
+ // row's `endpoint` column. The only expected failure is "column already
491
+ // exists," which is swallowed. SQLite / MySQL don't take a DEFAULT on a
492
+ // non-constant, so the column is nullable and treated as epoch 0 when
493
+ // absent on legacy rows.
494
+ async function _ensureRotationEpochColumn() {
495
+ try {
496
+ await externalDb().query(
497
+ "ALTER TABLE _blamejs_cluster_state ADD COLUMN rotationEpoch BIGINT",
498
+ [],
499
+ { backend: configuredExternalDbBackend }
500
+ );
501
+ } catch (_e) { /* column already exists (or table absent — caught upstream) */ }
502
+ }
503
+
504
+ // Consistency model (CWE-345 binding-integrity for sealed columns):
505
+ //
506
+ // Every node fingerprints its vault PUBLIC keys (SHA3-512, one-way) and
507
+ // the cluster agrees on ONE canonical fingerprint stored in
508
+ // _blamejs_cluster_state. A node holding a different key seals new
509
+ // writes the rest of the cluster can't unseal — silent corruption — so
510
+ // an UNDECLARED mismatch fails closed (FATAL: VAULT_KEY_DRIFT).
511
+ //
512
+ // A vault-key rotation (b.vault.rotate, lib/vault/rotate.js) legitimately
513
+ // changes the public-key fingerprint on EVERY node. The rotation only
514
+ // re-seals the local dataDir; it does not touch the external coordination
515
+ // row, so the canonical fingerprint goes stale and every node would
516
+ // refuse boot. acceptVaultKeyRotation: true is the operator's signed-off
517
+ // declaration "this change is a rotation, not drift": the booting node
518
+ // ADVANCES the canonical row to its own fingerprint and bumps a
519
+ // monotonic rotationEpoch. expectedVaultKeyFp narrows the adoption to a
520
+ // single blessed fingerprint so a stale / wrong key file is still
521
+ // refused. The strict refusal is unchanged when no rotation is declared,
522
+ // which is exactly the cross-node drift case the check defends.
523
+ //
524
+ // The epoch is observability + a future-replay guard, not an auth
525
+ // boundary — the auth boundary is the operator's deliberate opt
526
+ // (acceptVaultKeyRotation) plus the optional fingerprint allowlist. A
527
+ // forged row can only ever cost a single declared boot, and a genuinely
528
+ // different key would still fail every sealed read.
436
529
  async function _checkVaultKeyConsistency() {
437
530
  var localFp = _vaultKeyFingerprint();
438
531
  if (localFp === null) {
@@ -469,10 +562,15 @@ async function _checkVaultKeyConsistency() {
469
562
  throw e;
470
563
  }
471
564
 
565
+ // Bring the rotationEpoch column into existence (idempotent). The INSERT
566
+ // above already proved the table is present, so a real ALTER failure
567
+ // here is "column exists" and is swallowed.
568
+ await _ensureRotationEpochColumn();
569
+
472
570
  // Read whatever fingerprint is canonical (ours if first boot,
473
571
  // someone else's if we lost the race or are joining an existing cluster).
474
572
  var rows = await externalDb().query(
475
- "SELECT vaultKeyFp, recordedByNode, recordedAt FROM _blamejs_cluster_state " +
573
+ "SELECT vaultKeyFp, recordedByNode, recordedAt, rotationEpoch FROM _blamejs_cluster_state " +
476
574
  "WHERE scope = 'state'",
477
575
  [],
478
576
  { backend: configuredExternalDbBackend }
@@ -486,22 +584,92 @@ async function _checkVaultKeyConsistency() {
486
584
  true);
487
585
  }
488
586
  var canonical = rows.rows[0];
587
+ var fpPrefix = C.BYTES.bytes(16);
489
588
  if (canonical.vaultKeyFp !== localFp) {
490
- var fpPrefix = C.BYTES.bytes(16);
491
- throw _err("VAULT_KEY_DRIFT",
492
- "FATAL: vault-key drift detected. " +
493
- "local node: " + nodeId +
494
- "; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
495
- "; canonical recorded by: " + canonical.recordedByNode +
496
- "; canonical fingerprint: " + canonical.vaultKeyFp.slice(0, fpPrefix) + "…" +
497
- ". This node holds a DIFFERENT vault key than the rest of the " +
498
- "cluster. Sealed-column writes from this node would be unreadable " +
499
- "by the others (and vice versa). Restore the same vault key file " +
500
- "before booting this node into the cluster.",
501
- true);
589
+ // Mismatch. Two readings: a legitimate vault-key rotation the
590
+ // operator has declared, or genuine cross-node drift. Without the
591
+ // declaration, always fail closed — sealed-column corruption is the
592
+ // worse outcome.
593
+ if (!configuredAcceptRotation) {
594
+ throw _err("VAULT_KEY_DRIFT",
595
+ "FATAL: vault-key drift detected. " +
596
+ "local node: " + nodeId +
597
+ "; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
598
+ "; canonical recorded by: " + canonical.recordedByNode +
599
+ "; canonical fingerprint: " + canonical.vaultKeyFp.slice(0, fpPrefix) + "…" +
600
+ ". This node holds a DIFFERENT vault key than the rest of the " +
601
+ "cluster. Sealed-column writes from this node would be unreadable " +
602
+ "by the others (and vice versa). If the key changed via " +
603
+ "b.vault.rotate, re-init with acceptVaultKeyRotation: true to " +
604
+ "advance the cluster's recorded fingerprint; otherwise restore the " +
605
+ "same vault key file before booting this node into the cluster.",
606
+ true);
607
+ }
608
+ // Rotation declared. If the operator blessed a specific fingerprint,
609
+ // the LOCAL key must match it — this rejects a stale / wrong key file
610
+ // that happens to differ from canonical for the wrong reason.
611
+ if (configuredExpectedVaultKeyFp && configuredExpectedVaultKeyFp !== localFp) {
612
+ throw _err("VAULT_KEY_ROTATION_MISMATCH",
613
+ "FATAL: acceptVaultKeyRotation is set but this node's vault-key " +
614
+ "fingerprint does not match the blessed expectedVaultKeyFp. " +
615
+ "local node: " + nodeId +
616
+ "; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
617
+ "; expected fingerprint: " + configuredExpectedVaultKeyFp.slice(0, fpPrefix) + "…" +
618
+ ". This node is NOT holding the rotated key the operator approved. " +
619
+ "Restore the post-rotation vault key file (or correct " +
620
+ "expectedVaultKeyFp) before booting this node into the cluster.",
621
+ true);
622
+ }
623
+ // Adopt: advance the canonical row to the new fingerprint and bump
624
+ // the monotonic rotation epoch. The UPDATE is gated on the OLD
625
+ // fingerprint so two nodes adopting concurrently converge on a single
626
+ // advance (the loser's WHERE matches nothing and it re-reads the
627
+ // already-advanced row below).
628
+ var priorEpoch = (canonical.rotationEpoch != null) ? Number(canonical.rotationEpoch) : 0;
629
+ if (!isFinite(priorEpoch) || priorEpoch < 0) priorEpoch = 0;
630
+ var nextEpoch = priorEpoch + 1;
631
+ await externalDb().query(
632
+ "UPDATE _blamejs_cluster_state SET " +
633
+ " vaultKeyFp = " + (ph ? "$1" : "?") + ", " +
634
+ " recordedAt = " + (ph ? "$2" : "?") + ", " +
635
+ " recordedByNode = " + (ph ? "$3" : "?") + ", " +
636
+ " rotationEpoch = " + (ph ? "$4" : "?") + " " +
637
+ "WHERE scope = 'state' AND vaultKeyFp = " + (ph ? "$5" : "?"),
638
+ [localFp, nowMs, nodeId, nextEpoch, canonical.vaultKeyFp],
639
+ { backend: configuredExternalDbBackend }
640
+ );
641
+ // Re-read so the post-adopt state reflects whoever actually won the
642
+ // advance (this node, or a peer that adopted the SAME rotated key a
643
+ // beat earlier). A surviving mismatch here means the row now carries a
644
+ // fingerprint that is neither the old one nor ours — a real drift that
645
+ // the rotation declaration does not cover, so fail closed.
646
+ var after = await externalDb().query(
647
+ "SELECT vaultKeyFp, recordedByNode, rotationEpoch FROM _blamejs_cluster_state " +
648
+ "WHERE scope = 'state'",
649
+ [],
650
+ { backend: configuredExternalDbBackend }
651
+ );
652
+ var post = (after.rows && after.rows[0]) || canonical;
653
+ if (post.vaultKeyFp !== localFp) {
654
+ throw _err("VAULT_KEY_DRIFT",
655
+ "FATAL: vault-key drift detected after rotation-accept. " +
656
+ "local node: " + nodeId +
657
+ "; local fingerprint: " + localFp.slice(0, fpPrefix) + "…" +
658
+ "; canonical fingerprint: " + post.vaultKeyFp.slice(0, fpPrefix) + "…" +
659
+ ". A concurrent node advanced the cluster to a DIFFERENT key than " +
660
+ "this node holds — the declared rotation does not cover this " +
661
+ "fingerprint. Restore the agreed post-rotation vault key file.",
662
+ true);
663
+ }
664
+ log("cluster vault-key rotation accepted (fingerprint " +
665
+ localFp.slice(0, fpPrefix) + "… epoch " +
666
+ (post.rotationEpoch != null ? Number(post.rotationEpoch) : nextEpoch) +
667
+ ", recorded by " + post.recordedByNode + ")");
668
+ return;
502
669
  }
503
670
  log("cluster vault-key consistency ok (fingerprint " +
504
- localFp.slice(0, C.BYTES.bytes(16)) + "… recorded by " + canonical.recordedByNode + ")");
671
+ localFp.slice(0, fpPrefix) + "… recorded by " + canonical.recordedByNode +
672
+ (canonical.rotationEpoch != null ? ", epoch " + Number(canonical.rotationEpoch) : "") + ")");
505
673
  }
506
674
 
507
675
  async function _tryAcquire() {
@@ -967,6 +1135,8 @@ async function shutdown() {
967
1135
  configuredExternalDbBackend = null;
968
1136
  configuredDialect = null;
969
1137
  configuredEndpoint = null;
1138
+ configuredAcceptRotation = false;
1139
+ configuredExpectedVaultKeyFp = null;
970
1140
  transitionHandlers = [];
971
1141
  // nodeId is preserved post-shutdown so audit metadata still reflects
972
1142
  // who this process was; cleared only by _resetForTest.
@@ -988,6 +1158,8 @@ function _resetForTest() {
988
1158
  configuredExternalDbBackend = null;
989
1159
  configuredDialect = null;
990
1160
  configuredEndpoint = null;
1161
+ configuredAcceptRotation = false;
1162
+ configuredExpectedVaultKeyFp = null;
991
1163
  transitionHandlers = [];
992
1164
  }
993
1165
 
@@ -67,6 +67,13 @@ function fromCp(cp) { return String.fromCharCode(cp); }
67
67
  var BIDI_RANGES = [0x200E, 0x200F, 0x061C, [0x202A, 0x202E], [0x2066, 0x2069]];
68
68
  var C0_CTRL_RANGES = [[0x0000, 0x0008], 0x000B, 0x000C, [0x000E, 0x001F]];
69
69
  var ZERO_WIDTH_RANGES = [0x00AD, [0x200B, 0x200D], 0x2060, 0xFEFF];
70
+ // TAG_RANGES — Unicode Tags block U+E0000..U+E007F. TAG U+E0001 plus
71
+ // the printable-ASCII tag map U+E0020..U+E007E carry an invisible copy
72
+ // of an ASCII instruction that renders as nothing but is read verbatim
73
+ // by an LLM tokenizer — the "ASCII smuggling" / Unicode-Tags prompt-
74
+ // injection class. Stripping the block from untrusted prompt segments
75
+ // removes the hidden instruction channel.
76
+ var TAG_RANGES = [[0xE0000, 0xE007F]];
70
77
 
71
78
  // allow:dynamic-regex — codepoints from BIDI_RANGES literal table
72
79
  var BIDI_RE = new RegExp("[" + charClass(BIDI_RANGES) + "]");
@@ -82,6 +89,12 @@ var ZERO_WIDTH_RE = new RegExp("[" + charClass(ZERO_WIDTH_RANGES) + "]");
82
89
  var ZW_RE_G = new RegExp("[" + charClass(ZERO_WIDTH_RANGES) + "]", "g");
83
90
  // allow:dynamic-regex — single literal codepoint U+0000
84
91
  var NULL_RE_G = new RegExp(hex4(0x0000), "g");
92
+ // Unicode Tags block (U+E0000..U+E007F). The \u{...} escape keeps this
93
+ // source file pure ASCII (the codepoint-class purity invariant) while
94
+ // matching astral codepoints — hence the `u` flag. Global form for the
95
+ // strip path.
96
+ var TAG_RE = /[\u{E0000}-\u{E007F}]/u;
97
+ var TAG_RE_G = /[\u{E0000}-\u{E007F}]/gu;
85
98
 
86
99
  var NULL_BYTE = fromCp(0x0000);
87
100
  var BOM_CHAR = fromCp(0xFEFF);
@@ -225,6 +238,7 @@ function assertNoCharThreats(text, opts, errorFactory, codePrefix) {
225
238
  // opts.controlPolicy === "strip" -> strip C0 controls
226
239
  // opts.nullBytePolicy === "strip" -> strip null bytes
227
240
  // opts.zeroWidthPolicy === "strip" -> strip zero-widths
241
+ // opts.tagsPolicy === "strip" -> strip Unicode Tags (U+E0000..)
228
242
  // Returns the cleaned string. Used by every guard's sanitize path so
229
243
  // each one doesn't reinvent the same sequence of replace() calls.
230
244
  function applyCharStripPolicies(text, opts) {
@@ -234,6 +248,7 @@ function applyCharStripPolicies(text, opts) {
234
248
  if (opts && opts.controlPolicy === "strip") out = out.replace(C0_CTRL_RE_G, "");
235
249
  if (opts && opts.nullBytePolicy === "strip") out = out.replace(NULL_RE_G, "");
236
250
  if (opts && opts.zeroWidthPolicy === "strip") out = out.replace(ZW_RE_G, "");
251
+ if (opts && opts.tagsPolicy === "strip") out = out.replace(TAG_RE_G, "");
237
252
  return out;
238
253
  }
239
254
 
@@ -244,6 +259,7 @@ module.exports = {
244
259
  BIDI_RANGES: BIDI_RANGES,
245
260
  C0_CTRL_RANGES: C0_CTRL_RANGES,
246
261
  ZERO_WIDTH_RANGES: ZERO_WIDTH_RANGES,
262
+ TAG_RANGES: TAG_RANGES,
247
263
  BIDI_RE: BIDI_RE,
248
264
  BIDI_RE_G: BIDI_RE_G,
249
265
  C0_CTRL_RE: C0_CTRL_RE,
@@ -251,6 +267,8 @@ module.exports = {
251
267
  ZERO_WIDTH_RE: ZERO_WIDTH_RE,
252
268
  ZW_RE_G: ZW_RE_G,
253
269
  NULL_RE_G: NULL_RE_G,
270
+ TAG_RE: TAG_RE,
271
+ TAG_RE_G: TAG_RE_G,
254
272
  NULL_BYTE: NULL_BYTE,
255
273
  BOM_CHAR: BOM_CHAR,
256
274
  applyCharStripPolicies: applyCharStripPolicies,