@blamejs/core 0.14.27 → 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.
Files changed (134) hide show
  1. package/CHANGELOG.md +4 -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 +107 -74
  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 +218 -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 +73 -24
  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 +497 -255
  26. package/lib/db-schema.js +209 -44
  27. package/lib/db.js +176 -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 +287 -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 +109 -72
  125. package/lib/sql.js +3885 -0
  126. package/lib/static.js +45 -7
  127. package/lib/subject.js +55 -17
  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-schema.js CHANGED
@@ -37,10 +37,18 @@
37
37
  * swallowing the original error.
38
38
  */
39
39
  var nodePath = require("node:path");
40
+ var lazyRequire = require("./lazy-require");
40
41
  var atomicFile = require("./atomic-file");
42
+ var frameworkSchema = require("./framework-schema");
41
43
  var safeSql = require("./safe-sql");
44
+ var sql = require("./sql");
42
45
  var observability = require("./observability");
43
46
 
47
+ // Lazy to break the db-schema -> compliance -> (audit/db) load chain.
48
+ // resolveDriftMode reads the globally-pinned posture so a regulated
49
+ // deployment refuses to boot under undeclared schema drift by default.
50
+ var compliance = lazyRequire(function () { return require("./compliance"); });
51
+
44
52
  // SQLite raw-SQL helper. node:sqlite DatabaseSync exposes a method on the
45
53
  // database object that runs raw SQL without bind parameters — used for DDL,
46
54
  // BEGIN/COMMIT/ROLLBACK, and PRAGMA. Bracket notation here avoids a
