@blamejs/core 0.14.27 → 0.15.1

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 (134) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/ai-content-detect.js +9 -10
  5. package/lib/api-key.js +158 -77
  6. package/lib/atomic-file.js +29 -1
  7. package/lib/audit-chain.js +47 -11
  8. package/lib/audit-sign.js +77 -2
  9. package/lib/audit-tools.js +79 -51
  10. package/lib/audit.js +228 -100
  11. package/lib/backup/index.js +13 -10
  12. package/lib/break-glass.js +202 -144
  13. package/lib/cache.js +174 -105
  14. package/lib/chain-writer.js +38 -16
  15. package/lib/cli.js +19 -14
  16. package/lib/cluster-provider-db.js +130 -104
  17. package/lib/cluster-storage.js +119 -22
  18. package/lib/cluster.js +119 -71
  19. package/lib/compliance.js +22 -0
  20. package/lib/consent.js +82 -29
  21. package/lib/constants.js +16 -11
  22. package/lib/crypto-field.js +387 -91
  23. package/lib/db-declare-row-policy.js +35 -22
  24. package/lib/db-file-lifecycle.js +3 -2
  25. package/lib/db-query.js +517 -256
  26. package/lib/db-schema.js +209 -44
  27. package/lib/db.js +202 -95
  28. package/lib/external-db-migrate.js +229 -139
  29. package/lib/external-db.js +25 -15
  30. package/lib/framework-error.js +11 -0
  31. package/lib/framework-files.js +73 -0
  32. package/lib/framework-schema.js +695 -394
  33. package/lib/gate-contract.js +596 -1
  34. package/lib/guard-agent-registry.js +26 -44
  35. package/lib/guard-all.js +1 -0
  36. package/lib/guard-auth.js +42 -112
  37. package/lib/guard-cidr.js +33 -154
  38. package/lib/guard-csv.js +46 -113
  39. package/lib/guard-domain.js +34 -157
  40. package/lib/guard-dsn.js +27 -43
  41. package/lib/guard-email.js +47 -69
  42. package/lib/guard-envelope.js +19 -32
  43. package/lib/guard-event-bus-payload.js +24 -42
  44. package/lib/guard-event-bus-topic.js +25 -43
  45. package/lib/guard-filename.js +42 -106
  46. package/lib/guard-graphql.js +42 -123
  47. package/lib/guard-html.js +53 -108
  48. package/lib/guard-idempotency-key.js +24 -42
  49. package/lib/guard-image.js +46 -103
  50. package/lib/guard-imap-command.js +18 -32
  51. package/lib/guard-jmap.js +16 -30
  52. package/lib/guard-json.js +38 -108
  53. package/lib/guard-jsonpath.js +38 -171
  54. package/lib/guard-jwt.js +49 -179
  55. package/lib/guard-list-id.js +25 -41
  56. package/lib/guard-list-unsubscribe.js +27 -43
  57. package/lib/guard-mail-compose.js +24 -42
  58. package/lib/guard-mail-move.js +26 -44
  59. package/lib/guard-mail-query.js +28 -46
  60. package/lib/guard-mail-reply.js +24 -42
  61. package/lib/guard-mail-sieve.js +24 -42
  62. package/lib/guard-managesieve-command.js +17 -31
  63. package/lib/guard-markdown.js +37 -104
  64. package/lib/guard-message-id.js +26 -45
  65. package/lib/guard-mime.js +39 -151
  66. package/lib/guard-oauth.js +54 -135
  67. package/lib/guard-pdf.js +45 -101
  68. package/lib/guard-pop3-command.js +21 -31
  69. package/lib/guard-posture-chain.js +24 -42
  70. package/lib/guard-regex.js +33 -107
  71. package/lib/guard-saga-config.js +24 -42
  72. package/lib/guard-shell.js +42 -172
  73. package/lib/guard-smtp-command.js +48 -54
  74. package/lib/guard-snapshot-envelope.js +24 -42
  75. package/lib/guard-sql.js +1491 -0
  76. package/lib/guard-stream-args.js +24 -43
  77. package/lib/guard-svg.js +47 -65
  78. package/lib/guard-template.js +35 -172
  79. package/lib/guard-tenant-id.js +26 -45
  80. package/lib/guard-time.js +32 -154
  81. package/lib/guard-trace-context.js +25 -44
  82. package/lib/guard-uuid.js +32 -153
  83. package/lib/guard-xml.js +38 -113
  84. package/lib/guard-yaml.js +51 -163
  85. package/lib/http-client.js +14 -0
  86. package/lib/inbox.js +120 -107
  87. package/lib/legal-hold.js +107 -50
  88. package/lib/log-stream-cloudwatch.js +47 -31
  89. package/lib/log-stream-otlp.js +32 -18
  90. package/lib/mail-crypto-smime.js +2 -6
  91. package/lib/mail-greylist.js +2 -6
  92. package/lib/mail-helo.js +2 -6
  93. package/lib/mail-journal.js +85 -64
  94. package/lib/mail-rbl.js +2 -6
  95. package/lib/mail-scan.js +2 -6
  96. package/lib/mail-spam-score.js +2 -6
  97. package/lib/mail-store.js +293 -154
  98. package/lib/middleware/fetch-metadata.js +17 -7
  99. package/lib/middleware/idempotency-key.js +54 -38
  100. package/lib/middleware/rate-limit.js +102 -32
  101. package/lib/middleware/security-headers.js +21 -5
  102. package/lib/migrations.js +108 -66
  103. package/lib/network-heartbeat.js +7 -0
  104. package/lib/nonce-store.js +31 -9
  105. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  106. package/lib/object-store/azure-blob.js +31 -3
  107. package/lib/object-store/sigv4.js +10 -0
  108. package/lib/outbox.js +136 -82
  109. package/lib/pqc-agent.js +44 -0
  110. package/lib/pubsub-cluster.js +42 -20
  111. package/lib/queue-local.js +202 -139
  112. package/lib/queue-redis.js +9 -1
  113. package/lib/queue-sqs.js +6 -0
  114. package/lib/retention.js +82 -39
  115. package/lib/safe-dns.js +29 -45
  116. package/lib/safe-ical.js +18 -33
  117. package/lib/safe-icap.js +27 -43
  118. package/lib/safe-sieve.js +21 -40
  119. package/lib/safe-sql.js +124 -3
  120. package/lib/safe-vcard.js +18 -33
  121. package/lib/scheduler.js +35 -12
  122. package/lib/seeders.js +122 -74
  123. package/lib/session-stores.js +42 -14
  124. package/lib/session.js +116 -72
  125. package/lib/sql.js +3885 -0
  126. package/lib/static.js +45 -7
  127. package/lib/subject.js +89 -49
  128. package/lib/vault/index.js +3 -2
  129. package/lib/vault/passphrase-ops.js +3 -2
  130. package/lib/vault/rotate.js +104 -64
  131. package/lib/vendor-data.js +2 -0
  132. package/lib/websocket.js +16 -0
  133. package/package.json +1 -1
  134. package/sbom.cdx.json +6 -6
