@blamejs/core 0.14.27 → 0.15.0
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.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +218 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +109 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +55 -17
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- 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,
|
|
122
|
-
sealed: nodePath.join(dataDir, "
|
|
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,
|
package/lib/audit-tools.js
CHANGED
|
@@ -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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (criteria.
|
|
282
|
-
if (criteria.
|
|
283
|
-
if (criteria.
|
|
284
|
-
if (criteria.
|
|
285
|
-
if (criteria.
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
303
|
-
"
|
|
304
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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[
|
|
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[
|
|
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,
|
|
405
|
-
if (built.files[
|
|
406
|
-
atomicFile.writeSync(nodePath.join(outDir,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
949
|
-
|
|
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.
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
"
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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 ?
|
|
1078
|
+
auditSliceFile: returnBytes ? frameworkFiles.fileName("rowsEnc") : (sliceResult && sliceResult.manifestPath),
|
|
1051
1079
|
auditSliceCount: sliceResult && sliceResult.rowCount,
|
|
1052
1080
|
runtime: {
|
|
1053
1081
|
nodeVersion: process.version,
|