@@ -92,20 +100,22 @@ function runInTransaction(db, fn, opts) {
92
100
 
93
101
  // ---- Internal migrations table ----
94
102
 
95
- var MIGRATIONS_TABLE = "_blamejs_migrations";
96
- // Pre-quoted for SQL interpolation keeps the call sites consistent
97
- // with lib/migrations.js and lib/seeders.js so an identifier rename
98
- // doesn't silently break.
99
- var Q_MIGRATIONS_TABLE = '"' + MIGRATIONS_TABLE + '"';
103
+ // Logical name; the physical name + configured prefix resolve through
104
+ // frameworkSchema.tableName, and every statement composes b.sql
105
+ // (quoteName: true) so the resolved name is quoted by construction.
106
+ var MIGRATIONS_TABLE = "_blamejs_migrations"; // allow:hand-rolled-sql logical name declaration; physical name + prefix resolve via frameworkSchema.tableName
107
+ function _migrationsTable() { return frameworkSchema.tableName(MIGRATIONS_TABLE); }
108
+ // b.sql opts for the local single-node sqlite handle this module's helpers
109
+ // run against (database.exec / database.prepare, never clusterStorage):
110
+ // "sqlite" dialect + quoteName so the resolved framework name quotes.
111
+ var _SQL_OPTS = { dialect: "sqlite", quoteName: true };
100
112
 
101
113
  function ensureMigrationsTable(database) {
102
- runSql(database,
103
- "CREATE TABLE IF NOT EXISTS " + Q_MIGRATIONS_TABLE + " (" +
104
- " name TEXT PRIMARY KEY," +
105
- " description TEXT," +
106
- " appliedAt TEXT NOT NULL" +
107
- ")"
108
- );
114
+ runSql(database, sql.createTable(_migrationsTable(), [
115
+ { name: "name", type: "text", primaryKey: true },
116
+ { name: "description", type: "text" },
117
+ { name: "appliedAt", type: "text", notNull: true },
118
+ ], _SQL_OPTS).sql);
109
119
  }
110
120
 
111
121
  // ---- Declarative reconcile ----
@@ -114,7 +124,7 @@ function ensureMigrationsTable(database) {
114
124
  // additive ALTER TABLE ADD COLUMN + CREATE INDEX IF NOT EXISTS for every
115
125
  // table in `schema`. Never drops columns or tables (data-loss safety).
116
126
  //
117
- // `opts.onDrift` adds opt-in detection of config-vs-live divergence — a
127
+ // `opts.onDrift` controls detection of config-vs-live divergence — a
118
128
  // compliance-evidence concern: the live DB should match the declared data
119
129
  // model so an auditor can trust the schema config as ground truth (the
120
130
  // change-/configuration-management control families in ISO 27001:2022
@@ -131,14 +141,24 @@ function ensureMigrationsTable(database) {
131
141
  // contract; this is detection + an operator-chosen reaction only.
132
142
  //
133
143
  // onDrift values (config-time enum; bad value throws):
134
- // "ignore" (default) pre-detection behavior, byte-for-byte; no
135
- // detection side effects. Existing deployments
136
- // with drift are not broken.
144
+ // "ignore" — no detection side effects. Existing deployments with
145
+ // benign drift are not broken.
137
146
  // "warn" — detect + emit a "db.schema.drift" observability event per
138
147
  // drifted table; never throws.
139
148
  // "refuse" — detect + THROW on the first drifted table, so a strict-
140
- // schema posture refuses to boot under divergence. The
141
- // operator's explicit posture choice.
149
+ // schema posture refuses to boot under divergence.
150
+ //
151
+ // Default (v0.15.0): "ignore" on an unpinned / non-regulated deployment
152
+ // (back-compat); "refuse" when a regulated compliance posture is
153
+ // globally pinned (b.compliance.set) and the operator did not pass an
154
+ // explicit onDrift. The live DB diverging from the declared data model
155
+ // is a change-/configuration-management finding the auditor reads as
156
+ // ground truth (ISO 27001:2022 A.8.9 + SOC 2 CC8.1 turn on "the running
157
+ // system equals the approved definition"); under a regulated posture
158
+ // the safe default is to refuse boot rather than silently serve a
159
+ // schema no one approved. Operators who knowingly run with drift under
160
+ // a regulated posture opt back to the prior behaviour with an explicit
161
+ // onDrift: "ignore" (or "warn" to keep the signal without the throw).
142
162
  //
143
163
  // Returns a { tables: [...], drifted: boolean } report.
144
164
  function reconcile(database, schema, opts) {
@@ -165,6 +185,13 @@ function reconcileTable(database, table, opts) {
165
185
  throw new Error("schema entry '" + table.name + "' missing 'columns' object");
166
186
  }
167
187
  var driftMode = resolveDriftMode(opts);
188
+ // Identifier quoting follows the handle's dialect (double-quote on
189
+ // sqlite/postgres, backtick on mysql) so the reconciler's CREATE / ALTER
190
+ // / FK DDL is portable. Reserved-word column names stay safe by being
191
+ // quoted; the operator's verbatim TYPE strings are emitted unchanged in
192
+ // type position (after a quoted identifier), never in identifier position.
193
+ var dialect = _handleDialect(database);
194
+ function q(ident) { return safeSql.quoteIdentifier(ident, dialect, { allowReserved: true }); }
168
195
 
169
196
  var name = table.name;
170
197
  validateIdent(name, "table name");
@@ -172,7 +199,7 @@ function reconcileTable(database, table, opts) {
172
199
  var colDefs = [];
173
200
  for (var col in table.columns) {
174
201
  validateIdent(col, "column name");
175
- colDefs.push('"' + col + '" ' + table.columns[col]);
202
+ colDefs.push(q(col) + " " + table.columns[col]);
176
203
  }
177
204
  if (colDefs.length === 0) {
178
205
  throw new Error("schema entry '" + name + "' has no columns");
@@ -188,7 +215,7 @@ function reconcileTable(database, table, opts) {
188
215
  throw new Error("primaryKey '" + c + "' is not declared in columns of table '" + name + "'");
189
216
  }
190
217
  });
191
- colDefs.push("PRIMARY KEY (" + pkCols.map(function (c) { return '"' + c + '"'; }).join(", ") + ")");
218
+ colDefs.push("PRIMARY KEY (" + pkCols.map(function (c) { return q(c); }).join(", ") + ")");
192
219
  }
193
220
 
194
221
  // Structured FOREIGN KEY declarations. Each entry:
@@ -217,21 +244,31 @@ function reconcileTable(database, table, opts) {
217
244
  if (localCols.length !== refCols.length) {
218
245
  throw new Error("foreignKey on '" + name + "': local-column count must match referenced-column count");
219
246
  }
220
- var clause = "FOREIGN KEY (" + localCols.map(function (c) { return '"' + c + '"'; }).join(", ") + ")" +
221
- ' REFERENCES "' + refTable + '" (' + refCols.map(function (c) { return '"' + c + '"'; }).join(", ") + ")";
247
+ var clause = "FOREIGN KEY (" + localCols.map(function (c) { return q(c); }).join(", ") + ")" +
248
+ " REFERENCES " + q(refTable) + " (" + refCols.map(function (c) { return q(c); }).join(", ") + ")";
222
249
  if (fk.onDelete) clause += " ON DELETE " + _validateAction(fk.onDelete, "ON DELETE", name);
223
250
  if (fk.onUpdate) clause += " ON UPDATE " + _validateAction(fk.onUpdate, "ON UPDATE", name);
224
251
  colDefs.push(clause);
225
252
  }
226
253
  }
227
254
 
228
- runSql(database, 'CREATE TABLE IF NOT EXISTS "' + name + '" (' + colDefs.join(", ") + ")");
255
+ // Operator-schema reconcile: colDefs carries the operator's VERBATIM
256
+ // per-column DDL strings (e.g. "TEXT PRIMARY KEY", "INTEGER NOT NULL
257
+ // DEFAULT 0") plus composite FOREIGN KEY clauses with referential
258
+ // actions — a grammar b.sql.createTable's structured { name, type,
259
+ // notNull, references } column specs cannot faithfully reproduce
260
+ // (no table-level composite-FK or arbitrary-inline-constraint slot).
261
+ // Every identifier here is validated (validateIdent) + quoted by
262
+ // construction, so quote-by-construction safety is preserved.
263
+ // allow:hand-rolled-sql — operator verbatim column DDL + composite FK clauses outside b.sql.createTable's structured API
264
+ runSql(database, "CREATE TABLE IF NOT EXISTS " + q(name) + " (" + colDefs.join(", ") + ")");
229
265
 
230
266
  var existingCols = listColumns(database, name);
231
267
  for (var newCol in table.columns) {
232
268
  if (!existingCols.has(newCol)) {
233
269
  try {
234
- runSql(database, 'ALTER TABLE "' + name + '" ADD COLUMN "' + newCol + '" ' + table.columns[newCol]);
270
+ // allow:hand-rolled-sql operator verbatim ADD COLUMN DDL (validated + quoted identifier); type string is operator-controlled
271
+ runSql(database, "ALTER TABLE " + q(name) + " ADD COLUMN " + q(newCol) + " " + table.columns[newCol]);
235
272
  } catch (e) {
236
273
  throw new Error("failed to add column '" + newCol + "' to '" + name + "': " + e.message);
237
274
  }
@@ -244,9 +281,10 @@ function reconcileTable(database, table, opts) {
244
281
  }
245
282
  }
246
283
 
247
- // Schema-drift detection (opt-in; default "ignore" => no-op). Compares
248
- // the live table's columns against the declared model AFTER the additive
249
- // ADD COLUMN pass so the diff reflects what reconcile could not fix:
284
+ // Schema-drift detection. Default mode is posture-driven: "refuse"
285
+ // under a regulated pinned posture, "ignore" otherwise (resolveDriftMode).
286
+ // Compares the live table's columns against the declared model AFTER the
287
+ // additive ADD COLUMN pass so the diff reflects what reconcile could not fix:
250
288
  // - extra = live-but-undeclared (out-of-band ALTER / hand-edit);
251
289
  // - missing = declared-but-still-absent (ADD COLUMN could not apply).
252
290
  // Dropped columns are never acted on — reconcile stays non-destructive.
@@ -310,17 +348,51 @@ function _validateAction(action, label, tableName) {
310
348
  return up;
311
349
  }
312
350
 
313
- // onDrift reaction modes. "ignore" preserves pre-drift-detection
314
- // behavior byte-for-byte; "warn" emits an observability signal and
315
- // reports; "refuse" throws so a strict-schema posture refuses to boot
316
- // when the live DB has diverged from the declared model.
351
+ // onDrift reaction modes. "ignore" takes no action on detected drift;
352
+ // "warn" emits an observability signal and reports; "refuse" throws so a
353
+ // strict-schema posture refuses to boot when the live DB has diverged
354
+ // from the declared model.
317
355
  var DRIFT_MODES = ["ignore", "warn", "refuse"];
318
356
 
319
- // resolveDriftMode config-time enum validation. Undefined => "ignore"
320
- // (default; existing deployments see zero behavior change). A bad value
321
- // is an operator typo at config time THROW (entry-point tier).
357
+ // Compliance postures under which schema conformance is an audit-evidence
358
+ // floor (change-/configuration-management control families: ISO 27001:2022
359
+ // A.8.9, SOC 2 CC8.1). When one of these is the globally-pinned posture
360
+ // and the operator left onDrift unset, the default flips from "ignore" to
361
+ // "refuse" so an unapproved live schema fails boot rather than serving
362
+ // silently. Membership match is exact against compliance().current().
363
+ var REGULATED_DRIFT_REFUSE = Object.freeze({
364
+ "hipaa": true, "pci-dss": true, "gdpr": true, "soc2": true,
365
+ "iso-27001-2022": true, "dora": true, "fedramp-rev5-moderate": true,
366
+ "nist-800-53": true, "nist-800-53-r5-privacy": true, "dpdp": true,
367
+ "lgpd-br": true, "pipl-cn": true, "uk-gdpr": true,
368
+ });
369
+
370
+ // _pinnedRegulatedDrift — the posture-driven default when onDrift is unset.
371
+ // Returns "refuse" when a regulated posture is globally pinned, "ignore"
372
+ // otherwise. Drop-safe: any failure resolving the posture (compliance not
373
+ // loaded, no posture pinned) yields the back-compat "ignore" — the gate
374
+ // only tightens the default when a regulated posture is provably pinned,
375
+ // never the reverse.
376
+ function _pinnedRegulatedDrift() {
377
+ try {
378
+ var pinned = compliance().current();
379
+ if (typeof pinned === "string" && REGULATED_DRIFT_REFUSE[pinned] === true) {
380
+ return "refuse";
381
+ }
382
+ } catch (_e) { /* compliance unavailable — fall through to back-compat */ }
383
+ return "ignore";
384
+ }
385
+
386
+ // resolveDriftMode — config-time enum validation. Unset => the
387
+ // posture-driven default ("refuse" under a regulated pinned posture,
388
+ // "ignore" otherwise; see REGULATED_DRIFT_REFUSE). An explicit value
389
+ // always wins, including "ignore" to opt back out under a regulated
390
+ // posture. A bad value is an operator typo at config time => THROW
391
+ // (entry-point tier).
322
392
  function resolveDriftMode(opts) {
323
- if (!opts || opts.onDrift === undefined || opts.onDrift === null) return "ignore";
393
+ if (!opts || opts.onDrift === undefined || opts.onDrift === null) {
394
+ return _pinnedRegulatedDrift();
395
+ }
324
396
  var mode = opts.onDrift;
325
397
  if (typeof mode !== "string" || DRIFT_MODES.indexOf(mode) === -1) {
326
398
  throw new TypeError(
@@ -345,17 +417,99 @@ function reconcileIndex(database, tableName, idx) {
345
417
  }
346
418
  validateIdent(indexName, "index name");
347
419
  cols.forEach(function (c) { validateIdent(c, "indexed column"); });
348
- var quotedCols = cols.map(function (c) { return '"' + c + '"'; }).join(", ");
420
+ var dialect = _handleDialect(database);
421
+ function q(ident) { return safeSql.quoteIdentifier(ident, dialect, { allowReserved: true }); }
422
+ var quotedCols = cols.map(function (c) { return q(c); }).join(", ");
423
+ // MySQL has no CREATE INDEX IF NOT EXISTS; a re-run of a declared index
424
+ // would error "Duplicate key name". The reconciler is idempotent by
425
+ // contract, so on MySQL a duplicate-index error is swallowed (the index
426
+ // already exists, which is the desired end state). Postgres + SQLite use
427
+ // the IF NOT EXISTS form natively.
428
+ if (dialect === "mysql") {
429
+ try {
430
+ runSql(database,
431
+ "CREATE " + (unique ? "UNIQUE " : "") + "INDEX " + q(indexName) +
432
+ " ON " + q(tableName) + " (" + quotedCols + ")");
433
+ } catch (e) {
434
+ if (!/exist|duplicate/i.test((e && e.message) || "")) throw e;
435
+ }
436
+ return;
437
+ }
349
438
  runSql(database,
350
- "CREATE " + (unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS \"" + indexName + "\"" +
351
- ' ON "' + tableName + '" (' + quotedCols + ")"
439
+ "CREATE " + (unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS " + q(indexName) +
440
+ " ON " + q(tableName) + " (" + quotedCols + ")"
352
441
  );
353
442
  }
354
443
 
444
+ // The dialect of a data-layer handle. db.init / db.from drive the
445
+ // framework's local node:sqlite handle (the default). An operator who
446
+ // reconciles / migrates / seeds their OWN Postgres / MySQL handle declares
447
+ // the dialect on the handle via `handle.dialect` so the SQL matches the
448
+ // backend. Absent / unknown falls back to "sqlite" — every existing
449
+ // local-handle caller is byte-identical. Shared by db-schema's reconciler,
450
+ // migrations.js, and seeders.js (the three sync data-layer files that drive
451
+ // a handle directly), so the resolution lives in one place.
452
+ function handleDialect(database) {
453
+ var d = database && database.dialect;
454
+ if (d === "postgres" || d === "mysql" || d === "sqlite") return d;
455
+ return "sqlite";
456
+ }
457
+ // Back-compat internal alias used throughout this module.
458
+ var _handleDialect = handleDialect;
459
+
460
+ // b.sql opts for a statement run directly against `database` (db.prepare /
461
+ // runSqlOnHandle, never clusterStorage): the handle's dialect + quoteName so
462
+ // the resolved framework table name quotes by construction.
463
+ function sqlOpts(database) {
464
+ return { dialect: handleDialect(database), quoteName: true };
465
+ }
466
+
467
+ // A registry/lock PRIMARY-KEY (or composite-PK / indexed) TEXT column type.
468
+ // MySQL refuses an unbounded TEXT/BLOB in a key without a prefix length, so
469
+ // a key-participating text column is VARCHAR(191) there (utf8mb4
470
+ // index-safe); Postgres + SQLite index TEXT directly. The value is emitted
471
+ // verbatim by b.sql in type position (after a quoted identifier), never as
472
+ // an identifier.
473
+ function keyTextType(database) {
474
+ return handleDialect(database) === "mysql" ? "VARCHAR(191)" : "text";
475
+ }
476
+
477
+ // List the live column names of a table. SQLite reads `PRAGMA table_info`;
478
+ // Postgres + MySQL read information_schema.columns (PRAGMA is SQLite-only —
479
+ // it throws "syntax error at PRAGMA" on the others). The table name binds
480
+ // as a `?` parameter (never concatenated into the SQL text), so an operator
481
+ // table name with metacharacters can't break the introspection query. On
482
+ // Postgres the unqualified information_schema query matches the table in
483
+ // any schema on the search_path; an operator running the same table name in
484
+ // multiple schemas qualifies via the `schema.table` handle convention
485
+ // elsewhere — listColumns reconciles by bare name here, matching the
486
+ // reconciler's CREATE TABLE (which is also bare-named).
355
487
  function listColumns(database, tableName) {
356
- var rows = database.prepare('PRAGMA table_info("' + tableName + '")').all();
488
+ var dialect = _handleDialect(database);
357
489
  var set = new Set();
358
- for (var i = 0; i < rows.length; i++) set.add(rows[i].name);
490
+ if (dialect === "sqlite") {
491
+ var rows = database.prepare('PRAGMA table_info("' + tableName + '")').all();
492
+ for (var i = 0; i < rows.length; i++) set.add(rows[i].name);
493
+ return set;
494
+ }
495
+ // Postgres + MySQL: information_schema.columns is SQL-standard on both.
496
+ // The column-name column is `column_name` on both; the table name binds.
497
+ // A fixed catalog-introspection SELECT against the SQL-standard
498
+ // information_schema.columns view (a schema-qualified system table b.sql's
499
+ // verb builders don't model); the ONLY value (table name) binds as a `?`,
500
+ // every column/table reference is a static literal — no injection surface.
501
+ // allow:hand-rolled-sql — static information_schema introspection, single bound param
502
+ var infoSql = "SELECT column_name FROM information_schema.columns " +
503
+ "WHERE table_name = ?";
504
+ var stmt = database.prepare(infoSql);
505
+ var irows = stmt.all.apply(stmt, [tableName]);
506
+ for (var j = 0; j < irows.length; j++) {
507
+ // node-postgres folds unquoted output column names to lowercase, so the
508
+ // result key is `column_name` on every driver; read it directly.
509
+ var name = irows[j].column_name;
510
+ if (name === undefined) name = irows[j].COLUMN_NAME; // some MySQL drivers upper-case
511
+ if (name !== undefined && name !== null) set.add(name);
512
+ }
359
513
  return set;
360
514
  }
361
515
 
@@ -384,7 +538,9 @@ function runMigrations(database, migrationDir) {
384
538
  }).map(function (e) { return e.name; }).sort();
385
539
 
386
540
  var appliedSet = new Set();
387
- database.prepare("SELECT name FROM " + Q_MIGRATIONS_TABLE).all().forEach(function (r) {
541
+ var namesQ = sql.select(_migrationsTable(), _SQL_OPTS).columns(["name"]).toSql();
542
+ var namesStmt = database.prepare(namesQ.sql);
543
+ namesStmt.all.apply(namesStmt, namesQ.params).forEach(function (r) {
388
544
  appliedSet.add(r.name);
389
545
  });
390
546
 
@@ -413,9 +569,11 @@ function runMigrations(database, migrationDir) {
413
569
  try {
414
570
  runInTransaction(database, function () {
415
571
  mig.up(database);
416
- database.prepare(
417
- "INSERT INTO " + Q_MIGRATIONS_TABLE + " (name, description, appliedAt) VALUES (?, ?, ?)"
418
- ).run(file, mig.description || "", new Date().toISOString());
572
+ var insQ = sql.insert(_migrationsTable(), _SQL_OPTS)
573
+ .values({ name: file, description: mig.description || "",
574
+ appliedAt: new Date().toISOString() }).toSql();
575
+ var insStmt = database.prepare(insQ.sql);
576
+ insStmt.run.apply(insStmt, insQ.params);
419
577
  });
420
578
  } catch (e) {
421
579
  throw new Error("migration '" + file + "' failed: " + e.message);
@@ -434,6 +592,13 @@ module.exports = {
434
592
  runSql: runSql,
435
593
  runSqlOnHandle: runSqlOnHandle,
436
594
  runInTransaction: runInTransaction,
595
+ // Shared data-layer dialect resolution — composed by migrations.js +
596
+ // seeders.js so the handle-dialect / b.sql-opts / key-text-type logic
597
+ // lives in exactly one place.
598
+ handleDialect: handleDialect,
599
+ sqlOpts: sqlOpts,
600
+ keyTextType: keyTextType,
601
+ listColumns: listColumns,
437
602
  MIGRATIONS_TABLE: MIGRATIONS_TABLE,
438
603
  DRIFT_MODES: DRIFT_MODES,
439
604
  };