package/lib/audit-sign.js CHANGED
@@ -63,6 +63,7 @@ var nodePath = require("node:path");
63
63
  var nodeCrypto = require("node:crypto");
64
64
  var atomicFile = require("./atomic-file");
65
65
  var { sha3Hash } = require("./crypto");
66
+ var frameworkFiles = require("./framework-files");
66
67
  var { defineClass } = require("./framework-error");
67
68
  var { boot } = require("./log");
68
69
  var safeBuffer = require("./safe-buffer");
@@ -118,11 +119,73 @@ var log = boot("audit-sign");
118
119
  function resolvePaths(dataDir) {
119
120
  return {
120
121
  dataDir: dataDir,
121
- plaintext: nodePath.join(dataDir, "audit-sign.key"),
122
- sealed: nodePath.join(dataDir, "audit-sign.key.sealed"),
122
+ plaintext: nodePath.join(dataDir, frameworkFiles.fileName("auditSignKey")),
123
+ sealed: nodePath.join(dataDir, frameworkFiles.fileName("auditSignKey") + ".sealed"),
124
+ // Unsealed registry of rotated-out PUBLIC keys (public keys are not
125
+ // secret). It lets verify-time code (b.audit.verifyCheckpoints) resolve
126
+ // the public key for a checkpoint signed under a now-rotated key WITHOUT
127
+ // the old passphrase, so a rotation does not strand historical checkpoints.
128
+ publicHistory: nodePath.join(dataDir, "audit-sign.pubkeys.json"),
123
129
  };
124
130
  }
