@blamejs/core 0.14.27 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +158 -77
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +228 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +82 -29
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +517 -256
- package/lib/db-schema.js +209 -44
- package/lib/db.js +202 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +293 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +116 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +89 -49
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/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
|
|
|
@@ -1940,6 +1984,31 @@ function hashFor(table, field, value) {
|
|
|
1940
1984
|
return lookup ? lookup.value : null;
|
|
1941
1985
|
}
|
|
1942
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* @primitive b.db.hashCandidatesFor
|
|
1989
|
+
* @signature b.db.hashCandidatesFor(table, field, value)
|
|
1990
|
+
* @since 0.15.1
|
|
1991
|
+
* @status stable
|
|
1992
|
+
* @related b.db.hashFor, b.db.from
|
|
1993
|
+
*
|
|
1994
|
+
* Dual-read sibling of `hashFor`. Returns `{ field, values }` where `values`
|
|
1995
|
+
* holds the active derived-hash digest AND — across the v0.15.0 keyed-MAC
|
|
1996
|
+
* default flip — the legacy salted-sha3 digest a row written before the flip
|
|
1997
|
+
* carries. A `WHERE <hashColumn> IN (...)` lookup over `values` matches both
|
|
1998
|
+
* keyed-indexed and legacy-indexed rows, so the flip never silently drops an
|
|
1999
|
+
* un-migrated row. Returns `null` when the field has no derived-hash
|
|
2000
|
+
* declaration on the table.
|
|
2001
|
+
*
|
|
2002
|
+
* @example
|
|
2003
|
+
* var c = b.db.hashCandidatesFor("users", "email", "alice@example.com");
|
|
2004
|
+
* b.db.from("users").whereIn(c.field, c.values).all();
|
|
2005
|
+
* // → rows matching either the keyed-MAC or the legacy digest
|
|
2006
|
+
*/
|
|
2007
|
+
function hashCandidatesFor(table, field, value) {
|
|
2008
|
+
_requireInit();
|
|
2009
|
+
return cryptoField.lookupHashCandidates(table, field, value);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1943
2012
|
// _ddlToJsonSchemaType — best-effort SQL→JSON Schema type mapping.
|
|
1944
2013
|
// SQLite is dynamically typed but the framework's DDL syntax pins
|
|
1945
2014
|
// concrete types; we map them here. Operator-supplied custom types
|
|
@@ -2264,21 +2333,24 @@ function _normalizePk(tableSpec) {
|
|
|
2264
2333
|
// that RAISE(ABORT) the operation. INSERT remains permitted (that's what
|
|
2265
2334
|
// audit.record / consent.grant do).
|
|
2266
2335
|
function _installAppendOnlyTriggers(database) {
|
|
2336
|
+
// b.sql has no CREATE TRIGGER builder — these append-only WORM triggers
|
|
2337
|
+
// are SQLite-specific (RAISE(ABORT) trigger bodies) over framework-
|
|
2338
|
+
// controlled, fixed table names. Identifiers are quoted by construction.
|
|
2267
2339
|
var tables = ["audit_log", "consent_log", "audit_checkpoints"];
|
|
2268
2340
|
for (var i = 0; i < tables.length; i++) {
|
|
2269
2341
|
var t = tables[i];
|
|
2270
2342
|
runSql(database,
|
|
2271
|
-
'CREATE TRIGGER IF NOT EXISTS "no_delete_' + t + '" ' +
|
|
2343
|
+
'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
2344
|
'BEFORE DELETE ON "' + t + '" ' +
|
|
2273
2345
|
'BEGIN ' +
|
|
2274
|
-
" SELECT RAISE(ABORT, '" + t + " is append-only — DELETE prohibited'); " +
|
|
2346
|
+
" SELECT RAISE(ABORT, '" + t + " is append-only — DELETE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2275
2347
|
'END'
|
|
2276
2348
|
);
|
|
2277
2349
|
runSql(database,
|
|
2278
|
-
'CREATE TRIGGER IF NOT EXISTS "no_update_' + t + '" ' +
|
|
2350
|
+
'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
2351
|
'BEFORE UPDATE ON "' + t + '" ' +
|
|
2280
2352
|
'BEGIN ' +
|
|
2281
|
-
" SELECT RAISE(ABORT, '" + t + " is append-only — UPDATE prohibited'); " +
|
|
2353
|
+
" SELECT RAISE(ABORT, '" + t + " is append-only — UPDATE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2282
2354
|
'END'
|
|
2283
2355
|
);
|
|
2284
2356
|
}
|
|
@@ -2291,19 +2363,22 @@ function _installAppendOnlyTriggers(database) {
|
|
|
2291
2363
|
// boot-time assertion under WORM_POSTURES catches operators who
|
|
2292
2364
|
// set the posture without declaring tables.
|
|
2293
2365
|
function _installWormTriggers(database, tableName) {
|
|
2366
|
+
// b.sql has no CREATE TRIGGER builder — operator-table WORM triggers are
|
|
2367
|
+
// SQLite-specific RAISE(ABORT) trigger bodies; tableName is validated
|
|
2368
|
+
// (validateIdentifier) and quoted by construction.
|
|
2294
2369
|
safeSql.validateIdentifier(tableName);
|
|
2295
2370
|
runSql(database,
|
|
2296
|
-
'CREATE TRIGGER IF NOT EXISTS "worm_no_delete_' + tableName + '" ' +
|
|
2371
|
+
'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
2372
|
'BEFORE DELETE ON "' + tableName + '" ' +
|
|
2298
2373
|
'BEGIN ' +
|
|
2299
|
-
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - DELETE prohibited'); " +
|
|
2374
|
+
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - DELETE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2300
2375
|
'END'
|
|
2301
2376
|
);
|
|
2302
2377
|
runSql(database,
|
|
2303
|
-
'CREATE TRIGGER IF NOT EXISTS "worm_no_update_' + tableName + '" ' +
|
|
2378
|
+
'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
2379
|
'BEFORE UPDATE ON "' + tableName + '" ' +
|
|
2305
2380
|
'BEGIN ' +
|
|
2306
|
-
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - UPDATE prohibited'); " +
|
|
2381
|
+
" SELECT RAISE(ABORT, '" + tableName + " is WORM (write-once-read-many) - UPDATE prohibited'); " + // allow:hand-rolled-sql — RAISE(ABORT) trigger body, not a query
|
|
2307
2382
|
'END'
|
|
2308
2383
|
);
|
|
2309
2384
|
}
|
|
@@ -2369,9 +2444,8 @@ function declareWorm(args) {
|
|
|
2369
2444
|
"the SQLite trigger primitive is single-node only");
|
|
2370
2445
|
}
|
|
2371
2446
|
var nowMs = Date.now();
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
);
|
|
2447
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (the prescribed prefix-aware indirection)
|
|
2448
|
+
var wormTable = frameworkSchema.tableName("_blamejs_worm_tables");
|
|
2375
2449
|
for (var j = 0; j < args.tables.length; j++) {
|
|
2376
2450
|
var t = args.tables[j];
|
|
2377
2451
|
if (t === "audit_log" || t === "consent_log" || t === "audit_checkpoints") {
|
|
@@ -2380,7 +2454,14 @@ function declareWorm(args) {
|
|
|
2380
2454
|
"use audit-tools.purge for sanctioned deletions");
|
|
2381
2455
|
}
|
|
2382
2456
|
_installWormTriggers(database, t);
|
|
2383
|
-
|
|
2457
|
+
// INSERT-or-replace on the tableName PK: b.sql upsert emits the
|
|
2458
|
+
// portable ON CONFLICT DO UPDATE form (same replace-on-PK semantics
|
|
2459
|
+
// as the prior INSERT OR REPLACE).
|
|
2460
|
+
var wormUp = sql.upsert(wormTable, _SQL_OPTS)
|
|
2461
|
+
.values({ tableName: t, posture: args.posture || null, declaredAt: nowMs })
|
|
2462
|
+
.onConflict(["tableName"]).doUpdateFromExcluded(["posture", "declaredAt"]).toSql();
|
|
2463
|
+
var wormStmt = database.prepare(wormUp.sql);
|
|
2464
|
+
wormStmt.run.apply(wormStmt, wormUp.params);
|
|
2384
2465
|
audit.safeEmit({
|
|
2385
2466
|
action: "db.worm.declared",
|
|
2386
2467
|
outcome: "success",
|
|
@@ -2397,9 +2478,11 @@ function _assertWormUnderPosture() {
|
|
|
2397
2478
|
if (cluster.isClusterMode()) return;
|
|
2398
2479
|
var rows;
|
|
2399
2480
|
try {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2481
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (prefix-aware), composed via b.sql
|
|
2482
|
+
var wormSel = sql.select(frameworkSchema.tableName("_blamejs_worm_tables"), _SQL_OPTS)
|
|
2483
|
+
.columns(["tableName"]).toSql();
|
|
2484
|
+
var wormSelStmt = database.prepare(wormSel.sql);
|
|
2485
|
+
rows = wormSelStmt.all.apply(wormSelStmt, wormSel.params);
|
|
2403
2486
|
} catch (_e) { rows = []; }
|
|
2404
2487
|
if (!rows || rows.length === 0) {
|
|
2405
2488
|
throw _wormErr("POSTURE_VIOLATION",
|
|
@@ -2473,12 +2556,17 @@ function declareRequireDualControl(args) {
|
|
|
2473
2556
|
"declareRequireDualControl: args.posture must be a non-empty string or null");
|
|
2474
2557
|
}
|
|
2475
2558
|
var nowMs = Date.now();
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
'(tableName, posture, m, n, declaredAt) VALUES (?, ?, ?, ?, ?)'
|
|
2479
|
-
);
|
|
2559
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (the prescribed prefix-aware indirection)
|
|
2560
|
+
var gatesTable = frameworkSchema.tableName("_blamejs_dual_control_gates");
|
|
2480
2561
|
for (var j = 0; j < args.tables.length; j++) {
|
|
2481
|
-
|
|
2562
|
+
// INSERT-or-replace on the tableName PK via b.sql upsert (same
|
|
2563
|
+
// replace-on-PK semantics as the prior INSERT OR REPLACE).
|
|
2564
|
+
var gateUp = sql.upsert(gatesTable, _SQL_OPTS)
|
|
2565
|
+
.values({ tableName: args.tables[j], posture: args.posture || null,
|
|
2566
|
+
m: m, n: n, declaredAt: nowMs })
|
|
2567
|
+
.onConflict(["tableName"]).doUpdateFromExcluded(["posture", "m", "n", "declaredAt"]).toSql();
|
|
2568
|
+
var gateStmt = database.prepare(gateUp.sql);
|
|
2569
|
+
gateStmt.run.apply(gateStmt, gateUp.params);
|
|
2482
2570
|
audit.safeEmit({
|
|
2483
2571
|
action: "db.dual_control.declared",
|
|
2484
2572
|
outcome: "success",
|
|
@@ -2493,9 +2581,11 @@ function _checkDualControlGate(tableName) {
|
|
|
2493
2581
|
if (cluster.isClusterMode()) return null;
|
|
2494
2582
|
var row;
|
|
2495
2583
|
try {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2584
|
+
// allow:hand-rolled-sql — logical name resolved through frameworkSchema.tableName (prefix-aware), composed via b.sql
|
|
2585
|
+
var gateSel = sql.select(frameworkSchema.tableName("_blamejs_dual_control_gates"), _SQL_OPTS)
|
|
2586
|
+
.columns(["tableName", "posture", "m", "n"]).where("tableName", tableName).toSql();
|
|
2587
|
+
var gateSelStmt = database.prepare(gateSel.sql);
|
|
2588
|
+
row = gateSelStmt.get.apply(gateSelStmt, gateSel.params);
|
|
2499
2589
|
} catch (_e) { return null; }
|
|
2500
2590
|
return row || null;
|
|
2501
2591
|
}
|
|
@@ -2582,16 +2672,18 @@ function eraseHard(tableName, rowId, opts) {
|
|
|
2582
2672
|
var t0 = Date.now();
|
|
2583
2673
|
var deleted = 0;
|
|
2584
2674
|
transaction(function () {
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
).
|
|
2675
|
+
// tableName is an operator app table (validateIdentifier'd above), not
|
|
2676
|
+
// a framework table, so it is NOT routed through tableName(); b.sql
|
|
2677
|
+
// quotes it by construction (quoteName) on the local sqlite handle.
|
|
2678
|
+
var rowSel = sql.select(tableName, _SQL_OPTS).where("_id", rowId).toSql();
|
|
2679
|
+
var rowSelStmt = database.prepare(rowSel.sql);
|
|
2680
|
+
var row = rowSelStmt.get.apply(rowSelStmt, rowSel.params);
|
|
2588
2681
|
if (row) {
|
|
2589
2682
|
try { cryptoField.eraseRow(tableName, row); } catch (_e) { /* table may have no sealed cols */ }
|
|
2590
2683
|
}
|
|
2591
|
-
var
|
|
2592
|
-
|
|
2593
|
-
);
|
|
2594
|
-
var result = del.run(rowId);
|
|
2684
|
+
var rowDel = sql.delete(tableName, _SQL_OPTS).where("_id", rowId).toSql();
|
|
2685
|
+
var rowDelStmt = database.prepare(rowDel.sql);
|
|
2686
|
+
var result = rowDelStmt.run.apply(rowDelStmt, rowDel.params);
|
|
2595
2687
|
deleted = (result && result.changes) || 0;
|
|
2596
2688
|
// REINDEX rebuilds every index on the table from scratch,
|
|
2597
2689
|
// dropping the B-tree pages that held the deleted row's index
|
|
@@ -2617,7 +2709,7 @@ function eraseHard(tableName, rowId, opts) {
|
|
|
2617
2709
|
// Read the audit.tip sidecar file in dataDir and compare to the current
|
|
2618
2710
|
// audit_log MAX(monotonicCounter). Refuse boot on rollback (current < tip).
|
|
2619
2711
|
function _checkRollback(dataDirPath) {
|
|
2620
|
-
var tipPath = nodePath.join(dataDirPath,
|
|
2712
|
+
var tipPath = nodePath.join(dataDirPath, frameworkFiles.fileName("auditTip"));
|
|
2621
2713
|
if (!nodeFs.existsSync(tipPath)) {
|
|
2622
2714
|
log("no audit.tip sidecar — skipping rollback check (first boot or operator-cleared)");
|
|
2623
2715
|
return;
|
|
@@ -2631,7 +2723,12 @@ function _checkRollback(dataDirPath) {
|
|
|
2631
2723
|
". Either delete it (forfeits rollback protection until next checkpoint) " +
|
|
2632
2724
|
"or restore from operator backup.");
|
|
2633
2725
|
}
|
|
2634
|
-
|
|
2726
|
+
// The local-SQLite chain table is named "audit_log" (its external-db
|
|
2727
|
+
// counterpart _blamejs_audit_log is the cluster path, handled elsewhere),
|
|
2728
|
+
// so the local read uses that literal name; b.sql quotes it (quoteName).
|
|
2729
|
+
var maxQ = sql.select("audit_log", _SQL_OPTS).max("monotonicCounter", "m").toSql();
|
|
2730
|
+
var maxStmt = database.prepare(maxQ.sql);
|
|
2731
|
+
var current = maxStmt.get.apply(maxStmt, maxQ.params);
|
|
2635
2732
|
var currentMax = current && current.m ? current.m : 0;
|
|
2636
2733
|
if (currentMax < tip.atMonotonicCounter) {
|
|
2637
2734
|
events.emit(events.EVENTS.AUDIT_ROLLBACK_DETECTED, {
|
|
@@ -3182,6 +3279,7 @@ module.exports = {
|
|
|
3182
3279
|
["e" + "xec"]: execRaw,
|
|
3183
3280
|
transaction: transaction,
|
|
3184
3281
|
hashFor: hashFor,
|
|
3282
|
+
hashCandidatesFor: hashCandidatesFor,
|
|
3185
3283
|
close: close,
|
|
3186
3284
|
// flushToDisk — force the live tmpfs SQLite to be re-encrypted to
|
|
3187
3285
|
// <dataDir>/db.enc immediately. In encrypted-at-rest mode the
|
|
@@ -3290,30 +3388,39 @@ module.exports = {
|
|
|
3290
3388
|
}
|
|
3291
3389
|
if (cluster.isClusterMode()) {
|
|
3292
3390
|
// External-db has no append-only triggers; ordinary DELETE works.
|
|
3391
|
+
// clusterStorage.execute rewrites the BARE logical table name to its
|
|
3392
|
+
// cluster-prefixed form and placeholderizes ?→$N, so b.sql emits the
|
|
3393
|
+
// bare name (no quoteName) here.
|
|
3293
3394
|
var cs = clusterStorage();
|
|
3294
|
-
var
|
|
3295
|
-
"
|
|
3296
|
-
);
|
|
3297
|
-
var
|
|
3298
|
-
"
|
|
3299
|
-
);
|
|
3395
|
+
var clusterLogDel = sql.delete("audit_log")
|
|
3396
|
+
.where("monotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3397
|
+
var d = await cs.execute(clusterLogDel.sql, clusterLogDel.params);
|
|
3398
|
+
var clusterChkDel = sql.delete("audit_checkpoints")
|
|
3399
|
+
.where("atMonotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3400
|
+
var dc = await cs.execute(clusterChkDel.sql, clusterChkDel.params);
|
|
3300
3401
|
return { rowsDeleted: d.rowCount || 0, checkpointsDeleted: dc.rowCount || 0 };
|
|
3301
3402
|
}
|
|
3302
3403
|
// Single-node: drop triggers, delete, recreate triggers — all in
|
|
3303
3404
|
// one transaction so a crash mid-operation doesn't leave the
|
|
3304
|
-
// table writable to general code.
|
|
3405
|
+
// table writable to general code. The local chain tables are named
|
|
3406
|
+
// "audit_log" / "audit_checkpoints" (no _blamejs_ prefix locally);
|
|
3407
|
+
// b.sql quotes them (quoteName) on the local handle.
|
|
3305
3408
|
var rowsDeleted = 0;
|
|
3306
3409
|
var checkpointsDeleted = 0;
|
|
3307
3410
|
transaction(function () {
|
|
3411
|
+
// allow:hand-rolled-sql — b.sql has no DROP TRIGGER builder; framework-controlled trigger name, append-only re-installed below
|
|
3308
3412
|
runSql(database, 'DROP TRIGGER IF EXISTS "no_delete_audit_log"');
|
|
3413
|
+
// allow:hand-rolled-sql — b.sql has no DROP TRIGGER builder; framework-controlled trigger name, append-only re-installed below
|
|
3309
3414
|
runSql(database, 'DROP TRIGGER IF EXISTS "no_delete_audit_checkpoints"');
|
|
3310
|
-
var
|
|
3311
|
-
"
|
|
3312
|
-
|
|
3415
|
+
var logDel = sql.delete("audit_log", _SQL_OPTS)
|
|
3416
|
+
.where("monotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3417
|
+
var logDelStmt = database.prepare(logDel.sql);
|
|
3418
|
+
var d = logDelStmt.run.apply(logDelStmt, logDel.params);
|
|
3313
3419
|
rowsDeleted = (d && d.changes) || 0;
|
|
3314
|
-
var
|
|
3315
|
-
"
|
|
3316
|
-
|
|
3420
|
+
var chkDel = sql.delete("audit_checkpoints", _SQL_OPTS)
|
|
3421
|
+
.where("atMonotonicCounter", "<=", lastPurgedCounter).toSql();
|
|
3422
|
+
var chkDelStmt = database.prepare(chkDel.sql);
|
|
3423
|
+
var dc = chkDelStmt.run.apply(chkDelStmt, chkDel.params);
|
|
3317
3424
|
checkpointsDeleted = (dc && dc.changes) || 0;
|
|
3318
3425
|
_installAppendOnlyTriggers(database);
|
|
3319
3426
|
});
|
|
@@ -3415,7 +3522,7 @@ module.exports = {
|
|
|
3415
3522
|
// Helper for audit.checkpoint to write the rollback-detection sidecar
|
|
3416
3523
|
_writeAuditTip: function (tip) {
|
|
3417
3524
|
if (!dataDir) return;
|
|
3418
|
-
var tipPath = nodePath.join(dataDir,
|
|
3525
|
+
var tipPath = nodePath.join(dataDir, frameworkFiles.fileName("auditTip"));
|
|
3419
3526
|
atomicFile.writeSync(tipPath, JSON.stringify(tip, null, 2), { fileMode: 0o600 });
|
|
3420
3527
|
},
|
|
3421
3528
|
};
|