@blamejs/core 0.7.1 → 0.7.18
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 +423 -389
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -282
- package/lib/api-key.js +660 -672
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -266
- package/lib/cache.js +1206 -1211
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +1136 -0
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -202
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -573
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -269
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -614
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -451
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -479
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -855
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -177
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -752
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -489
- package/lib/request-helpers.js +340 -336
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -630
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +879 -114
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -270
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -598
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/audit-chain.js
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Audit hash chain — tamper-evidence math.
|
|
4
|
-
*
|
|
5
|
-
* Per the compliance spec ("Tamper evidence (the chain + checkpoint signing)"
|
|
6
|
-
* in the roadmap):
|
|
7
|
-
*
|
|
8
|
-
* rowHash = SHA3-512(
|
|
9
|
-
* prevHash || canonicalize(row-fields-except-hash) || nonce
|
|
10
|
-
* )
|
|
11
|
-
*
|
|
12
|
-
* Each row's prevHash equals the previous row's rowHash (in monotonic-counter
|
|
13
|
-
* order). The first row uses ZERO_HASH as prevHash. Verification walks the
|
|
14
|
-
* chain forward; any row whose prevHash doesn't match the running hash, or
|
|
15
|
-
* whose rowHash recomputes differently, breaks the chain.
|
|
16
|
-
*
|
|
17
|
-
* Checkpoint signing (ML-DSA-87 over (atRow || atRowHash)) lives in
|
|
18
|
-
* lib/audit-sign.js. This module owns the chain hash math only;
|
|
19
|
-
* verification is O(n) and walks every row at boot.
|
|
20
|
-
*/
|
|
21
|
-
var canonicalJson = require("./canonical-json");
|
|
22
|
-
var C = require("./constants");
|
|
23
|
-
var { sha3Hash } = require("./crypto");
|
|
24
|
-
|
|
25
|
-
// SHA3-512 outputs 64 bytes; routed through C.BYTES so the file's byte
|
|
26
|
-
// arithmetic has one source of truth. Hex-encoded width is twice the
|
|
27
|
-
// byte count.
|
|
28
|
-
var SHA3_512_BYTES = C.BYTES.bytes(64);
|
|
29
|
-
var SHA3_512_HEX_LEN = SHA3_512_BYTES * 2;
|
|
30
|
-
|
|
31
|
-
// All-zero SHA3-512 sentinel prevHash for the first row.
|
|
32
|
-
var ZERO_HASH = "0".repeat(SHA3_512_HEX_LEN);
|
|
33
|
-
|
|
34
|
-
// Canonicalize a row for hashing. Excludes the hash/nonce columns themselves
|
|
35
|
-
// and any caller-specified columns. Sorted keys, JSON-encoded values; Buffer
|
|
36
|
-
// values converted to hex for stable byte serialization. Routes through
|
|
37
|
-
// the shared `lib/canonical-json` walker so the four canonicalize sites
|
|
38
|
-
// (this one, audit-tools, config-drift, pagination) share one
|
|
39
|
-
// implementation of the bug-class fix that started in v0.6.60 and
|
|
40
|
-
// completed in v0.6.67.
|
|
41
|
-
function canonicalize(row, excludeKeys) {
|
|
42
|
-
var ex = new Set(excludeKeys || []);
|
|
43
|
-
var keys = Object.keys(row).filter(function (k) { return !ex.has(k); }).sort();
|
|
44
|
-
var pairs = {};
|
|
45
|
-
for (var i = 0; i < keys.length; i++) {
|
|
46
|
-
pairs[keys[i]] = row[keys[i]];
|
|
47
|
-
}
|
|
48
|
-
return canonicalJson.stringify(pairs);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Compute a row's hash given its predecessor's hash, the row's logical fields
|
|
52
|
-
// (already excluding prevHash, rowHash, nonce), and the row's nonce buffer.
|
|
53
|
-
function computeRowHash(prevHash, rowFields, nonce) {
|
|
54
|
-
if (typeof prevHash !== "string" || prevHash.length !== SHA3_512_HEX_LEN) {
|
|
55
|
-
throw new Error("prevHash must be a " + SHA3_512_HEX_LEN +
|
|
56
|
-
"-char hex string (SHA3-512); got length " +
|
|
57
|
-
(prevHash && prevHash.length));
|
|
58
|
-
}
|
|
59
|
-
if (!Buffer.isBuffer(nonce) || nonce.length === 0) {
|
|
60
|
-
throw new Error("nonce must be a non-empty Buffer");
|
|
61
|
-
}
|
|
62
|
-
var canonical = canonicalize(rowFields);
|
|
63
|
-
var input = Buffer.concat([
|
|
64
|
-
Buffer.from(prevHash, "hex"),
|
|
65
|
-
Buffer.from(canonical, "utf8"),
|
|
66
|
-
nonce,
|
|
67
|
-
]);
|
|
68
|
-
return sha3Hash(input);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Read the current chain tip (last row's rowHash + monotonicCounter) for a
|
|
72
|
-
// given audit table. Async to accommodate operator-supplied external-db
|
|
73
|
-
// drivers; queryOneAsync is `async (sql, params?) → row | null`.
|
|
74
|
-
async function getChainTip(queryOneAsync, tableName) {
|
|
75
|
-
var row = await queryOneAsync(
|
|
76
|
-
'SELECT rowHash, monotonicCounter FROM "' + tableName + '" ' +
|
|
77
|
-
"ORDER BY monotonicCounter DESC LIMIT 1"
|
|
78
|
-
);
|
|
79
|
-
if (!row) return { prevHash: ZERO_HASH, counter: 0 };
|
|
80
|
-
return { prevHash: row.rowHash, counter: row.monotonicCounter };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Walk the entire chain forward, recomputing each row's hash. Returns an
|
|
84
|
-
// object describing the result; callers decide how to react (refuse-to-boot,
|
|
85
|
-
// log warning, etc.). queryAllAsync is `async (sql, params?) → rows`.
|
|
86
|
-
//
|
|
87
|
-
// audit_log only: if a `_blamejs_audit_purge_anchor` row exists, the walk
|
|
88
|
-
// starts at lastPurgedCounter+1 with prevHash = lastPurgedRowHash. The
|
|
89
|
-
// anchor is written by audit-tools.purge() after a successful archive,
|
|
90
|
-
// and lets the chain math survive deletion of historical rows without
|
|
91
|
-
// the bundle as the source of truth.
|
|
92
|
-
async function verifyChain(queryAllAsync, tableName, opts) {
|
|
93
|
-
opts = opts || {};
|
|
94
|
-
|
|
95
|
-
var prevHash = ZERO_HASH;
|
|
96
|
-
var skipBeforeCounter = 0;
|
|
97
|
-
if (tableName === "audit_log") {
|
|
98
|
-
var anchor;
|
|
99
|
-
try {
|
|
100
|
-
anchor = await queryAllAsync(
|
|
101
|
-
"SELECT lastPurgedCounter, lastPurgedRowHash FROM _blamejs_audit_purge_anchor " +
|
|
102
|
-
"WHERE scope = 'audit'"
|
|
103
|
-
);
|
|
104
|
-
} catch (_e) {
|
|
105
|
-
// Anchor table may not exist on a deployment that has never been
|
|
106
|
-
// through a purge. Treat as no anchor.
|
|
107
|
-
anchor = [];
|
|
108
|
-
}
|
|
109
|
-
if (Array.isArray(anchor) && anchor.length > 0) {
|
|
110
|
-
prevHash = anchor[0].lastPurgedRowHash;
|
|
111
|
-
skipBeforeCounter = Number(anchor[0].lastPurgedCounter);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
var rows = await queryAllAsync(
|
|
116
|
-
'SELECT * FROM "' + tableName + '" ORDER BY monotonicCounter ASC'
|
|
117
|
-
);
|
|
118
|
-
if (skipBeforeCounter > 0) {
|
|
119
|
-
rows = rows.filter(function (r) {
|
|
120
|
-
return Number(r.monotonicCounter) > skipBeforeCounter;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (rows.length === 0) {
|
|
125
|
-
return { ok: true, table: tableName, rowsVerified: 0, lastHash: prevHash };
|
|
126
|
-
}
|
|
127
|
-
for (var i = 0; i < rows.length; i++) {
|
|
128
|
-
var row = rows[i];
|
|
129
|
-
if (row.prevHash !== prevHash) {
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
table: tableName,
|
|
133
|
-
rowsVerified: i,
|
|
134
|
-
breakAt: i,
|
|
135
|
-
breakRowId: row._id,
|
|
136
|
-
reason: "prevHash mismatch",
|
|
137
|
-
expected: prevHash,
|
|
138
|
-
actual: row.prevHash,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
var fields = Object.assign({}, row);
|
|
142
|
-
delete fields.prevHash;
|
|
143
|
-
delete fields.rowHash;
|
|
144
|
-
delete fields.nonce;
|
|
145
|
-
// fencingToken is cluster-coordination bookkeeping — orthogonal to
|
|
146
|
-
// tamper-evidence. Excluded from chain hash inputs so deployments
|
|
147
|
-
// upgrading from a pre-cluster schema (no fencingToken column) verify
|
|
148
|
-
// identically before and after the ALTER TABLE.
|
|
149
|
-
delete fields.fencingToken;
|
|
150
|
-
var nonceBuf = Buffer.isBuffer(row.nonce) ? row.nonce : Buffer.from(row.nonce);
|
|
151
|
-
var computed = computeRowHash(prevHash, fields, nonceBuf);
|
|
152
|
-
if (computed !== row.rowHash) {
|
|
153
|
-
return {
|
|
154
|
-
ok: false,
|
|
155
|
-
table: tableName,
|
|
156
|
-
rowsVerified: i,
|
|
157
|
-
breakAt: i,
|
|
158
|
-
breakRowId: row._id,
|
|
159
|
-
reason: "rowHash mismatch",
|
|
160
|
-
expected: computed,
|
|
161
|
-
actual: row.rowHash,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
prevHash = row.rowHash;
|
|
165
|
-
|
|
166
|
-
if (opts.maxRows && i >= opts.maxRows - 1) break;
|
|
167
|
-
}
|
|
168
|
-
return { ok: true, table: tableName, rowsVerified: rows.length, lastHash: prevHash };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
module.exports = {
|
|
172
|
-
ZERO_HASH: ZERO_HASH,
|
|
173
|
-
canonicalize: canonicalize,
|
|
174
|
-
computeRowHash: computeRowHash,
|
|
175
|
-
getChainTip: getChainTip,
|
|
176
|
-
verifyChain: verifyChain,
|
|
177
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audit hash chain — tamper-evidence math.
|
|
4
|
+
*
|
|
5
|
+
* Per the compliance spec ("Tamper evidence (the chain + checkpoint signing)"
|
|
6
|
+
* in the roadmap):
|
|
7
|
+
*
|
|
8
|
+
* rowHash = SHA3-512(
|
|
9
|
+
* prevHash || canonicalize(row-fields-except-hash) || nonce
|
|
10
|
+
* )
|
|
11
|
+
*
|
|
12
|
+
* Each row's prevHash equals the previous row's rowHash (in monotonic-counter
|
|
13
|
+
* order). The first row uses ZERO_HASH as prevHash. Verification walks the
|
|
14
|
+
* chain forward; any row whose prevHash doesn't match the running hash, or
|
|
15
|
+
* whose rowHash recomputes differently, breaks the chain.
|
|
16
|
+
*
|
|
17
|
+
* Checkpoint signing (ML-DSA-87 over (atRow || atRowHash)) lives in
|
|
18
|
+
* lib/audit-sign.js. This module owns the chain hash math only;
|
|
19
|
+
* verification is O(n) and walks every row at boot.
|
|
20
|
+
*/
|
|
21
|
+
var canonicalJson = require("./canonical-json");
|
|
22
|
+
var C = require("./constants");
|
|
23
|
+
var { sha3Hash } = require("./crypto");
|
|
24
|
+
|
|
25
|
+
// SHA3-512 outputs 64 bytes; routed through C.BYTES so the file's byte
|
|
26
|
+
// arithmetic has one source of truth. Hex-encoded width is twice the
|
|
27
|
+
// byte count.
|
|
28
|
+
var SHA3_512_BYTES = C.BYTES.bytes(64);
|
|
29
|
+
var SHA3_512_HEX_LEN = SHA3_512_BYTES * 2;
|
|
30
|
+
|
|
31
|
+
// All-zero SHA3-512 sentinel prevHash for the first row.
|
|
32
|
+
var ZERO_HASH = "0".repeat(SHA3_512_HEX_LEN);
|
|
33
|
+
|
|
34
|
+
// Canonicalize a row for hashing. Excludes the hash/nonce columns themselves
|
|
35
|
+
// and any caller-specified columns. Sorted keys, JSON-encoded values; Buffer
|
|
36
|
+
// values converted to hex for stable byte serialization. Routes through
|
|
37
|
+
// the shared `lib/canonical-json` walker so the four canonicalize sites
|
|
38
|
+
// (this one, audit-tools, config-drift, pagination) share one
|
|
39
|
+
// implementation of the bug-class fix that started in v0.6.60 and
|
|
40
|
+
// completed in v0.6.67.
|
|
41
|
+
function canonicalize(row, excludeKeys) {
|
|
42
|
+
var ex = new Set(excludeKeys || []);
|
|
43
|
+
var keys = Object.keys(row).filter(function (k) { return !ex.has(k); }).sort();
|
|
44
|
+
var pairs = {};
|
|
45
|
+
for (var i = 0; i < keys.length; i++) {
|
|
46
|
+
pairs[keys[i]] = row[keys[i]];
|
|
47
|
+
}
|
|
48
|
+
return canonicalJson.stringify(pairs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Compute a row's hash given its predecessor's hash, the row's logical fields
|
|
52
|
+
// (already excluding prevHash, rowHash, nonce), and the row's nonce buffer.
|
|
53
|
+
function computeRowHash(prevHash, rowFields, nonce) {
|
|
54
|
+
if (typeof prevHash !== "string" || prevHash.length !== SHA3_512_HEX_LEN) {
|
|
55
|
+
throw new Error("prevHash must be a " + SHA3_512_HEX_LEN +
|
|
56
|
+
"-char hex string (SHA3-512); got length " +
|
|
57
|
+
(prevHash && prevHash.length));
|
|
58
|
+
}
|
|
59
|
+
if (!Buffer.isBuffer(nonce) || nonce.length === 0) {
|
|
60
|
+
throw new Error("nonce must be a non-empty Buffer");
|
|
61
|
+
}
|
|
62
|
+
var canonical = canonicalize(rowFields);
|
|
63
|
+
var input = Buffer.concat([
|
|
64
|
+
Buffer.from(prevHash, "hex"),
|
|
65
|
+
Buffer.from(canonical, "utf8"),
|
|
66
|
+
nonce,
|
|
67
|
+
]);
|
|
68
|
+
return sha3Hash(input);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Read the current chain tip (last row's rowHash + monotonicCounter) for a
|
|
72
|
+
// given audit table. Async to accommodate operator-supplied external-db
|
|
73
|
+
// drivers; queryOneAsync is `async (sql, params?) → row | null`.
|
|
74
|
+
async function getChainTip(queryOneAsync, tableName) {
|
|
75
|
+
var row = await queryOneAsync(
|
|
76
|
+
'SELECT rowHash, monotonicCounter FROM "' + tableName + '" ' +
|
|
77
|
+
"ORDER BY monotonicCounter DESC LIMIT 1"
|
|
78
|
+
);
|
|
79
|
+
if (!row) return { prevHash: ZERO_HASH, counter: 0 };
|
|
80
|
+
return { prevHash: row.rowHash, counter: row.monotonicCounter };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Walk the entire chain forward, recomputing each row's hash. Returns an
|
|
84
|
+
// object describing the result; callers decide how to react (refuse-to-boot,
|
|
85
|
+
// log warning, etc.). queryAllAsync is `async (sql, params?) → rows`.
|
|
86
|
+
//
|
|
87
|
+
// audit_log only: if a `_blamejs_audit_purge_anchor` row exists, the walk
|
|
88
|
+
// starts at lastPurgedCounter+1 with prevHash = lastPurgedRowHash. The
|
|
89
|
+
// anchor is written by audit-tools.purge() after a successful archive,
|
|
90
|
+
// and lets the chain math survive deletion of historical rows without
|
|
91
|
+
// the bundle as the source of truth.
|
|
92
|
+
async function verifyChain(queryAllAsync, tableName, opts) {
|
|
93
|
+
opts = opts || {};
|
|
94
|
+
|
|
95
|
+
var prevHash = ZERO_HASH;
|
|
96
|
+
var skipBeforeCounter = 0;
|
|
97
|
+
if (tableName === "audit_log") {
|
|
98
|
+
var anchor;
|
|
99
|
+
try {
|
|
100
|
+
anchor = await queryAllAsync(
|
|
101
|
+
"SELECT lastPurgedCounter, lastPurgedRowHash FROM _blamejs_audit_purge_anchor " +
|
|
102
|
+
"WHERE scope = 'audit'"
|
|
103
|
+
);
|
|
104
|
+
} catch (_e) {
|
|
105
|
+
// Anchor table may not exist on a deployment that has never been
|
|
106
|
+
// through a purge. Treat as no anchor.
|
|
107
|
+
anchor = [];
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(anchor) && anchor.length > 0) {
|
|
110
|
+
prevHash = anchor[0].lastPurgedRowHash;
|
|
111
|
+
skipBeforeCounter = Number(anchor[0].lastPurgedCounter);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var rows = await queryAllAsync(
|
|
116
|
+
'SELECT * FROM "' + tableName + '" ORDER BY monotonicCounter ASC'
|
|
117
|
+
);
|
|
118
|
+
if (skipBeforeCounter > 0) {
|
|
119
|
+
rows = rows.filter(function (r) {
|
|
120
|
+
return Number(r.monotonicCounter) > skipBeforeCounter;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (rows.length === 0) {
|
|
125
|
+
return { ok: true, table: tableName, rowsVerified: 0, lastHash: prevHash };
|
|
126
|
+
}
|
|
127
|
+
for (var i = 0; i < rows.length; i++) {
|
|
128
|
+
var row = rows[i];
|
|
129
|
+
if (row.prevHash !== prevHash) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
table: tableName,
|
|
133
|
+
rowsVerified: i,
|
|
134
|
+
breakAt: i,
|
|
135
|
+
breakRowId: row._id,
|
|
136
|
+
reason: "prevHash mismatch",
|
|
137
|
+
expected: prevHash,
|
|
138
|
+
actual: row.prevHash,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
var fields = Object.assign({}, row);
|
|
142
|
+
delete fields.prevHash;
|
|
143
|
+
delete fields.rowHash;
|
|
144
|
+
delete fields.nonce;
|
|
145
|
+
// fencingToken is cluster-coordination bookkeeping — orthogonal to
|
|
146
|
+
// tamper-evidence. Excluded from chain hash inputs so deployments
|
|
147
|
+
// upgrading from a pre-cluster schema (no fencingToken column) verify
|
|
148
|
+
// identically before and after the ALTER TABLE.
|
|
149
|
+
delete fields.fencingToken;
|
|
150
|
+
var nonceBuf = Buffer.isBuffer(row.nonce) ? row.nonce : Buffer.from(row.nonce);
|
|
151
|
+
var computed = computeRowHash(prevHash, fields, nonceBuf);
|
|
152
|
+
if (computed !== row.rowHash) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
table: tableName,
|
|
156
|
+
rowsVerified: i,
|
|
157
|
+
breakAt: i,
|
|
158
|
+
breakRowId: row._id,
|
|
159
|
+
reason: "rowHash mismatch",
|
|
160
|
+
expected: computed,
|
|
161
|
+
actual: row.rowHash,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
prevHash = row.rowHash;
|
|
165
|
+
|
|
166
|
+
if (opts.maxRows && i >= opts.maxRows - 1) break;
|
|
167
|
+
}
|
|
168
|
+
return { ok: true, table: tableName, rowsVerified: rows.length, lastHash: prevHash };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
ZERO_HASH: ZERO_HASH,
|
|
173
|
+
canonicalize: canonicalize,
|
|
174
|
+
computeRowHash: computeRowHash,
|
|
175
|
+
getChainTip: getChainTip,
|
|
176
|
+
verifyChain: verifyChain,
|
|
177
|
+
};
|