125
131
 
132
+ // Append a rotated-out public key to the unsealed public-key history. Public
133
+ // keys carry no secret, so storing them in the clear is safe and is what
134
+ // makes passphrase-free historical verification possible. De-duplicated by
135
+ // fingerprint; best-effort (a write failure must not abort the rotation, the
136
+ // sealed private-key history is the durable archive of record).
137
+ function _appendPublicHistory(entry) {
138
+ if (!paths || !paths.publicHistory) return;
139
+ var list = [];
140
+ try {
141
+ if (nodeFs.existsSync(paths.publicHistory)) {
142
+ var parsed = safeJson.parse(atomicFile.readSync(paths.publicHistory));
143
+ if (Array.isArray(parsed)) list = parsed;
144
+ }
145
+ } catch (_e) { list = []; } // corrupt registry — rebuild from this entry
146
+ for (var i = 0; i < list.length; i += 1) {
147
+ if (list[i] && list[i].fingerprint === entry.fingerprint) return; // already recorded
148
+ }
149
+ list.push(entry);
150
+ try {
151
+ atomicFile.writeSync(paths.publicHistory, JSON.stringify(list, null, 2), { fileMode: 0o600 });
152
+ } catch (_e) { /* best-effort */ }
153
+ }
154
+
155
+ /**
156
+ * @primitive b.auditSign.getPublicKeyByFingerprint
157
+ * @signature b.auditSign.getPublicKeyByFingerprint(fingerprint)
158
+ * @since 0.14.29
159
+ * @status stable
160
+ * @related b.auditSign.getPublicKey, b.auditSign.verify, b.auditSign.rotateSigningKey
161
+ *
162
+ * Resolve the audit-signing public key (SPKI PEM) for a fingerprint: the
163
+ * live key, or a rotated-out key recorded in the unsealed public-key history
164
+ * that `rotateSigningKey` maintains. Returns `null` when no key matches. Only
165
+ * public material is consulted, so no passphrase is needed - this is what
166
+ * lets `b.audit.verifyCheckpoints` verify a checkpoint signed under a
167
+ * now-rotated key without stranding history.
168
+ *
169
+ * @example
170
+ * var pem = b.auditSign.getPublicKeyByFingerprint(checkpoint.publicKeyFingerprint);
171
+ * // -> "-----BEGIN PUBLIC KEY-----\n..." (or null if the key is unknown)
172
+ */
173
+ function getPublicKeyByFingerprint(fp) {
174
+ _requireInit();
175
+ if (fp === keys.fingerprint) return keys.publicKey;
176
+ if (!paths || !paths.publicHistory || !nodeFs.existsSync(paths.publicHistory)) return null;
177
+ var list;
178
+ try { list = safeJson.parse(atomicFile.readSync(paths.publicHistory)); }
179
+ catch (_e) { return null; }
180
+ if (!Array.isArray(list)) return null;
181
+ for (var i = 0; i < list.length; i += 1) {
182
+ if (list[i] && list[i].fingerprint === fp && typeof list[i].publicKey === "string") {
183
+ return list[i].publicKey;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
126
189
  function _computeFingerprint(publicKeyPem) {
127
190
  return sha3Hash(publicKeyPem);
128
191
  }
@@ -677,6 +740,17 @@ async function rotateSigningKey(rotOpts) {
677
740
  catch (_e) { /* history copy is best-effort */ }
678
741
  }
679
742
 
743
+ // Record the rotated-out PUBLIC key (unsealed) so b.audit.verifyCheckpoints
744
+ // can verify a checkpoint signed under it after rotation without the old
745
+ // passphrase. Without this the public key only lives inside the sealed
746
+ // history archive and verification of pre-rotation checkpoints is stranded.
747
+ _appendPublicHistory({
748
+ fingerprint: prevFingerprint,
749
+ publicKey: prevPublicKey,
750
+ algorithm: prevAlgorithm,
751
+ rotatedAt: new Date().toISOString(),
752
+ });
753
+
680
754
  // Persist the new keypair through the same path as boot — sealed
681
755
  // mode re-wraps with the operator's passphrase; plaintext mode
682
756
  // writes JSON. We don't accept a passphrase override here; the
@@ -740,6 +814,7 @@ module.exports = {
740
814
  reSignAll: reSignAll,
741
815
  getPublicKey: getPublicKey,
742
816
  getPublicKeyFingerprint: getPublicKeyFingerprint,
817
+ getPublicKeyByFingerprint: getPublicKeyByFingerprint,
743
818
  getMode: getMode,
744
819
  getAlgorithm: getAlgorithm,
745
820
  DEFAULT_SIGNING_ALG: DEFAULT_SIGNING_ALG,
@@ -63,9 +63,11 @@ var canonicalJson = require("./canonical-json");
63
63
  var auditSign = require("./audit-sign");
64
64
  var backupCrypto = require("./backup/crypto");
65
65
  var clusterStorage = require("./cluster-storage");
66
+ var frameworkFiles = require("./framework-files");
66
67
  var lazyRequire = require("./lazy-require");
67
68
  var validateOpts = require("./validate-opts");
68
69
  var safeJson = require("./safe-json");
70
+ var sql = require("./sql");
69
71
  var { defineClass } = require("./framework-error");
70
72
 
71
73
  var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
@@ -79,6 +81,17 @@ var audit = lazyRequire(function () { return require("./audit"); });
79
81
 
80
82
  var AuditToolsError = defineClass("AuditToolsError", { alwaysPermanent: true });
81
83
 
84
+ // b.sql opts for every framework-table statement: thread the ACTIVE backend
85
+ // dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
86
+ // "mysql" in cluster mode) so the emitted identifier quoting + dialect
87
+ // idioms (ON CONFLICT vs ON DUPLICATE KEY) match the backend the SQL
88
+ // dispatches to. Defaulting to "sqlite" works on Postgres only by accident
89
+ // (both double-quote identifiers) and emits the wrong quoting on MySQL.
90
+ // clusterStorage.execute still rewrites table names + translates `?`
91
+ // placeholders at dispatch; this controls only the builder-side quoting +
92
+ // idiom selection.
93
+ function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
94
+
82
95
  // Dual-control gate constants for the audit_log physical purge. The
83
96
  // purge erases signed audit history, so when an operator has declared
84
97
  // audit_log under b.db.declareRequireDualControl the deletion requires
@@ -274,42 +287,48 @@ function _verifyChainSlice(rows, startPrevHash) {
274
287
  // cluster-storage reader so the tooling works in both single-node and
275
288
  // cluster deployments without the caller knowing which mode is active.
276
289
  async function _defaultReadRows(criteria) {
277
- var sql = 'SELECT * FROM "audit_log"';
278
- var conds = [];
279
- var params = [];
280
- if (criteria.fromMs != null) { conds.push("recordedAt >= ?"); params.push(criteria.fromMs); }
281
- if (criteria.toMs != null) { conds.push("recordedAt <= ?"); params.push(criteria.toMs); }
282
- if (criteria.beforeMs != null) { conds.push("recordedAt < ?"); params.push(criteria.beforeMs); }
283
- if (criteria.action) { conds.push("action = ?"); params.push(criteria.action); }
284
- if (criteria.firstCounter != null) { conds.push("monotonicCounter >= ?"); params.push(criteria.firstCounter); }
285
- if (criteria.lastCounter != null) { conds.push("monotonicCounter <= ?"); params.push(criteria.lastCounter); }
286
- if (conds.length > 0) sql += " WHERE " + conds.join(" AND ");
287
- sql += " ORDER BY monotonicCounter ASC";
288
- return clusterStorage.executeAll(sql, params);
290
+ // Compose the criteria onto a b.sql SELECT with a BARE logical table name
291
+ // (clusterStorage rewrites the framework name + placeholderizes); b.sql
292
+ // quotes the camelCase columns + binds every value.
293
+ var qb = sql.select("audit_log", _sqlOpts());
294
+ if (criteria.fromMs != null) qb.whereOp("recordedAt", ">=", criteria.fromMs);
295
+ if (criteria.toMs != null) qb.whereOp("recordedAt", "<=", criteria.toMs);
296
+ if (criteria.beforeMs != null) qb.whereOp("recordedAt", "<", criteria.beforeMs);
297
+ if (criteria.action) qb.where("action", criteria.action);
298
+ if (criteria.firstCounter != null) qb.whereOp("monotonicCounter", ">=", criteria.firstCounter);
299
+ if (criteria.lastCounter != null) qb.whereOp("monotonicCounter", "<=", criteria.lastCounter);
300
+ qb.orderBy("monotonicCounter", "asc");
301
+ var built = qb.toSql();
302
+ return clusterStorage.executeAll(built.sql, built.params);
289
303
  }
290
304
 
291
305
  async function _defaultReadCoveringCheckpoint(lastCounter) {
292
- return clusterStorage.executeOne(
293
- "SELECT * FROM audit_checkpoints " +
294
- "WHERE atMonotonicCounter >= ? " +
295
- "ORDER BY atMonotonicCounter ASC LIMIT 1",
296
- [lastCounter]
297
- );
306
+ var built = sql.select("audit_checkpoints", _sqlOpts())
307
+ .whereOp("atMonotonicCounter", ">=", lastCounter)
308
+ .orderBy("atMonotonicCounter", "asc")
309
+ .limit(1)
310
+ .toSql();
311
+ return clusterStorage.executeOne(built.sql, built.params);
298
312
  }
299
313
 
300
314
  async function _defaultReadPredecessorRowHash(firstCounter) {
301
315
  if (firstCounter <= 1) return auditChain.ZERO_HASH;
302
- var row = await clusterStorage.executeOne(
303
- "SELECT rowHash FROM audit_log WHERE monotonicCounter = ?",
304
- [firstCounter - 1]
305
- );
316
+ var rowBuilt = sql.select("audit_log", _sqlOpts())
317
+ .columns(["rowHash"])
318
+ .where("monotonicCounter", firstCounter - 1)
319
+ .toSql();
320
+ var row = await clusterStorage.executeOne(rowBuilt.sql, rowBuilt.params);
306
321
  if (!row) {
307
322
  // First row of the slice is right after a purged range. Read the
308
- // purge anchor's lastRowHash instead.
309
- var anchor = await clusterStorage.executeOne(
310
- "SELECT lastPurgedRowHash, lastPurgedCounter FROM _blamejs_audit_purge_anchor " +
311
- "WHERE scope = 'audit'"
312
- );
323
+ // purge anchor's lastRowHash instead. The anchor is an external-only
324
+ // table whose LOGICAL name IS the `_blamejs_`-prefixed name (it maps
325
+ // to itself in LOCAL_TO_EXTERNAL); b.sql must receive it bare so
326
+ // clusterStorage rewrites it. allow:hand-rolled-sql — bare logical key.
327
+ var anchorBuilt = sql.select("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
328
+ .columns(["lastPurgedRowHash", "lastPurgedCounter"])
329
+ .where("scope", "audit")
330
+ .toSql();
331
+ var anchor = await clusterStorage.executeOne(anchorBuilt.sql, anchorBuilt.params);
313
332
  if (anchor && Number(anchor.lastPurgedCounter) === firstCounter - 1) {
314
333
  return anchor.lastPurgedRowHash;
315
334
  }
@@ -343,7 +362,7 @@ async function _buildBundle(args) {
343
362
  return JSON.stringify(_rowToWireForm(r));
344
363
  }).join("\n") + "\n";
345
364
  var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
346
- files["rows.enc"] = rowsEnc.encrypted;
365
+ files[frameworkFiles.fileName("rowsEnc")] = rowsEnc.encrypted;
347
366
 
348
367
  // 2. (archive) Encrypt the checkpoint JSON
349
368
  var checkpointSalt = null;
@@ -351,7 +370,7 @@ async function _buildBundle(args) {
351
370
  if (checkpoint) {
352
371
  var ckptJson = _canonicalize(_rowToWireForm(checkpoint));
353
372
  var ckptEnc = await backupCrypto.encryptWithFreshSalt(ckptJson, passphrase);
354
- files["checkpoint.enc"] = ckptEnc.encrypted;
373
+ files[frameworkFiles.fileName("checkpointEnc")] = ckptEnc.encrypted;
355
374
  checkpointSalt = ckptEnc.salt;
356
375
  checkpointEncrypted = ckptEnc.encrypted;
357
376
  }
@@ -401,9 +420,9 @@ async function _writeBundle(args) {
401
420
  var built = await _buildBundle(args);
402
421
 
403
422
  atomicFile.ensureDir(outDir);
404
- atomicFile.writeSync(nodePath.join(outDir, "rows.enc"), built.files["rows.enc"], { fileMode: 0o600 });
405
- if (built.files["checkpoint.enc"]) {
406
- atomicFile.writeSync(nodePath.join(outDir, "checkpoint.enc"), built.files["checkpoint.enc"], { fileMode: 0o600 });
423
+ atomicFile.writeSync(nodePath.join(outDir, frameworkFiles.fileName("rowsEnc")), built.files[frameworkFiles.fileName("rowsEnc")], { fileMode: 0o600 });
424
+ if (built.files[frameworkFiles.fileName("checkpointEnc")]) {
425
+ atomicFile.writeSync(nodePath.join(outDir, frameworkFiles.fileName("checkpointEnc")), built.files[frameworkFiles.fileName("checkpointEnc")], { fileMode: 0o600 });
407
426
  }
408
427
  var manifestPath = nodePath.join(outDir, "manifest.json");
409
428
  atomicFile.writeSync(manifestPath, built.files["manifest.json"], { fileMode: 0o600 });
@@ -432,7 +451,7 @@ async function _readBundle(inDir, passphrase) {
432
451
  "manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
433
452
  }
434
453
 
435
- var rowsEncPath = nodePath.join(inDir, "rows.enc");
454
+ var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
436
455
  if (!nodeFs.existsSync(rowsEncPath)) {
437
456
  throw new AuditToolsError("audit-tools/no-rows-blob",
438
457
  "rows.enc missing in " + inDir);
@@ -450,7 +469,7 @@ async function _readBundle(inDir, passphrase) {
450
469
 
451
470
  var checkpoint = null;
452
471
  if (manifest.kind === KIND_ARCHIVE) {
453
- var ckptPath = nodePath.join(inDir, "checkpoint.enc");
472
+ var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
454
473
  if (!nodeFs.existsSync(ckptPath)) {
455
474
  throw new AuditToolsError("audit-tools/no-checkpoint-blob",
456
475
  "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
@@ -945,26 +964,35 @@ async function purge(opts) {
945
964
  }
946
965
 
947
966
  async function _defaultReadPurgeAnchor() {
948
- return clusterStorage.executeOne(
949
- "SELECT * FROM _blamejs_audit_purge_anchor WHERE scope = 'audit'"
950
- );
967
+ // External-only table — its logical name IS the `_blamejs_`-prefixed name
968
+ // (self-mapped in LOCAL_TO_EXTERNAL); b.sql receives it bare so
969
+ // clusterStorage rewrites it. allow:hand-rolled-sql — bare logical key.
970
+ var built = sql.select("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
971
+ .where("scope", "audit")
972
+ .toSql();
973
+ return clusterStorage.executeOne(built.sql, built.params);
951
974
  }
952
975
 
953
976
  async function _defaultApplyPurge(args) {
954
977
  var del = await db().purgeAuditChain({ lastPurgedCounter: args.lastPurgedCounter });
955
- // UPSERT the single-row anchor. SQLite + Postgres both support
956
- // INSERT ... ON CONFLICT(scope) DO UPDATE.
957
- await clusterStorage.execute(
958
- "INSERT INTO _blamejs_audit_purge_anchor " +
959
- "(scope, lastPurgedCounter, lastPurgedRowHash, archiveBundleId, purgedAt) " +
960
- "VALUES ('audit', ?, ?, ?, ?) " +
961
- "ON CONFLICT(scope) DO UPDATE SET " +
962
- "lastPurgedCounter = excluded.lastPurgedCounter, " +
963
- "lastPurgedRowHash = excluded.lastPurgedRowHash, " +
964
- "archiveBundleId = excluded.archiveBundleId, " +
965
- "purgedAt = excluded.purgedAt",
966
- [args.lastPurgedCounter, args.lastPurgedRowHash, args.archiveBundleId, args.purgedAt]
967
- );
978
+ // UPSERT the single-row anchor via b.sql ON CONFLICT(scope) DO UPDATE
979
+ // (SQLite + Postgres). The anchor is external-only; its logical name IS
980
+ // the `_blamejs_`-prefixed name (self-mapped), passed bare so
981
+ // clusterStorage rewrites + placeholderizes. b.sql quotes the camelCase
982
+ // columns + binds 'audit'. allow:hand-rolled-sql — bare logical key.
983
+ var built = sql.upsert("_blamejs_audit_purge_anchor", _sqlOpts()) // allow:hand-rolled-sql
984
+ .columns(["scope", "lastPurgedCounter", "lastPurgedRowHash", "archiveBundleId", "purgedAt"])
985
+ .values({
986
+ scope: "audit",
987
+ lastPurgedCounter: args.lastPurgedCounter,
988
+ lastPurgedRowHash: args.lastPurgedRowHash,
989
+ archiveBundleId: args.archiveBundleId,
990
+ purgedAt: args.purgedAt,
991
+ })
992
+ .onConflict(["scope"])
993
+ .doUpdateFromExcluded(["lastPurgedCounter", "lastPurgedRowHash", "archiveBundleId", "purgedAt"])
994
+ .toSql();
995
+ await clusterStorage.execute(built.sql, built.params);
968
996
  return {
969
997
  rowsDeleted: del.rowsDeleted,
970
998
  checkpointsDeleted: del.checkpointsDeleted,
@@ -1047,7 +1075,7 @@ async function forensicSnapshot(opts) {
1047
1075
  reason: opts.reason,
1048
1076
  actor: opts.actor || null,
1049
1077
  composedAt: new Date().toISOString(),
1050
- auditSliceFile: returnBytes ? "rows.enc" : (sliceResult && sliceResult.manifestPath),
1078
+ auditSliceFile: returnBytes ? frameworkFiles.fileName("rowsEnc") : (sliceResult && sliceResult.manifestPath),
1051
1079
  auditSliceCount: sliceResult && sliceResult.rowCount,
1052
1080
  runtime: {
1053
1081
  nodeVersion: process.version,