@blamejs/core 0.7.4 → 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 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- 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 -256
- package/lib/cache.js +1206 -1206
- 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 +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- 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 -936
- 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 -264
- 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 -612
- 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 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- 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 -903
- 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 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- 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 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- 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 -319
- 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 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/chain-writer.js
CHANGED
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Chain-writer primitive — race-safe append to a hash-chained log table.
|
|
4
|
-
*
|
|
5
|
-
* The framework's audit_log AND consent_log have the same shape:
|
|
6
|
-
*
|
|
7
|
-
* 1. Take the next monotonic counter
|
|
8
|
-
* 2. Compute prevHash from the previous row's rowHash
|
|
9
|
-
* 3. Seal the logical row via field-crypto (sealedFields → vault.seal,
|
|
10
|
-
* derivedHashes computed)
|
|
11
|
-
* 4. Materialize null entries for every hashable column (so canonicalize
|
|
12
|
-
* at write-time and verify-time agree on the key set)
|
|
13
|
-
* 5. Compute rowHash over the sealed content (excluding chain bookkeeping)
|
|
14
|
-
* 6. INSERT with prevHash / rowHash / nonce / fencingToken
|
|
15
|
-
*
|
|
16
|
-
* audit.js and consent.js previously each carried their own copy of
|
|
17
|
-
* the chain-write pattern. The duplication produced bugs at the same
|
|
18
|
-
* architectural points: a chain-fork race had to be fixed in audit
|
|
19
|
-
* via Mutex, and consent had the same race because the fix hadn't
|
|
20
|
-
* propagated. Per the framework's "if a task repeats more than once
|
|
21
|
-
* it should be a primitive" rule, the pattern lives here and every
|
|
22
|
-
* chain-writer consumer gets the same safety guarantees automatically.
|
|
23
|
-
*
|
|
24
|
-
* Each chain-writer instance owns:
|
|
25
|
-
* - The table name (validated via sql-safe.assertOneOf at construction)
|
|
26
|
-
* - The full column list (for INSERT)
|
|
27
|
-
* - The hashable column list (for canonicalization)
|
|
28
|
-
* - A Mutex serializing the chain (read-prev → compute-hash → insert)
|
|
29
|
-
* - A Once initializing the in-process counter from MAX(monotonicCounter)
|
|
30
|
-
*
|
|
31
|
-
* Writes go through the cluster-storage dispatcher so the same chain
|
|
32
|
-
* definition works in single-node SQLite and cluster-mode external-db.
|
|
33
|
-
*
|
|
34
|
-
* Public API:
|
|
35
|
-
*
|
|
36
|
-
* chainWriter.create({
|
|
37
|
-
* table: "audit_log" | "consent_log" | …,
|
|
38
|
-
* columnsForInsert: [string], order matters; INSERT uses this
|
|
39
|
-
* hashableColumns: [string], for canonicalize null-fill
|
|
40
|
-
* validateAction: function (event) optional; throws on invalid input
|
|
41
|
-
* })
|
|
42
|
-
*
|
|
43
|
-
* writer.append(logical) async; returns { rowHash, prevHash, …logical }
|
|
44
|
-
* writer._resetForTest() re-initializes counter + mutex
|
|
45
|
-
*
|
|
46
|
-
* Operators usually don't construct chain-writers directly; audit and
|
|
47
|
-
* consent each construct one at module load.
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
var { generateToken, generateBytes } = require("./crypto");
|
|
51
|
-
var auditChain = require("./audit-chain");
|
|
52
|
-
var cryptoField = require("./crypto-field");
|
|
53
|
-
var cluster = require("./cluster");
|
|
54
|
-
var clusterStorage = require("./cluster-storage");
|
|
55
|
-
var safeAsync = require("./safe-async");
|
|
56
|
-
var safeSql = require("./safe-sql");
|
|
57
|
-
var C = require("./constants");
|
|
58
|
-
var { FrameworkError } = require("./framework-error");
|
|
59
|
-
|
|
60
|
-
// Allowlist of chain table names. Adding a new chain-backed table
|
|
61
|
-
// (e.g. some future _blamejs_security_log) requires registering it here
|
|
62
|
-
// so an operator can't accidentally point a chain-writer at a non-chain
|
|
63
|
-
// table and corrupt the chain semantics.
|
|
64
|
-
var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
|
|
65
|
-
|
|
66
|
-
var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
|
|
67
|
-
|
|
68
|
-
class ChainWriterError extends FrameworkError {
|
|
69
|
-
constructor(message, code) {
|
|
70
|
-
super(message);
|
|
71
|
-
this.name = "ChainWriterError";
|
|
72
|
-
this.code = code || "chain-writer/invalid";
|
|
73
|
-
this.isChainWriterError = true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function create(opts) {
|
|
78
|
-
if (!opts || !opts.table || !Array.isArray(opts.columnsForInsert) ||
|
|
79
|
-
!Array.isArray(opts.hashableColumns)) {
|
|
80
|
-
throw new ChainWriterError(
|
|
81
|
-
"create requires { table, columnsForInsert, hashableColumns }",
|
|
82
|
-
"chain-writer/invalid-config"
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
// Validate table name shape AND require it's in the chain-table allowlist.
|
|
86
|
-
safeSql.validateIdentifier(opts.table);
|
|
87
|
-
safeSql.assertOneOf(opts.table, ALLOWED_CHAIN_TABLES);
|
|
88
|
-
|
|
89
|
-
// Validate every column name against the SQL identifier rules — we
|
|
90
|
-
// interpolate them into the INSERT SQL.
|
|
91
|
-
for (var i = 0; i < opts.columnsForInsert.length; i++) {
|
|
92
|
-
safeSql.validateIdentifier(opts.columnsForInsert[i]);
|
|
93
|
-
}
|
|
94
|
-
for (var j = 0; j < opts.hashableColumns.length; j++) {
|
|
95
|
-
safeSql.validateIdentifier(opts.hashableColumns[j]);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
var table = opts.table;
|
|
99
|
-
var columnsForInsert = opts.columnsForInsert.slice();
|
|
100
|
-
var hashableColumns = opts.hashableColumns.slice();
|
|
101
|
-
var validateInput = opts.validateInput || null;
|
|
102
|
-
|
|
103
|
-
// Per-chain Mutex serializes the read-prev-tip + compute-hash + insert
|
|
104
|
-
// sequence. Without serialization, two concurrent awaiting append() calls
|
|
105
|
-
// would hash against the same prev-tip and produce sibling rows with the
|
|
106
|
-
// same prevHash — forking the chain.
|
|
107
|
-
var _chainMutex = new safeAsync.Mutex();
|
|
108
|
-
|
|
109
|
-
// Lazy counter primer — first append reads MAX(monotonicCounter) and
|
|
110
|
-
// increments from there. Once ensures concurrent first-callers share
|
|
111
|
-
// one in-flight init Promise.
|
|
112
|
-
var _nextCounter = 1;
|
|
113
|
-
var _counterInit = null;
|
|
114
|
-
|
|
115
|
-
function _ensureCounterInit() {
|
|
116
|
-
if (!_counterInit) {
|
|
117
|
-
_counterInit = new safeAsync.Once(async function () {
|
|
118
|
-
var row = await safeAsync.withTimeout(
|
|
119
|
-
safeAsync.asyncRetry(function () {
|
|
120
|
-
return clusterStorage.executeOne(
|
|
121
|
-
"SELECT MAX(monotonicCounter) AS m FROM " + safeSql.quoteIdentifier(table)
|
|
122
|
-
);
|
|
123
|
-
}),
|
|
124
|
-
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
125
|
-
{ name: table + ".readMaxCounter" }
|
|
126
|
-
);
|
|
127
|
-
_nextCounter = (row && row.m ? Number(row.m) : 0) + 1;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return _counterInit.invoke();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function _readChainTipRow() {
|
|
134
|
-
return await safeAsync.withTimeout(
|
|
135
|
-
safeAsync.asyncRetry(function () {
|
|
136
|
-
return clusterStorage.executeOne(
|
|
137
|
-
"SELECT rowHash FROM " + safeSql.quoteIdentifier(table) +
|
|
138
|
-
" ORDER BY monotonicCounter DESC LIMIT 1"
|
|
139
|
-
);
|
|
140
|
-
}),
|
|
141
|
-
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
142
|
-
{ name: table + ".readChainTip" }
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function _insertRow(values) {
|
|
147
|
-
// Build INSERT with quoted identifiers + ? placeholders. cluster-
|
|
148
|
-
// storage handles dialect-specific placeholder translation.
|
|
149
|
-
var quoted = columnsForInsert.map(function (c) { return safeSql.quoteIdentifier(c); }).join(", ");
|
|
150
|
-
var placeholders = columnsForInsert.map(function () { return "?"; }).join(", ");
|
|
151
|
-
return await safeAsync.withTimeout(
|
|
152
|
-
clusterStorage.execute(
|
|
153
|
-
"INSERT INTO " + safeSql.quoteIdentifier(table) +
|
|
154
|
-
" (" + quoted + ") VALUES (" + placeholders + ")",
|
|
155
|
-
values
|
|
156
|
-
),
|
|
157
|
-
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
158
|
-
{ name: table + ".insertRow" }
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function append(logical) {
|
|
163
|
-
if (validateInput) validateInput(logical);
|
|
164
|
-
cluster.requireLeader();
|
|
165
|
-
await _ensureCounterInit();
|
|
166
|
-
|
|
167
|
-
return await _chainMutex.runExclusive(async function () {
|
|
168
|
-
return await _appendInsideMutex(logical);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function _appendInsideMutex(logical) {
|
|
173
|
-
var counter = _nextCounter++;
|
|
174
|
-
var nowMs = Date.now();
|
|
175
|
-
var nonce = generateBytes(C.BYTES.bytes(16));
|
|
176
|
-
|
|
177
|
-
// Caller-supplied logical row: spread + add framework-managed fields.
|
|
178
|
-
var fullLogical = Object.assign({}, logical, {
|
|
179
|
-
_id: (logical && logical._id) || generateToken(C.BYTES.bytes(16)),
|
|
180
|
-
recordedAt: nowMs,
|
|
181
|
-
monotonicCounter: counter,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Seal sealed-fields, compute derived hashes
|
|
185
|
-
var sealed = cryptoField.sealRow(table, fullLogical);
|
|
186
|
-
|
|
187
|
-
// Materialize null entries for every hashable column the schema
|
|
188
|
-
// expects, so canonicalize sees the same key set at write-time and
|
|
189
|
-
// verify-time. JSON canonicalization distinguishes missing-key
|
|
190
|
-
// from key:null — must not.
|
|
191
|
-
for (var hci = 0; hci < hashableColumns.length; hci++) {
|
|
192
|
-
if (!(hashableColumns[hci] in sealed)) sealed[hashableColumns[hci]] = null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Compute rowHash over the sealed content fields
|
|
196
|
-
var tipRow = await _readChainTipRow();
|
|
197
|
-
var prevHash = tipRow ? tipRow.rowHash : auditChain.ZERO_HASH;
|
|
198
|
-
var rowHash = auditChain.computeRowHash(prevHash, sealed, nonce);
|
|
199
|
-
|
|
200
|
-
sealed.prevHash = prevHash;
|
|
201
|
-
sealed.rowHash = rowHash;
|
|
202
|
-
sealed.nonce = nonce;
|
|
203
|
-
|
|
204
|
-
var fencingToken = cluster.fencingToken();
|
|
205
|
-
var values = columnsForInsert.map(function (c) {
|
|
206
|
-
if (c === "fencingToken") return fencingToken;
|
|
207
|
-
return c in sealed ? sealed[c] : null;
|
|
208
|
-
});
|
|
209
|
-
await _insertRow(values);
|
|
210
|
-
|
|
211
|
-
return Object.assign({ rowHash: rowHash, prevHash: prevHash }, fullLogical);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function _resetForTest() {
|
|
215
|
-
_chainMutex = new safeAsync.Mutex();
|
|
216
|
-
_counterInit = null;
|
|
217
|
-
_nextCounter = 1;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
table: table,
|
|
222
|
-
append: append,
|
|
223
|
-
_resetForTest: _resetForTest,
|
|
224
|
-
// Expose for diagnostic introspection
|
|
225
|
-
_getMutexForTest: function () { return _chainMutex; },
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
module.exports = {
|
|
230
|
-
create: create,
|
|
231
|
-
ChainWriterError: ChainWriterError,
|
|
232
|
-
ALLOWED_CHAIN_TABLES: ALLOWED_CHAIN_TABLES,
|
|
233
|
-
FRAMEWORK_SQL_TIMEOUT_MS: FRAMEWORK_SQL_TIMEOUT_MS,
|
|
234
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Chain-writer primitive — race-safe append to a hash-chained log table.
|
|
4
|
+
*
|
|
5
|
+
* The framework's audit_log AND consent_log have the same shape:
|
|
6
|
+
*
|
|
7
|
+
* 1. Take the next monotonic counter
|
|
8
|
+
* 2. Compute prevHash from the previous row's rowHash
|
|
9
|
+
* 3. Seal the logical row via field-crypto (sealedFields → vault.seal,
|
|
10
|
+
* derivedHashes computed)
|
|
11
|
+
* 4. Materialize null entries for every hashable column (so canonicalize
|
|
12
|
+
* at write-time and verify-time agree on the key set)
|
|
13
|
+
* 5. Compute rowHash over the sealed content (excluding chain bookkeeping)
|
|
14
|
+
* 6. INSERT with prevHash / rowHash / nonce / fencingToken
|
|
15
|
+
*
|
|
16
|
+
* audit.js and consent.js previously each carried their own copy of
|
|
17
|
+
* the chain-write pattern. The duplication produced bugs at the same
|
|
18
|
+
* architectural points: a chain-fork race had to be fixed in audit
|
|
19
|
+
* via Mutex, and consent had the same race because the fix hadn't
|
|
20
|
+
* propagated. Per the framework's "if a task repeats more than once
|
|
21
|
+
* it should be a primitive" rule, the pattern lives here and every
|
|
22
|
+
* chain-writer consumer gets the same safety guarantees automatically.
|
|
23
|
+
*
|
|
24
|
+
* Each chain-writer instance owns:
|
|
25
|
+
* - The table name (validated via sql-safe.assertOneOf at construction)
|
|
26
|
+
* - The full column list (for INSERT)
|
|
27
|
+
* - The hashable column list (for canonicalization)
|
|
28
|
+
* - A Mutex serializing the chain (read-prev → compute-hash → insert)
|
|
29
|
+
* - A Once initializing the in-process counter from MAX(monotonicCounter)
|
|
30
|
+
*
|
|
31
|
+
* Writes go through the cluster-storage dispatcher so the same chain
|
|
32
|
+
* definition works in single-node SQLite and cluster-mode external-db.
|
|
33
|
+
*
|
|
34
|
+
* Public API:
|
|
35
|
+
*
|
|
36
|
+
* chainWriter.create({
|
|
37
|
+
* table: "audit_log" | "consent_log" | …,
|
|
38
|
+
* columnsForInsert: [string], order matters; INSERT uses this
|
|
39
|
+
* hashableColumns: [string], for canonicalize null-fill
|
|
40
|
+
* validateAction: function (event) optional; throws on invalid input
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* writer.append(logical) async; returns { rowHash, prevHash, …logical }
|
|
44
|
+
* writer._resetForTest() re-initializes counter + mutex
|
|
45
|
+
*
|
|
46
|
+
* Operators usually don't construct chain-writers directly; audit and
|
|
47
|
+
* consent each construct one at module load.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var { generateToken, generateBytes } = require("./crypto");
|
|
51
|
+
var auditChain = require("./audit-chain");
|
|
52
|
+
var cryptoField = require("./crypto-field");
|
|
53
|
+
var cluster = require("./cluster");
|
|
54
|
+
var clusterStorage = require("./cluster-storage");
|
|
55
|
+
var safeAsync = require("./safe-async");
|
|
56
|
+
var safeSql = require("./safe-sql");
|
|
57
|
+
var C = require("./constants");
|
|
58
|
+
var { FrameworkError } = require("./framework-error");
|
|
59
|
+
|
|
60
|
+
// Allowlist of chain table names. Adding a new chain-backed table
|
|
61
|
+
// (e.g. some future _blamejs_security_log) requires registering it here
|
|
62
|
+
// so an operator can't accidentally point a chain-writer at a non-chain
|
|
63
|
+
// table and corrupt the chain semantics.
|
|
64
|
+
var ALLOWED_CHAIN_TABLES = new Set(["audit_log", "consent_log"]);
|
|
65
|
+
|
|
66
|
+
var FRAMEWORK_SQL_TIMEOUT_MS = C.TIME.seconds(30);
|
|
67
|
+
|
|
68
|
+
class ChainWriterError extends FrameworkError {
|
|
69
|
+
constructor(message, code) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ChainWriterError";
|
|
72
|
+
this.code = code || "chain-writer/invalid";
|
|
73
|
+
this.isChainWriterError = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function create(opts) {
|
|
78
|
+
if (!opts || !opts.table || !Array.isArray(opts.columnsForInsert) ||
|
|
79
|
+
!Array.isArray(opts.hashableColumns)) {
|
|
80
|
+
throw new ChainWriterError(
|
|
81
|
+
"create requires { table, columnsForInsert, hashableColumns }",
|
|
82
|
+
"chain-writer/invalid-config"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
// Validate table name shape AND require it's in the chain-table allowlist.
|
|
86
|
+
safeSql.validateIdentifier(opts.table);
|
|
87
|
+
safeSql.assertOneOf(opts.table, ALLOWED_CHAIN_TABLES);
|
|
88
|
+
|
|
89
|
+
// Validate every column name against the SQL identifier rules — we
|
|
90
|
+
// interpolate them into the INSERT SQL.
|
|
91
|
+
for (var i = 0; i < opts.columnsForInsert.length; i++) {
|
|
92
|
+
safeSql.validateIdentifier(opts.columnsForInsert[i]);
|
|
93
|
+
}
|
|
94
|
+
for (var j = 0; j < opts.hashableColumns.length; j++) {
|
|
95
|
+
safeSql.validateIdentifier(opts.hashableColumns[j]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var table = opts.table;
|
|
99
|
+
var columnsForInsert = opts.columnsForInsert.slice();
|
|
100
|
+
var hashableColumns = opts.hashableColumns.slice();
|
|
101
|
+
var validateInput = opts.validateInput || null;
|
|
102
|
+
|
|
103
|
+
// Per-chain Mutex serializes the read-prev-tip + compute-hash + insert
|
|
104
|
+
// sequence. Without serialization, two concurrent awaiting append() calls
|
|
105
|
+
// would hash against the same prev-tip and produce sibling rows with the
|
|
106
|
+
// same prevHash — forking the chain.
|
|
107
|
+
var _chainMutex = new safeAsync.Mutex();
|
|
108
|
+
|
|
109
|
+
// Lazy counter primer — first append reads MAX(monotonicCounter) and
|
|
110
|
+
// increments from there. Once ensures concurrent first-callers share
|
|
111
|
+
// one in-flight init Promise.
|
|
112
|
+
var _nextCounter = 1;
|
|
113
|
+
var _counterInit = null;
|
|
114
|
+
|
|
115
|
+
function _ensureCounterInit() {
|
|
116
|
+
if (!_counterInit) {
|
|
117
|
+
_counterInit = new safeAsync.Once(async function () {
|
|
118
|
+
var row = await safeAsync.withTimeout(
|
|
119
|
+
safeAsync.asyncRetry(function () {
|
|
120
|
+
return clusterStorage.executeOne(
|
|
121
|
+
"SELECT MAX(monotonicCounter) AS m FROM " + safeSql.quoteIdentifier(table)
|
|
122
|
+
);
|
|
123
|
+
}),
|
|
124
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
125
|
+
{ name: table + ".readMaxCounter" }
|
|
126
|
+
);
|
|
127
|
+
_nextCounter = (row && row.m ? Number(row.m) : 0) + 1;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return _counterInit.invoke();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function _readChainTipRow() {
|
|
134
|
+
return await safeAsync.withTimeout(
|
|
135
|
+
safeAsync.asyncRetry(function () {
|
|
136
|
+
return clusterStorage.executeOne(
|
|
137
|
+
"SELECT rowHash FROM " + safeSql.quoteIdentifier(table) +
|
|
138
|
+
" ORDER BY monotonicCounter DESC LIMIT 1"
|
|
139
|
+
);
|
|
140
|
+
}),
|
|
141
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
142
|
+
{ name: table + ".readChainTip" }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function _insertRow(values) {
|
|
147
|
+
// Build INSERT with quoted identifiers + ? placeholders. cluster-
|
|
148
|
+
// storage handles dialect-specific placeholder translation.
|
|
149
|
+
var quoted = columnsForInsert.map(function (c) { return safeSql.quoteIdentifier(c); }).join(", ");
|
|
150
|
+
var placeholders = columnsForInsert.map(function () { return "?"; }).join(", ");
|
|
151
|
+
return await safeAsync.withTimeout(
|
|
152
|
+
clusterStorage.execute(
|
|
153
|
+
"INSERT INTO " + safeSql.quoteIdentifier(table) +
|
|
154
|
+
" (" + quoted + ") VALUES (" + placeholders + ")",
|
|
155
|
+
values
|
|
156
|
+
),
|
|
157
|
+
FRAMEWORK_SQL_TIMEOUT_MS,
|
|
158
|
+
{ name: table + ".insertRow" }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function append(logical) {
|
|
163
|
+
if (validateInput) validateInput(logical);
|
|
164
|
+
cluster.requireLeader();
|
|
165
|
+
await _ensureCounterInit();
|
|
166
|
+
|
|
167
|
+
return await _chainMutex.runExclusive(async function () {
|
|
168
|
+
return await _appendInsideMutex(logical);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function _appendInsideMutex(logical) {
|
|
173
|
+
var counter = _nextCounter++;
|
|
174
|
+
var nowMs = Date.now();
|
|
175
|
+
var nonce = generateBytes(C.BYTES.bytes(16));
|
|
176
|
+
|
|
177
|
+
// Caller-supplied logical row: spread + add framework-managed fields.
|
|
178
|
+
var fullLogical = Object.assign({}, logical, {
|
|
179
|
+
_id: (logical && logical._id) || generateToken(C.BYTES.bytes(16)),
|
|
180
|
+
recordedAt: nowMs,
|
|
181
|
+
monotonicCounter: counter,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Seal sealed-fields, compute derived hashes
|
|
185
|
+
var sealed = cryptoField.sealRow(table, fullLogical);
|
|
186
|
+
|
|
187
|
+
// Materialize null entries for every hashable column the schema
|
|
188
|
+
// expects, so canonicalize sees the same key set at write-time and
|
|
189
|
+
// verify-time. JSON canonicalization distinguishes missing-key
|
|
190
|
+
// from key:null — must not.
|
|
191
|
+
for (var hci = 0; hci < hashableColumns.length; hci++) {
|
|
192
|
+
if (!(hashableColumns[hci] in sealed)) sealed[hashableColumns[hci]] = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Compute rowHash over the sealed content fields
|
|
196
|
+
var tipRow = await _readChainTipRow();
|
|
197
|
+
var prevHash = tipRow ? tipRow.rowHash : auditChain.ZERO_HASH;
|
|
198
|
+
var rowHash = auditChain.computeRowHash(prevHash, sealed, nonce);
|
|
199
|
+
|
|
200
|
+
sealed.prevHash = prevHash;
|
|
201
|
+
sealed.rowHash = rowHash;
|
|
202
|
+
sealed.nonce = nonce;
|
|
203
|
+
|
|
204
|
+
var fencingToken = cluster.fencingToken();
|
|
205
|
+
var values = columnsForInsert.map(function (c) {
|
|
206
|
+
if (c === "fencingToken") return fencingToken;
|
|
207
|
+
return c in sealed ? sealed[c] : null;
|
|
208
|
+
});
|
|
209
|
+
await _insertRow(values);
|
|
210
|
+
|
|
211
|
+
return Object.assign({ rowHash: rowHash, prevHash: prevHash }, fullLogical);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _resetForTest() {
|
|
215
|
+
_chainMutex = new safeAsync.Mutex();
|
|
216
|
+
_counterInit = null;
|
|
217
|
+
_nextCounter = 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
table: table,
|
|
222
|
+
append: append,
|
|
223
|
+
_resetForTest: _resetForTest,
|
|
224
|
+
// Expose for diagnostic introspection
|
|
225
|
+
_getMutexForTest: function () { return _chainMutex; },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
create: create,
|
|
231
|
+
ChainWriterError: ChainWriterError,
|
|
232
|
+
ALLOWED_CHAIN_TABLES: ALLOWED_CHAIN_TABLES,
|
|
233
|
+
FRAMEWORK_SQL_TIMEOUT_MS: FRAMEWORK_SQL_TIMEOUT_MS,
|
|
234
|
+
};
|