@blamejs/core 0.14.26 → 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 +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/agent-envelope-mac.js +104 -0
- package/lib/agent-event-bus.js +105 -4
- package/lib/agent-posture-chain.js +8 -42
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +62 -4
- 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 +249 -123
- package/lib/auth/openid-federation.js +108 -47
- 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 +169 -4
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +474 -92
- 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/error-page.js +14 -1
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/file-upload.js +52 -7
- package/lib/framework-error.js +14 -1
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +649 -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 +37 -9
- 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-server-jmap.js +117 -12
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/body-parser.js +71 -25
- package/lib/middleware/csrf-protect.js +19 -8
- 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 +57 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/observability.js +87 -0
- package/lib/otel-export.js +25 -1
- package/lib/outbox.js +136 -82
- package/lib/parsers/safe-xml.js +47 -7
- 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/redact.js +68 -11
- package/lib/redis-client.js +160 -31
- package/lib/retention.js +82 -39
- package/lib/router.js +212 -5
- 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/ssrf-guard.js +51 -4
- package/lib/static.js +177 -34
- 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 +35 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/db.js
CHANGED
|
@@ -60,6 +60,8 @@ var dbDeclareView = require("./db-declare-view");
|
|
|
60
60
|
var { Query } = require("./db-query");
|
|
61
61
|
var dbSchema = require("./db-schema");
|
|
62
62
|
var { defineClass } = require("./framework-error");
|
|
63
|
+
var frameworkFiles = require("./framework-files");
|
|
64
|
+
var frameworkSchema = require("./framework-schema");
|
|
63
65
|
var { boot } = require("./log");
|
|
64
66
|
var lazyRequire = require("./lazy-require");
|
|
65
67
|
var observability = require("./observability");
|
|
@@ -68,10 +70,18 @@ var safeAsync = require("./safe-async");
|
|
|
68
70
|
var safeEnv = require("./parsers/safe-env");
|
|
69
71
|
var safeJson = require("./safe-json");
|
|
70
72
|
var safeSql = require("./safe-sql");
|
|
73
|
+
var sql = require("./sql");
|
|
71
74
|
var validateOpts = require("./validate-opts");
|
|
72
75
|
var vault = require("./vault");
|
|
73
76
|
var vaultAad = require("./vault-aad");
|
|
74
77
|
|
|
78
|
+
// b.sql opts for the local single-node sqlite handle (database.prepare,
|
|
79
|
+
// never clusterStorage): "sqlite" dialect + quoteName so the resolved
|
|
80
|
+
// framework table name quotes by construction. The few DML sites here that
|
|
81
|
+
// target a framework state table resolve its name through
|
|
82
|
+
// frameworkSchema.tableName so a configured table prefix flows through.
|
|
83
|
+
var _SQL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
84
|
+
|
|
75
85
|
var DbError = defineClass("DbError", { alwaysPermanent: true });
|
|
76
86
|
var WormViolationError = require("./framework-error").WormViolationError;
|
|
77
87
|
var _wormErr = WormViolationError.factory;
|
|
@@ -173,27 +183,30 @@ var columnGateMode = "reject";
|
|
|
173
183
|
// are provisioned by the framework before app schema reconciles. Apps cannot
|
|
174
184
|
// opt out, override, or rename them. An app schema entry colliding with any of
|
|
175
185
|
// these names is refused at init.
|
|
186
|
+
// These are the canonical LOCAL reserved table-name declarations (the
|
|
187
|
+
// guard set an operator schema may not collide with), NOT query SQL — the
|
|
188
|
+
// literal names ARE the contract. allow:hand-rolled-sql markers below.
|
|
176
189
|
var RESERVED_TABLE_NAMES = new Set([
|
|
177
190
|
"audit_log",
|
|
178
191
|
"audit_checkpoints",
|
|
179
192
|
"consent_log",
|
|
180
|
-
"_blamejs_subject_restrictions",
|
|
181
|
-
"_blamejs_subject_erasures",
|
|
182
|
-
"_blamejs_sessions",
|
|
183
|
-
"_blamejs_jobs",
|
|
184
|
-
"_blamejs_migrations",
|
|
185
|
-
"_blamejs_counters",
|
|
186
|
-
"_blamejs_audit_purge_anchor",
|
|
187
|
-
"_blamejs_scheduler_ticks",
|
|
188
|
-
"_blamejs_rate_limit_counters",
|
|
189
|
-
"_blamejs_pubsub_messages",
|
|
190
|
-
"_blamejs_api_encrypt_nonces",
|
|
191
|
-
"_blamejs_api_keys",
|
|
192
|
-
"_blamejs_cache",
|
|
193
|
-
"_blamejs_seeders",
|
|
194
|
-
"_blamejs_seeders_lock",
|
|
195
|
-
"_blamejs_break_glass_policies",
|
|
196
|
-
"_blamejs_break_glass_grants",
|
|
193
|
+
"_blamejs_subject_restrictions", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
194
|
+
"_blamejs_subject_erasures", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
195
|
+
"_blamejs_sessions", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
196
|
+
"_blamejs_jobs", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
197
|
+
"_blamejs_migrations", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
198
|
+
"_blamejs_counters", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
199
|
+
"_blamejs_audit_purge_anchor", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
200
|
+
"_blamejs_scheduler_ticks", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
201
|
+
"_blamejs_rate_limit_counters", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
202
|
+
"_blamejs_pubsub_messages", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
203
|
+
"_blamejs_api_encrypt_nonces", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
204
|
+
"_blamejs_api_keys", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
205
|
+
"_blamejs_cache", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
206
|
+
"_blamejs_seeders", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
207
|
+
"_blamejs_seeders_lock", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
208
|
+
"_blamejs_break_glass_policies", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
209
|
+
"_blamejs_break_glass_grants", // allow:hand-rolled-sql — canonical reserved local table-name declaration
|
|
197
210
|
]);
|
|
198
211
|
|
|
199
212
|
var FRAMEWORK_SCHEMA = [
|
|
@@ -260,7 +273,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
260
273
|
},
|
|
261
274
|
},
|
|
262
275
|
{
|
|
263
|
-
name: "_blamejs_subject_restrictions",
|
|
276
|
+
name: "_blamejs_subject_restrictions", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
264
277
|
columns: {
|
|
265
278
|
subjectIdHash: "TEXT PRIMARY KEY",
|
|
266
279
|
since: "INTEGER NOT NULL",
|
|
@@ -269,7 +282,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
269
282
|
sealedFields: ["reason"],
|
|
270
283
|
},
|
|
271
284
|
{
|
|
272
|
-
name: "_blamejs_subject_erasures",
|
|
285
|
+
name: "_blamejs_subject_erasures", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
273
286
|
columns: {
|
|
274
287
|
subjectIdHash: "TEXT PRIMARY KEY",
|
|
275
288
|
erasedAt: "INTEGER NOT NULL",
|
|
@@ -281,7 +294,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
281
294
|
// b.retention consult b.legalHold.isHeld(subjectId) before
|
|
282
295
|
// accepting any deletion. Per FRCP Rule 26/37(e), GDPR Art
|
|
283
296
|
// 17(3)(e), SEC Rule 17a-4, HIPAA §164.530(j)(2).
|
|
284
|
-
name: "_blamejs_legal_hold",
|
|
297
|
+
name: "_blamejs_legal_hold", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
285
298
|
columns: {
|
|
286
299
|
subjectIdHash: "TEXT PRIMARY KEY",
|
|
287
300
|
placedAt: "INTEGER NOT NULL",
|
|
@@ -303,7 +316,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
303
316
|
// keypair rotation auto-reseals it old-root -> new-root via
|
|
304
317
|
// rotate._rotateColumn — without this a rotation would orphan every
|
|
305
318
|
// wrapped secret and brick every keyed row.
|
|
306
|
-
name: "_blamejs_per_row_keys",
|
|
319
|
+
name: "_blamejs_per_row_keys", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
307
320
|
columns: {
|
|
308
321
|
// _id is the rotation pipeline's keyset-pagination + UPDATE key
|
|
309
322
|
// (rotate._rotateColumn SELECTs _id and orders by it); the natural
|
|
@@ -328,7 +341,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
328
341
|
// demanded the WORM declaration; boot-time assertions iterate
|
|
329
342
|
// this registry to verify triggers are installed under the
|
|
330
343
|
// current b.compliance.current() posture.
|
|
331
|
-
name: "_blamejs_worm_tables",
|
|
344
|
+
name: "_blamejs_worm_tables", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
332
345
|
columns: {
|
|
333
346
|
tableName: "TEXT PRIMARY KEY",
|
|
334
347
|
posture: "TEXT",
|
|
@@ -341,7 +354,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
341
354
|
// destructive ops; under the named posture the framework refuses
|
|
342
355
|
// execution unless the caller passes a consumed dual-control
|
|
343
356
|
// grant.
|
|
344
|
-
name: "_blamejs_dual_control_gates",
|
|
357
|
+
name: "_blamejs_dual_control_gates", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
345
358
|
columns: {
|
|
346
359
|
tableName: "TEXT PRIMARY KEY",
|
|
347
360
|
posture: "TEXT",
|
|
@@ -368,7 +381,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
368
381
|
sealedFields: [],
|
|
369
382
|
},
|
|
370
383
|
{
|
|
371
|
-
name: "_blamejs_audit_purge_anchor",
|
|
384
|
+
name: "_blamejs_audit_purge_anchor", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
372
385
|
columns: {
|
|
373
386
|
// CHECK constraint: scope is one of the framework's audit-
|
|
374
387
|
// chain anchor scopes (`audit` / `consent`). Pre-v0.8.37 a
|
|
@@ -390,7 +403,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
390
403
|
// leader's INSERT loses with a constraint violation, and that node
|
|
391
404
|
// skips the tick. Closes the once-globally gap during cluster
|
|
392
405
|
// leader hand-offs where two leaders briefly coexist.
|
|
393
|
-
name: "_blamejs_scheduler_ticks",
|
|
406
|
+
name: "_blamejs_scheduler_ticks", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
394
407
|
columns: {
|
|
395
408
|
tickKey: "TEXT PRIMARY KEY",
|
|
396
409
|
name: "TEXT NOT NULL",
|
|
@@ -406,7 +419,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
406
419
|
// the cluster-shared rate-limit backend. One row per (key); the
|
|
407
420
|
// count rolls over atomically when the windowStart advances. Used
|
|
408
421
|
// by lib/middleware/rate-limit.js when scope: 'cluster' is set.
|
|
409
|
-
name: "_blamejs_rate_limit_counters",
|
|
422
|
+
name: "_blamejs_rate_limit_counters", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
410
423
|
columns: {
|
|
411
424
|
key: "TEXT PRIMARY KEY",
|
|
412
425
|
windowStart: "INTEGER NOT NULL",
|
|
@@ -422,7 +435,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
422
435
|
// publish; other nodes poll for new ids and dispatch to their
|
|
423
436
|
// local subscribers. Rows older than the configured retention
|
|
424
437
|
// window are pruned by the backend on a rate-limited basis.
|
|
425
|
-
name: "_blamejs_pubsub_messages",
|
|
438
|
+
name: "_blamejs_pubsub_messages", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
426
439
|
columns: {
|
|
427
440
|
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
428
441
|
topic: "TEXT NOT NULL",
|
|
@@ -440,7 +453,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
440
453
|
// exposes the original 16-byte client nonces. Hashing is
|
|
441
454
|
// deterministic so the PRIMARY KEY conflict is what catches a
|
|
442
455
|
// replay attempt within the replay window.
|
|
443
|
-
name: "_blamejs_api_encrypt_nonces",
|
|
456
|
+
name: "_blamejs_api_encrypt_nonces", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
444
457
|
columns: {
|
|
445
458
|
nonceHash: "TEXT PRIMARY KEY",
|
|
446
459
|
expireAt: "INTEGER NOT NULL",
|
|
@@ -449,7 +462,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
449
462
|
sealedFields: [],
|
|
450
463
|
},
|
|
451
464
|
{
|
|
452
|
-
name: "_blamejs_sessions",
|
|
465
|
+
name: "_blamejs_sessions", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
453
466
|
columns: {
|
|
454
467
|
sidHash: "TEXT PRIMARY KEY",
|
|
455
468
|
userId: "TEXT NOT NULL",
|
|
@@ -470,7 +483,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
470
483
|
// verify. Same dual-storage pattern as sessions: this row mirrors
|
|
471
484
|
// the cluster-mode DDL in framework-schema.js so cluster-storage
|
|
472
485
|
// can route to either backend transparently.
|
|
473
|
-
name: "_blamejs_api_keys",
|
|
486
|
+
name: "_blamejs_api_keys", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
474
487
|
columns: {
|
|
475
488
|
id: "TEXT PRIMARY KEY",
|
|
476
489
|
namespace: "TEXT NOT NULL",
|
|
@@ -501,7 +514,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
501
514
|
derivedHashes: { ownerIdHash: { from: "ownerId" } },
|
|
502
515
|
},
|
|
503
516
|
{
|
|
504
|
-
name: "_blamejs_jobs",
|
|
517
|
+
name: "_blamejs_jobs", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
505
518
|
columns: {
|
|
506
519
|
_id: "TEXT PRIMARY KEY",
|
|
507
520
|
queueName: "TEXT NOT NULL",
|
|
@@ -546,7 +559,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
546
559
|
// JSON-serialized; expiresAt is unix-ms (Number.MAX_SAFE_INTEGER for
|
|
547
560
|
// never-expiring entries). Not sealed: cache values are operator-
|
|
548
561
|
// chosen application data, the operator decides what's worth storing.
|
|
549
|
-
name: "_blamejs_cache",
|
|
562
|
+
name: "_blamejs_cache", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
550
563
|
columns: {
|
|
551
564
|
cacheKey: "TEXT PRIMARY KEY",
|
|
552
565
|
valueJson: "TEXT NOT NULL",
|
|
@@ -562,7 +575,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
562
575
|
// PK (cacheKey, tag) lets one cacheKey carry many tags; index on
|
|
563
576
|
// tag makes invalidation a single indexed scan. Cleared together
|
|
564
577
|
// with the matching _blamejs_cache rows on del / clear / sweep.
|
|
565
|
-
name: "_blamejs_cache_tags",
|
|
578
|
+
name: "_blamejs_cache_tags", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
566
579
|
columns: {
|
|
567
580
|
cacheKey: "TEXT NOT NULL",
|
|
568
581
|
tag: "TEXT NOT NULL",
|
|
@@ -578,7 +591,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
578
591
|
// collide with prod fixtures by name). rerunnable=1 entries get
|
|
579
592
|
// their appliedAt updated in place on every run; non-rerunnable
|
|
580
593
|
// entries are insert-once.
|
|
581
|
-
name: "_blamejs_seeders",
|
|
594
|
+
name: "_blamejs_seeders", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
582
595
|
columns: {
|
|
583
596
|
env: "TEXT NOT NULL",
|
|
584
597
|
name: "TEXT NOT NULL",
|
|
@@ -596,7 +609,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
596
609
|
// on scope='lock' enforces single row). Two processes calling
|
|
597
610
|
// `seed run` against the same DB race on this PK; loser sees a
|
|
598
611
|
// clear "lock held" error.
|
|
599
|
-
name: "_blamejs_seeders_lock",
|
|
612
|
+
name: "_blamejs_seeders_lock", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
600
613
|
columns: {
|
|
601
614
|
scope: "TEXT PRIMARY KEY CHECK (scope = 'lock')",
|
|
602
615
|
lockedAt: "INTEGER NOT NULL",
|
|
@@ -610,7 +623,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
610
623
|
// glass-locked and what the operator's grant rules are. Sealed
|
|
611
624
|
// columns hold the column-list, factor-list, and bypass config so
|
|
612
625
|
// policy contents aren't browsable in cleartext.
|
|
613
|
-
name: "_blamejs_break_glass_policies",
|
|
626
|
+
name: "_blamejs_break_glass_policies", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
614
627
|
columns: {
|
|
615
628
|
tableName: "TEXT PRIMARY KEY",
|
|
616
629
|
columnsJson: "TEXT NOT NULL",
|
|
@@ -639,7 +652,7 @@ var FRAMEWORK_SCHEMA = [
|
|
|
639
652
|
// (each row access = its own grant).
|
|
640
653
|
// Sealed columns hold reason + scopeColumns so audit-readable
|
|
641
654
|
// metadata doesn't leak in cleartext.
|
|
642
|
-
name: "_blamejs_break_glass_grants",
|
|
655
|
+
name: "_blamejs_break_glass_grants", // allow:hand-rolled-sql — canonical local-schema table-name declaration
|
|
643
656
|
columns: {
|
|
644
657
|
_id: "TEXT PRIMARY KEY",
|
|
645
658
|
issuedToActorId: "TEXT NOT NULL",
|
|
@@ -702,7 +715,7 @@ function loadOrCreateDbKey(dataDirPath, keyPathOverride) {
|
|
|
702
715
|
// needs to live outside `dataDir` (e.g. a separate volume mounted
|
|
703
716
|
// from a KMS-fronted secret store). Default places it next to the
|
|
704
717
|
// encrypted DB so backup capture is one-tarball.
|
|
705
|
-
var keyPath = keyPathOverride || nodePath.join(dataDirPath,
|
|
718
|
+
var keyPath = keyPathOverride || nodePath.join(dataDirPath, frameworkFiles.fileName("dbKeyEnc"));
|
|
706
719
|
var aad = _dbKeyAad(dataDirPath, keyPath);
|
|
707
720
|
if (nodeFs.existsSync(keyPath)) {
|
|
708
721
|
var sealed = atomicFile.readSync(keyPath, { encoding: "utf8" }).trim();
|
|
@@ -970,6 +983,7 @@ function cleanStaleTmpDbs(tmpDir) {
|
|
|
970
983
|
* schema: Array, // required — [{ name, columns, indexes, sealedFields, derivedHashes, foreignKeys, primaryKey, subjectField, personalDataCategories }, ...]
|
|
971
984
|
* atRest: "encrypted"|"plain", // default "encrypted"
|
|
972
985
|
* tmpDir: string, // override the encrypted-mode tmpfs path (default /dev/shm or BLAMEJS_TMPDIR)
|
|
986
|
+
* allowNonTmpfsTmpDir: boolean, // default false — encrypted mode THROWS when tmpDir is not a recognized tmpfs mount (plaintext-on-disk leak); pass true to downgrade to a warning when the mount is verified in-memory out-of-band
|
|
973
987
|
* migrationDir: string, // optional — path to ./migrations/ (run-once each)
|
|
974
988
|
* streamLimit: number, // default 1_000_000 — db.stream row ceiling
|
|
975
989
|
* columnGate: "reject"|"warn"|"off", // default "reject" — refuse queries on columns not declared in the table schema
|
|
@@ -1042,6 +1056,15 @@ async function init(opts) {
|
|
|
1042
1056
|
JSON.stringify(opts.columnGate));
|
|
1043
1057
|
}
|
|
1044
1058
|
columnGateMode = opts.columnGate || "reject";
|
|
1059
|
+
// Configurable framework-table prefix. Config-time: setTablePrefix
|
|
1060
|
+
// throws a FrameworkSchemaError on a non-identifier prefix so a typo
|
|
1061
|
+
// surfaces at boot rather than as a silently-misnamed table. Applied
|
|
1062
|
+
// BEFORE any schema creation below so framework DDL + the cluster
|
|
1063
|
+
// tableName resolver honor it. Default ("_blamejs_") is byte-identical
|
|
1064
|
+
// to the historical names, so omitting the option is a no-op.
|
|
1065
|
+
if (opts.tablePrefix !== undefined) {
|
|
1066
|
+
frameworkSchema.setTablePrefix(opts.tablePrefix);
|
|
1067
|
+
}
|
|
1045
1068
|
dataDir = opts.dataDir;
|
|
1046
1069
|
if (!nodeFs.existsSync(dataDir)) nodeFs.mkdirSync(dataDir, { recursive: true });
|
|
1047
1070
|
|
|
@@ -1054,21 +1077,42 @@ async function init(opts) {
|
|
|
1054
1077
|
}
|
|
1055
1078
|
if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
|
|
1056
1079
|
|
|
1057
|
-
// If the resolved tmpDir is NOT actually tmpfs, the
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1080
|
+
// If the resolved tmpDir is NOT actually tmpfs, the plaintext working
|
|
1081
|
+
// copy lives on persistent storage and leaks into backup snapshots,
|
|
1082
|
+
// replication, and forensic disk images — defeating the whole point of
|
|
1083
|
+
// encrypted-at-rest mode. On Linux we verify the resolved path lands
|
|
1084
|
+
// under a known in-memory mount (/dev/shm /run/shm /run/user, plus
|
|
1085
|
+
// /tmp which is tmpfs on systemd-default + most container images).
|
|
1086
|
+
//
|
|
1087
|
+
// Fail-closed default (v0.15.0): a tmpDir that resolves OUTSIDE those
|
|
1088
|
+
// mounts THROWS db/tmpdir-not-tmpfs at boot rather than logging a
|
|
1089
|
+
// warning the operator never reads — the prior warn-only path silently
|
|
1090
|
+
// shipped plaintext to disk under the encrypted-mode default. The
|
|
1091
|
+
// documented opt-out is opts.allowNonTmpfsTmpDir: true, for the operator
|
|
1092
|
+
// who has verified the mount is in-memory out-of-band (e.g. a ramfs /
|
|
1093
|
+
// a tmpfs bind-mounted at a non-standard path the heuristic can't see)
|
|
1094
|
+
// or who has accepted the disk-residency tradeoff. The opt-out downgrades
|
|
1095
|
+
// to the prior warning. (Free-space headroom is enforced separately via
|
|
1096
|
+
// fs.statfsSync in the storage guard below.) The heuristic is Linux-only;
|
|
1097
|
+
// other platforms can't be probed by path and emit the warning unchanged.
|
|
1063
1098
|
if (process.platform === "linux") {
|
|
1064
1099
|
var realTmp = "";
|
|
1065
1100
|
try { realTmp = nodeFs.realpathSync(tmpDir); } catch (_e) { /* stat best-effort */ }
|
|
1066
1101
|
if (realTmp.indexOf("/dev/shm") !== 0 && realTmp.indexOf("/run/shm") !== 0 &&
|
|
1067
1102
|
realTmp.indexOf("/run/user/") !== 0 && realTmp.indexOf("/tmp") !== 0) {
|
|
1068
|
-
|
|
1069
|
-
"') does not resolve under /dev/shm /run/shm /run/user /tmp —
|
|
1070
|
-
"
|
|
1071
|
-
"snapshots, replication, and forensic disk images."
|
|
1103
|
+
var tmpfsMsg = "db.init: tmpDir '" + tmpDir + "' (real: '" + realTmp +
|
|
1104
|
+
"') does not resolve under /dev/shm /run/shm /run/user /tmp — it is not a " +
|
|
1105
|
+
"recognized tmpfs mount. A persistent-disk tmpDir leaks the decrypted " +
|
|
1106
|
+
"working copy into backup snapshots, replication, and forensic disk images.";
|
|
1107
|
+
if (opts.allowNonTmpfsTmpDir === true) {
|
|
1108
|
+
log.warn("WARNING: " + tmpfsMsg + " (allowNonTmpfsTmpDir:true — verify the " +
|
|
1109
|
+
"mount is in-memory out-of-band.)");
|
|
1110
|
+
} else {
|
|
1111
|
+
throw _dbErr("db/tmpdir-not-tmpfs", "FATAL: " + tmpfsMsg +
|
|
1112
|
+
" Mount a tmpfs at the path (or set BLAMEJS_TMPDIR / opts.tmpDir to one), " +
|
|
1113
|
+
"or pass opts.allowNonTmpfsTmpDir: true to accept the disk-residency tradeoff, " +
|
|
1114
|
+
"or pass atRest: 'plain' if encryption-at-rest is not required.");
|
|
1115
|
+
}
|
|
1072
1116
|
}
|
|
1073
1117
|
}
|
|
1074
1118
|
|
|
@@ -1077,7 +1121,7 @@ async function init(opts) {
|
|
|
1077
1121
|
// just the basename under `dataDir` (default "db.enc"). Helps when
|
|
1078
1122
|
// multiple framework-shaped instances share a dataDir.
|
|
1079
1123
|
encPath = opts.encryptedDbPath ||
|
|
1080
|
-
nodePath.join(dataDir, opts.encryptedDbName ||
|
|
1124
|
+
nodePath.join(dataDir, opts.encryptedDbName || frameworkFiles.fileName("dbEnc"));
|
|
1081
1125
|
dbPath = nodePath.join(tmpDir, "blamejs-" + generateToken(C.BYTES.bytes(16)) + ".db");
|
|
1082
1126
|
encKey = loadOrCreateDbKey(dataDir, opts.dbKeyPath);
|
|
1083
1127
|
|
|
@@ -2264,21 +2308,24 @@ function _normalizePk(tableSpec) {
|
|
|
2264
2308
|
// that RAISE(ABORT) the operation. INSERT remains permitted (that's what
|
|
2265
2309
|
// audit.record / consent.grant do).
|
|
2266
2310
|
function _installAppendOnlyTriggers(database) {
|
|
2311
|
+
// b.sql has no CREATE TRIGGER builder — these append-only WORM triggers
|
|
2312
|
+
// are SQLite-specific (RAISE(ABORT) trigger bodies) over framework-
|
|
2313
|
+
// controlled, fixed table names. Identifiers are quoted by construction.
|
|
2267
2314
|
var tables = ["audit_log", "consent_log", "audit_checkpoints"];
|
|
2268
2315
|
for (var i = 0; i < tables.length; i++) {
|
|
2269
2316
|
var t = tables[i];
|
|
2270
2317
|
runSql(database,
|
|
2271
|
-
'CREATE TRIGGER IF NOT EXISTS "no_delete_' + t + '" ' +
|
|
2318
|
+
'CREATE TRIGGER IF NOT EXISTS "no_delete_' + t + '" ' + // allow:hand-rolled-sql — b.sql has no CREATE TRIGGER builder; SQLite append-only WORM trigger, fixed framework table
|
|
2272
2319
|
'BEFORE DELETE ON "' + t + '" ' +
|
|
2273
2320
|
'BEGIN ' +
|
|
2274
|
-
" SELECT RAISE(ABORT, '" + t + " is append-only — DELETE prohibited'); " +
|
|
2321
|
+
" SELECT RAISE(ABORT, '" + t + " is append-only — DELETE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2275
2322
|
'END'
|
|
2276
2323
|
);
|
|
2277
2324
|
runSql(database,
|
|
2278
|
-
'CREATE TRIGGER IF NOT EXISTS "no_update_' + t + '" ' +
|
|
2325
|
+
'CREATE TRIGGER IF NOT EXISTS "no_update_' + t + '" ' + // allow:hand-rolled-sql — b.sql has no CREATE TRIGGER builder; SQLite append-only WORM trigger, fixed framework table
|
|
2279
2326
|
'BEFORE UPDATE ON "' + t + '" ' +
|
|
2280
2327
|
'BEGIN ' +
|
|
2281
|
-
" SELECT RAISE(ABORT, '" + t + " is append-only — UPDATE prohibited'); " +
|
|
2328
|
+
" SELECT RAISE(ABORT, '" + t + " is append-only — UPDATE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2282
2329
|
'END'
|
|
2283
2330
|
);
|
|
2284
2331
|
}
|
|
@@ -2291,19 +2338,22 @@ function _installAppendOnlyTriggers(database) {
|
|
|
2291
2338
|
// boot-time assertion under WORM_POSTURES catches operators who
|
|
2292
2339
|
// set the posture without declaring tables.
|
|
2293
2340
|
function _installWormTriggers(database, tableName) {
|
|
2341
|
+
// b.sql has no CREATE TRIGGER builder — operator-table WORM triggers are
|
|
2342
|
+
// SQLite-specific RAISE(ABORT) trigger bodies; tableName is validated
|
|
2343
|
+
// (validateIdentifier) and quoted by construction.
|
|
2294
2344
|
safeSql.validateIdentifier(tableName);
|
|
2295
2345
|
runSql(database,
|
|
2296
|
-
'CREATE TRIGGER IF NOT EXISTS "worm_no_delete_' + tableName + '" ' +
|
|
2346
|
+
'CREATE TRIGGER IF NOT EXISTS "worm_no_delete_' + tableName + '" ' + // allow:hand-rolled-sql — b.sql has no CREATE TRIGGER builder; SQLite WORM trigger over validated operator table
|
|
2297
2347
|
'BEFORE DELETE ON "' + tableName + '" ' +
|
|
2298
2348
|
'BEGIN ' +
|
|
2299
|
-
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - DELETE prohibited'); " +
|
|
2349
|
+
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - DELETE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2300
2350
|
'END'
|
|
2301
2351
|
);
|
|
2302
2352
|
runSql(database,
|
|
2303
|
-
'CREATE TRIGGER IF NOT EXISTS "worm_no_update_' + tableName + '" ' +
|
|
2353
|
+
'CREATE TRIGGER IF NOT EXISTS "worm_no_update_' + tableName + '" ' + // allow:hand-rolled-sql — b.sql has no CREATE TRIGGER builder; SQLite WORM trigger over validated operator table
|
|
2304
2354
|
'BEFORE UPDATE ON "' + tableName + '" ' +
|
|
2305
2355
|
'BEGIN ' +
|
|
2306
|
-
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - UPDATE prohibited'); " +
|
|
2356
|
+
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - UPDATE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2307
2357
|
'END'
|
|
2308
2358
|
);
|
|
2309
2359
|
}
|
|
@@ -2369,9 +2419,8 @@ function declareWorm(args) {
|
|
|
2369
2419
|
"the SQLite trigger primitive is single-node only");
|
|
2370
2420
|
}
|
|
2371
2421
|
var nowMs = Date.now();
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
);
|
|
2422
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (the prescribed prefix-aware indirection)
|
|
2423
|
+
var wormTable = frameworkSchema.tableName("_blamejs_worm_tables");
|
|
2375
2424
|
for (var j = 0; j < args.tables.length; j++) {
|
|
2376
2425
|
var t = args.tables[j];
|
|
2377
2426
|
if (t === "audit_log" || t === "consent_log" || t === "audit_checkpoints") {
|
|
@@ -2380,7 +2429,14 @@ function declareWorm(args) {
|
|
|
2380
2429
|
"use audit-tools.purge for sanctioned deletions");
|
|
2381
2430
|
}
|
|
2382
2431
|
_installWormTriggers(database, t);
|
|
2383
|
-
|
|
2432
|
+
// INSERT-or-replace on the tableName PK: b.sql upsert emits the
|
|
2433
|
+
// portable ON CONFLICT DO UPDATE form (same replace-on-PK semantics
|
|
2434
|
+
// as the prior INSERT OR REPLACE).
|
|
2435
|
+
var wormUp = sql.upsert(wormTable, _SQL_OPTS)
|
|
2436
|
+
.values({ tableName: t, posture: args.posture || null, declaredAt: nowMs })
|
|
2437
|
+
.onConflict(["tableName"]).doUpdateFromExcluded(["posture", "declaredAt"]).toSql();
|
|
2438
|
+
var wormStmt = database.prepare(wormUp.sql);
|
|
2439
|
+
wormStmt.run.apply(wormStmt, wormUp.params);
|
|
2384
2440
|
audit.safeEmit({
|
|
2385
2441
|
action: "db.worm.declared",
|
|
2386
2442
|
outcome: "success",
|
|
@@ -2397,9 +2453,11 @@ function _assertWormUnderPosture() {
|
|
|
2397
2453
|
if (cluster.isClusterMode()) return;
|
|
2398
2454
|
var rows;
|
|
2399
2455
|
try {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2456
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (prefix-aware), composed via b.sql
|
|
2457
|
+
var wormSel = sql.select(frameworkSchema.tableName("_blamejs_worm_tables"), _SQL_OPTS)
|
|
2458
|
+
.columns(["tableName"]).toSql();
|
|
2459
|
+
var wormSelStmt = database.prepare(wormSel.sql);
|
|
2460
|
+
rows = wormSelStmt.all.apply(wormSelStmt, wormSel.params);
|
|
2403
2461
|
} catch (_e) { rows = []; }
|
|
2404
2462
|
if (!rows || rows.length === 0) {
|
|
2405
2463
|
throw _wormErr("POSTURE_VIOLATION",
|
|
@@ -2473,12 +2531,17 @@ function declareRequireDualControl(args) {
|
|
|
2473
2531
|
"declareRequireDualControl: args.posture must be a non-empty string or null");
|
|
2474
2532
|
}
|
|
2475
2533
|
var nowMs = Date.now();
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
'(tableName, posture, m, n, declaredAt) VALUES (?, ?, ?, ?, ?)'
|
|
2479
|
-
);
|
|
2534
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (the prescribed prefix-aware indirection)
|
|
2535
|
+
var gatesTable = frameworkSchema.tableName("_blamejs_dual_control_gates");
|
|
2480
2536
|
for (var j = 0; j < args.tables.length; j++) {
|
|
2481
|
-
|
|
2537
|
+
// INSERT-or-replace on the tableName PK via b.sql upsert (same
|
|
2538
|
+
// replace-on-PK semantics as the prior INSERT OR REPLACE).
|
|
2539
|
+
var gateUp = sql.upsert(gatesTable, _SQL_OPTS)
|
|
2540
|
+
.values({ tableName: args.tables[j], posture: args.posture || null,
|
|
2541
|
+
m: m, n: n, declaredAt: nowMs })
|
|
2542
|
+
.onConflict(["tableName"]).doUpdateFromExcluded(["posture", "m", "n", "declaredAt"]).toSql();
|
|
2543
|
+
var gateStmt = database.prepare(gateUp.sql);
|
|
2544
|
+
gateStmt.run.apply(gateStmt, gateUp.params);
|
|
2482
2545
|
audit.safeEmit({
|
|
2483
2546
|
action: "db.dual_control.declared",
|
|
2484
2547
|
outcome: "success",
|
|
@@ -2493,9 +2556,11 @@ function _checkDualControlGate(tableName) {
|
|
|
2493
2556
|
if (cluster.isClusterMode()) return null;
|
|
2494
2557
|
var row;
|
|
2495
2558
|
try {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2559
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (prefix-aware), composed via b.sql
|
|
2560
|
+
var gateSel = sql.select(frameworkSchema.tableName("_blamejs_dual_control_gates"), _SQL_OPTS)
|
|
2561
|
+
.columns(["tableName", "posture", "m", "n"]).where("tableName", tableName).toSql();
|
|
2562
|
+
var gateSelStmt = database.prepare(gateSel.sql);
|
|
2563
|
+
row = gateSelStmt.get.apply(gateSelStmt, gateSel.params);
|
|
2499
2564
|
} catch (_e) { return null; }
|
|
2500
2565
|
return row || null;
|
|
2501
2566
|
}
|
|
@@ -2582,16 +2647,18 @@ function eraseHard(tableName, rowId, opts) {
|
|
|
2582
2647
|
var t0 = Date.now();
|
|
2583
2648
|
var deleted = 0;
|
|
2584
2649
|
transaction(function () {
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
).
|
|
2650
|
+
// tableName is an operator app table (validateIdentifier'd above), not
|
|
2651
|
+
// a framework table, so it is NOT routed through tableName(); b.sql
|
|
2652
|
+
// quotes it by construction (quoteName) on the local sqlite handle.
|
|
2653
|
+
var rowSel = sql.select(tableName, _SQL_OPTS).where("_id", rowId).toSql();
|
|
2654
|
+
var rowSelStmt = database.prepare(rowSel.sql);
|
|
2655
|
+
var row = rowSelStmt.get.apply(rowSelStmt, rowSel.params);
|
|
2588
2656
|
if (row) {
|
|
2589
2657
|
try { cryptoField.eraseRow(tableName, row); } catch (_e) { /* table may have no sealed cols */ }
|
|
2590
2658
|
}
|
|
2591
|
-
var
|
|
2592
|
-
|
|
2593
|
-
);
|
|
2594
|
-
var result = del.run(rowId);
|
|
2659
|
+
var rowDel = sql.delete(tableName, _SQL_OPTS).where("_id", rowId).toSql();
|
|
2660
|
+
var rowDelStmt = database.prepare(rowDel.sql);
|
|
2661
|
+
var result = rowDelStmt.run.apply(rowDelStmt, rowDel.params);
|
|
2595
2662
|
deleted = (result && result.changes) || 0;
|
|
2596
2663
|
// REINDEX rebuilds every index on the table from scratch,
|
|
2597
2664
|
// dropping the B-tree pages that held the deleted row's index
|
|
@@ -2617,7 +2684,7 @@ function eraseHard(tableName, rowId, opts) {
|
|
|
2617
2684
|
// Read the audit.tip sidecar file in dataDir and compare to the current
|
|
2618
2685
|
// audit_log MAX(monotonicCounter). Refuse boot on rollback (current < tip).
|
|
2619
2686
|
function _checkRollback(dataDirPath) {
|
|
2620
|
-
var tipPath = nodePath.join(dataDirPath,
|
|
2687
|
+
var tipPath = nodePath.join(dataDirPath, frameworkFiles.fileName("auditTip"));
|
|
2621
2688
|
if (!nodeFs.existsSync(tipPath)) {
|
|
2622
2689
|
log("no audit.tip sidecar — skipping rollback check (first boot or operator-cleared)");
|
|
2623
2690
|
return;
|
|
@@ -2631,7 +2698,12 @@ function _checkRollback(dataDirPath) {
|
|
|
2631
2698
|
". Either delete it (forfeits rollback protection until next checkpoint) " +
|
|
2632
2699
|
"or restore from operator backup.");
|
|
2633
2700
|
}
|
|
2634
|
-
|
|
2701
|
+
// The local-SQLite chain table is named "audit_log" (its external-db
|
|
2702
|
+
// counterpart _blamejs_audit_log is the cluster path, handled elsewhere),
|
|
2703
|
+
// so the local read uses that literal name; b.sql quotes it (quoteName).
|
|
2704
|
+
var maxQ = sql.select("audit_log", _SQL_OPTS).max("monotonicCounter", "m").toSql();
|
|
2705
|
+
var maxStmt = database.prepare(maxQ.sql);
|
|
2706
|
+
var current = maxStmt.get.apply(maxStmt, maxQ.params);
|
|
2635
2707
|
var currentMax = current && current.m ? current.m : 0;
|
|
2636
2708
|
if (currentMax < tip.atMonotonicCounter) {
|
|
2637
2709
|
events.emit(events.EVENTS.AUDIT_ROLLBACK_DETECTED, {
|
|
@@ -3290,30 +3362,39 @@ module.exports = {
|
|
|
3290
3362
|
}
|
|
3291
3363
|
if (cluster.isClusterMode()) {
|
|
3292
3364
|
// External-db has no append-only triggers; ordinary DELETE works.
|
|
3365
|
+
// clusterStorage.execute rewrites the BARE logical table name to its
|
|
3366
|
+
// cluster-prefixed form and placeholderizes ?→$N, so b.sql emits the
|
|
3367
|
+
// bare name (no quoteName) here.
|
|
3293
3368
|
var cs = clusterStorage();
|
|
3294
|
-
var
|
|
3295
|
-
"
|
|
3296
|
-
);
|
|
3297
|
-
var
|
|
3298
|
-
"
|
|
3299
|
-
);
|
|
3369
|
+
var clusterLogDel = sql.delete("audit_log")
|
|
3370
|
+
.where("monotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3371
|
+
var d = await cs.execute(clusterLogDel.sql, clusterLogDel.params);
|
|
3372
|
+
var clusterChkDel = sql.delete("audit_checkpoints")
|
|
3373
|
+
.where("atMonotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3374
|
+
var dc = await cs.execute(clusterChkDel.sql, clusterChkDel.params);
|
|
3300
3375
|
return { rowsDeleted: d.rowCount || 0, checkpointsDeleted: dc.rowCount || 0 };
|
|
3301
3376
|
}
|
|
3302
3377
|
// Single-node: drop triggers, delete, recreate triggers — all in
|
|
3303
3378
|
// one transaction so a crash mid-operation doesn't leave the
|
|
3304
|
-
// table writable to general code.
|
|
3379
|
+
// table writable to general code. The local chain tables are named
|
|
3380
|
+
// "audit_log" / "audit_checkpoints" (no _blamejs_ prefix locally);
|
|
3381
|
+
// b.sql quotes them (quoteName) on the local handle.
|
|
3305
3382
|
var rowsDeleted = 0;
|
|
3306
3383
|
var checkpointsDeleted = 0;
|
|
3307
3384
|
transaction(function () {
|
|
3385
|
+
// allow:hand-rolled-sql — b.sql has no DROP TRIGGER builder; framework-controlled trigger name, append-only re-installed below
|
|
3308
3386
|
runSql(database, 'DROP TRIGGER IF EXISTS "no_delete_audit_log"');
|
|
3387
|
+
// allow:hand-rolled-sql — b.sql has no DROP TRIGGER builder; framework-controlled trigger name, append-only re-installed below
|
|
3309
3388
|
runSql(database, 'DROP TRIGGER IF EXISTS "no_delete_audit_checkpoints"');
|
|
3310
|
-
var
|
|
3311
|
-
"
|
|
3312
|
-
|
|
3389
|
+
var logDel = sql.delete("audit_log", _SQL_OPTS)
|
|
3390
|
+
.where("monotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3391
|
+
var logDelStmt = database.prepare(logDel.sql);
|
|
3392
|
+
var d = logDelStmt.run.apply(logDelStmt, logDel.params);
|
|
3313
3393
|
rowsDeleted = (d && d.changes) || 0;
|
|
3314
|
-
var
|
|
3315
|
-
"
|
|
3316
|
-
|
|
3394
|
+
var chkDel = sql.delete("audit_checkpoints", _SQL_OPTS)
|
|
3395
|
+
.where("atMonotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3396
|
+
var chkDelStmt = database.prepare(chkDel.sql);
|
|
3397
|
+
var dc = chkDelStmt.run.apply(chkDelStmt, chkDel.params);
|
|
3317
3398
|
checkpointsDeleted = (dc && dc.changes) || 0;
|
|
3318
3399
|
_installAppendOnlyTriggers(database);
|
|
3319
3400
|
});
|
|
@@ -3415,7 +3496,7 @@ module.exports = {
|
|
|
3415
3496
|
// Helper for audit.checkpoint to write the rollback-detection sidecar
|
|
3416
3497
|
_writeAuditTip: function (tip) {
|
|
3417
3498
|
if (!dataDir) return;
|
|
3418
|
-
var tipPath = nodePath.join(dataDir,
|
|
3499
|
+
var tipPath = nodePath.join(dataDir, frameworkFiles.fileName("auditTip"));
|
|
3419
3500
|
atomicFile.writeSync(tipPath, JSON.stringify(tip, null, 2), { fileMode: 0o600 });
|
|
3420
3501
|
},
|
|
3421
3502
|
};
|
package/lib/error-page.js
CHANGED
|
@@ -373,9 +373,22 @@ function create(opts) {
|
|
|
373
373
|
// Audit every error. Best-effort — never let an audit-write failure
|
|
374
374
|
// mask the original error. Outcome differentiates 5xx (failure) vs
|
|
375
375
|
// 4xx (denied) so consumers can filter without re-classifying status.
|
|
376
|
+
//
|
|
377
|
+
// Use safeEmit, not emit: the metadata.stack and reason fields carry
|
|
378
|
+
// the original exception's stack + message, which routinely embed
|
|
379
|
+
// secrets (a database connection string, an API key, a bearer token
|
|
380
|
+
// surfaced inside a thrown error). emit() writes straight to the
|
|
381
|
+
// tamper-evident, durable audit chain WITHOUT redaction, so those
|
|
382
|
+
// secrets would persist in plaintext in the signed log
|
|
383
|
+
// (CWE-532: insertion of sensitive information into log file).
|
|
384
|
+
// safeEmit runs b.redact.redact() over actor / reason / metadata —
|
|
385
|
+
// including nested keys like metadata.stack — before the record
|
|
386
|
+
// reaches the chain, scrubbing connection strings, JWTs, PEM blocks,
|
|
387
|
+
// and AWS keys. safeEmit is also drop-silent on malformed input,
|
|
388
|
+
// matching this hot-path "audit best-effort" posture.
|
|
376
389
|
if (auditOn) {
|
|
377
390
|
try {
|
|
378
|
-
audit().
|
|
391
|
+
audit().safeEmit({
|
|
379
392
|
action: auditAction,
|
|
380
393
|
outcome: info.status >= 500 ? "failure" : "denied",
|
|
381
394
|
actor: requestHelpers.extractActorContext(req, {
|