@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/ai-content-detect.js +9 -10
  5. package/lib/api-key.js +158 -77
  6. package/lib/atomic-file.js +29 -1
  7. package/lib/audit-chain.js +47 -11
  8. package/lib/audit-sign.js +77 -2
  9. package/lib/audit-tools.js +79 -51
  10. package/lib/audit.js +228 -100
  11. package/lib/backup/index.js +13 -10
  12. package/lib/break-glass.js +202 -144
  13. package/lib/cache.js +174 -105
  14. package/lib/chain-writer.js +38 -16
  15. package/lib/cli.js +19 -14
  16. package/lib/cluster-provider-db.js +130 -104
  17. package/lib/cluster-storage.js +119 -22
  18. package/lib/cluster.js +119 -71
  19. package/lib/compliance.js +22 -0
  20. package/lib/consent.js +82 -29
  21. package/lib/constants.js +16 -11
  22. package/lib/crypto-field.js +387 -91
  23. package/lib/db-declare-row-policy.js +35 -22
  24. package/lib/db-file-lifecycle.js +3 -2
  25. package/lib/db-query.js +517 -256
  26. package/lib/db-schema.js +209 -44
  27. package/lib/db.js +202 -95
  28. package/lib/external-db-migrate.js +229 -139
  29. package/lib/external-db.js +25 -15
  30. package/lib/framework-error.js +11 -0
  31. package/lib/framework-files.js +73 -0
  32. package/lib/framework-schema.js +695 -394
  33. package/lib/gate-contract.js +596 -1
  34. package/lib/guard-agent-registry.js +26 -44
  35. package/lib/guard-all.js +1 -0
  36. package/lib/guard-auth.js +42 -112
  37. package/lib/guard-cidr.js +33 -154
  38. package/lib/guard-csv.js +46 -113
  39. package/lib/guard-domain.js +34 -157
  40. package/lib/guard-dsn.js +27 -43
  41. package/lib/guard-email.js +47 -69
  42. package/lib/guard-envelope.js +19 -32
  43. package/lib/guard-event-bus-payload.js +24 -42
  44. package/lib/guard-event-bus-topic.js +25 -43
  45. package/lib/guard-filename.js +42 -106
  46. package/lib/guard-graphql.js +42 -123
  47. package/lib/guard-html.js +53 -108
  48. package/lib/guard-idempotency-key.js +24 -42
  49. package/lib/guard-image.js +46 -103
  50. package/lib/guard-imap-command.js +18 -32
  51. package/lib/guard-jmap.js +16 -30
  52. package/lib/guard-json.js +38 -108
  53. package/lib/guard-jsonpath.js +38 -171
  54. package/lib/guard-jwt.js +49 -179
  55. package/lib/guard-list-id.js +25 -41
  56. package/lib/guard-list-unsubscribe.js +27 -43
  57. package/lib/guard-mail-compose.js +24 -42
  58. package/lib/guard-mail-move.js +26 -44
  59. package/lib/guard-mail-query.js +28 -46
  60. package/lib/guard-mail-reply.js +24 -42
  61. package/lib/guard-mail-sieve.js +24 -42
  62. package/lib/guard-managesieve-command.js +17 -31
  63. package/lib/guard-markdown.js +37 -104
  64. package/lib/guard-message-id.js +26 -45
  65. package/lib/guard-mime.js +39 -151
  66. package/lib/guard-oauth.js +54 -135
  67. package/lib/guard-pdf.js +45 -101
  68. package/lib/guard-pop3-command.js +21 -31
  69. package/lib/guard-posture-chain.js +24 -42
  70. package/lib/guard-regex.js +33 -107
  71. package/lib/guard-saga-config.js +24 -42
  72. package/lib/guard-shell.js +42 -172
  73. package/lib/guard-smtp-command.js +48 -54
  74. package/lib/guard-snapshot-envelope.js +24 -42
  75. package/lib/guard-sql.js +1491 -0
  76. package/lib/guard-stream-args.js +24 -43
  77. package/lib/guard-svg.js +47 -65
  78. package/lib/guard-template.js +35 -172
  79. package/lib/guard-tenant-id.js +26 -45
  80. package/lib/guard-time.js +32 -154
  81. package/lib/guard-trace-context.js +25 -44
  82. package/lib/guard-uuid.js +32 -153
  83. package/lib/guard-xml.js +38 -113
  84. package/lib/guard-yaml.js +51 -163
  85. package/lib/http-client.js +14 -0
  86. package/lib/inbox.js +120 -107
  87. package/lib/legal-hold.js +107 -50
  88. package/lib/log-stream-cloudwatch.js +47 -31
  89. package/lib/log-stream-otlp.js +32 -18
  90. package/lib/mail-crypto-smime.js +2 -6
  91. package/lib/mail-greylist.js +2 -6
  92. package/lib/mail-helo.js +2 -6
  93. package/lib/mail-journal.js +85 -64
  94. package/lib/mail-rbl.js +2 -6
  95. package/lib/mail-scan.js +2 -6
  96. package/lib/mail-spam-score.js +2 -6
  97. package/lib/mail-store.js +293 -154
  98. package/lib/middleware/fetch-metadata.js +17 -7
  99. package/lib/middleware/idempotency-key.js +54 -38
  100. package/lib/middleware/rate-limit.js +102 -32
  101. package/lib/middleware/security-headers.js +21 -5
  102. package/lib/migrations.js +108 -66
  103. package/lib/network-heartbeat.js +7 -0
  104. package/lib/nonce-store.js +31 -9
  105. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  106. package/lib/object-store/azure-blob.js +31 -3
  107. package/lib/object-store/sigv4.js +10 -0
  108. package/lib/outbox.js +136 -82
  109. package/lib/pqc-agent.js +44 -0
  110. package/lib/pubsub-cluster.js +42 -20
  111. package/lib/queue-local.js +202 -139
  112. package/lib/queue-redis.js +9 -1
  113. package/lib/queue-sqs.js +6 -0
  114. package/lib/retention.js +82 -39
  115. package/lib/safe-dns.js +29 -45
  116. package/lib/safe-ical.js +18 -33
  117. package/lib/safe-icap.js +27 -43
  118. package/lib/safe-sieve.js +21 -40
  119. package/lib/safe-sql.js +124 -3
  120. package/lib/safe-vcard.js +18 -33
  121. package/lib/scheduler.js +35 -12
  122. package/lib/seeders.js +122 -74
  123. package/lib/session-stores.js +42 -14
  124. package/lib/session.js +116 -72
  125. package/lib/sql.js +3885 -0
  126. package/lib/static.js +45 -7
  127. package/lib/subject.js +89 -49
  128. package/lib/vault/index.js +3 -2
  129. package/lib/vault/passphrase-ops.js +3 -2
  130. package/lib/vault/rotate.js +104 -64
  131. package/lib/vendor-data.js +2 -0
  132. package/lib/websocket.js +16 -0
  133. package/package.json +1 -1
  134. 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, "db.key.enc");
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
- // plaintext DB file lives on persistent storage. We check that tmpDir
1059
- // resolves under /dev/shm or /run/shm on Linux as a heuristic; on other
1060
- // platforms we warn that the operator must verify tmpfs binding
1061
- // out-of-band. (Free-space headroom is enforced separately via
1062
- // fs.statfsSync in the storage guard below.)
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
- log.warn("WARNING: db.init: tmpDir '" + tmpDir + "' (real: '" + realTmp +
1069
- "') does not resolve under /dev/shm /run/shm /run/user /tmp — verify it is " +
1070
- "actually a tmpfs mount. A persistent-disk tmpDir leaks plaintext into backup " +
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 || "db.enc");
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
- var ins = database.prepare(
2373
- 'INSERT OR REPLACE INTO "_blamejs_worm_tables" (tableName, posture, declaredAt) VALUES (?, ?, ?)'
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
- ins.run(t, args.posture || null, nowMs);
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
- rows = database.prepare(
2401
- 'SELECT tableName FROM "_blamejs_worm_tables"'
2402
- ).all();
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
- var ins = database.prepare(
2477
- 'INSERT OR REPLACE INTO "_blamejs_dual_control_gates" ' +
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
- ins.run(args.tables[j], args.posture || null, m, n, nowMs);
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
- row = database.prepare(
2497
- 'SELECT tableName, posture, m, n FROM "_blamejs_dual_control_gates" WHERE tableName = ?'
2498
- ).get(tableName);
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
- var row = database.prepare(
2586
- 'SELECT * FROM "' + tableName + '" WHERE _id = ?'
2587
- ).get(rowId);
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 del = database.prepare(
2592
- 'DELETE FROM "' + tableName + '" WHERE _id = ?'
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, "audit.tip");
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
- var current = database.prepare("SELECT MAX(monotonicCounter) AS m FROM audit_log").get();
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 d = await cs.execute(
3295
- "DELETE FROM audit_log WHERE monotonicCounter <= ?", [lastPurgedCounter]
3296
- );
3297
- var dc = await cs.execute(
3298
- "DELETE FROM audit_checkpoints WHERE atMonotonicCounter <= ?", [lastPurgedCounter]
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 d = database.prepare(
3311
- "DELETE FROM audit_log WHERE monotonicCounter <= ?"
3312
- ).run(lastPurgedCounter);
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 dc = database.prepare(
3315
- "DELETE FROM audit_checkpoints WHERE atMonotonicCounter <= ?"
3316
- ).run(lastPurgedCounter);
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, "audit.tip");
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
  };