@blamejs/blamejs-shop 0.3.11 → 0.3.13

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 (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +157 -117
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/currency-rounding.js +2 -14
  5. package/lib/index.js +1 -0
  6. package/lib/text-guard.js +227 -0
  7. package/lib/vendor/MANIFEST.json +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/README.md +3 -2
  10. package/lib/vendor/blamejs/SECURITY.md +3 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +14 -2
  12. package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
  13. package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
  14. package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
  15. package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
  16. package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
  17. package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
  18. package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
  19. package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
  20. package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
  21. package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
  22. package/lib/vendor/blamejs/lib/app.js +2 -2
  23. package/lib/vendor/blamejs/lib/archive-read.js +1 -1
  24. package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
  25. package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
  26. package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
  27. package/lib/vendor/blamejs/lib/audit.js +2 -2
  28. package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
  29. package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
  30. package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
  31. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
  32. package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
  33. package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
  34. package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
  35. package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
  36. package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
  37. package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
  38. package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
  39. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
  40. package/lib/vendor/blamejs/lib/backup/index.js +18 -18
  41. package/lib/vendor/blamejs/lib/cache.js +4 -4
  42. package/lib/vendor/blamejs/lib/calendar.js +5 -5
  43. package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
  44. package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
  45. package/lib/vendor/blamejs/lib/compliance.js +14 -14
  46. package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
  47. package/lib/vendor/blamejs/lib/crypto.js +5 -6
  48. package/lib/vendor/blamejs/lib/db-query.js +131 -9
  49. package/lib/vendor/blamejs/lib/db.js +106 -22
  50. package/lib/vendor/blamejs/lib/external-db.js +64 -16
  51. package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
  52. package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
  53. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
  54. package/lib/vendor/blamejs/lib/incident-report.js +150 -0
  55. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
  56. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
  57. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
  58. package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
  59. package/lib/vendor/blamejs/lib/mail-store.js +1 -1
  60. package/lib/vendor/blamejs/lib/metrics.js +8 -8
  61. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
  62. package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
  63. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
  64. package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
  65. package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
  66. package/lib/vendor/blamejs/lib/network-dns.js +1 -2
  67. package/lib/vendor/blamejs/lib/network-tls.js +0 -1
  68. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  69. package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
  70. package/lib/vendor/blamejs/lib/retention.js +1 -1
  71. package/lib/vendor/blamejs/lib/retry.js +1 -1
  72. package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
  73. package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
  74. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  75. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
  76. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  77. package/lib/vendor/blamejs/lib/static.js +1 -1
  78. package/lib/vendor/blamejs/lib/subject.js +2 -2
  79. package/lib/vendor/blamejs/lib/vault/index.js +64 -1
  80. package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
  81. package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
  82. package/lib/vendor/blamejs/package.json +1 -1
  83. package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
  84. package/lib/vendor/blamejs/scripts/release.js +28 -3
  85. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
  87. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
  92. package/lib/webhook-subscriptions.js +11 -24
  93. package/lib/webhooks.js +12 -33
  94. package/package.json +1 -1
@@ -52,7 +52,7 @@ var compliance = lazyRequire(function () { return require("./compliance"); })
52
52
  var db = lazyRequire(function () { return require("./db"); });
53
53
  var audit = lazyRequire(function () { return require("./audit"); });
54
54
 
55
- // F-POSTURE-1 cascade hook + F-RTBF-2 integration. Recording the
55
+ // Posture cascade hook + erase-vacuum integration. Recording the
56
56
  // posture lets eraseRow call b.db.vacuumAfterErase({ mode: "full" })
57
57
  // automatically under postures whose POSTURE_DEFAULTS sets
58
58
  // requireVacuumAfterErase: true (gdpr / dpdp / pipl-cn / lgpd-br /
@@ -113,7 +113,7 @@ function getActivePosture() { return _activePosture; }
113
113
  // Per-table registry, populated by db.init()
114
114
  var schemas = Object.create(null);
115
115
 
116
- // F-CBT-1 — per-COLUMN data residency registry. Real GDPR / DPDP
116
+ // Per-COLUMN data residency registry. Real GDPR / DPDP
117
117
  // deployments have row-level mixed residency: a `users.name` column
118
118
  // may be global, but `users.addressLine1` must stay in EU storage.
119
119
  // db.init({ schema }) carries the operator's residency declaration
@@ -123,7 +123,7 @@ var schemas = Object.create(null);
123
123
  // { tableName: { columnName: "eu" | "us" | "global" | <tag> } }
124
124
  var columnResidency = Object.create(null);
125
125
 
126
- // F-RTBF-3 — per-row key declaration registry. For tables that opt
126
+ // Per-row key declaration registry. For tables that opt
127
127
  // into per-row keying, b.subject.eraseHard deletes the wrapped K_row
128
128
  // from _blamejs_per_row_keys, leaving WAL/replica residual ciphertext
129
129
  // undecryptable.
@@ -152,7 +152,7 @@ var perRowKeyTables = Object.create(null);
152
152
  * // b.vault.aad — AEAD-binds the ciphertext
153
153
  * // to (table, rowIdField=primary key, column)
154
154
  * // so a DB-write attacker can't copy a
155
- * // sealed value between rows. CRYPTO-1.
155
+ * // sealed value between rows.
156
156
  * rowIdField: string, // when aad=true, the column name carrying
157
157
  * // the row identity. Default "id". The row
158
158
  * // passed to sealRow MUST already have this
@@ -173,7 +173,7 @@ var perRowKeyTables = Object.create(null);
173
173
  * });
174
174
  * b.cryptoField.getSealedFields("patients"); // → ["ssn", "diagnosis"]
175
175
  *
176
- * // AAD-bound table (recommended for new schemas — CRYPTO-1).
176
+ * // AAD-bound table (recommended for new schemas).
177
177
  * b.cryptoField.registerTable("idempotency_keys", {
178
178
  * sealedFields: ["headers", "body"],
179
179
  * aad: true,
@@ -185,16 +185,56 @@ function registerTable(name, opts) {
185
185
  var rowIdField = typeof opts.rowIdField === "string" && opts.rowIdField.length > 0
186
186
  ? opts.rowIdField : "id";
187
187
  var schemaVersion = opts.schemaVersion != null ? String(opts.schemaVersion) : "1";
188
+ var derivedHashMode = opts.derivedHashMode || "salted-sha3";
189
+ if (derivedHashMode !== "salted-sha3" && derivedHashMode !== "hmac-shake256") {
190
+ throw new Error("registerTable: derivedHashMode must be 'salted-sha3' (default) or " +
191
+ "'hmac-shake256', got " + JSON.stringify(derivedHashMode));
192
+ }
193
+ var derivedHashes = Object.assign({}, opts.derivedHashes || {});
194
+ for (var col in derivedHashes) {
195
+ if (!Object.prototype.hasOwnProperty.call(derivedHashes, col)) continue;
196
+ var colMode = derivedHashes[col] && derivedHashes[col].mode;
197
+ if (colMode !== undefined && colMode !== "salted-sha3" && colMode !== "hmac-shake256") {
198
+ throw new Error("registerTable: derivedHashes." + col + ".mode must be " +
199
+ "'salted-sha3' or 'hmac-shake256', got " + JSON.stringify(colMode));
200
+ }
201
+ }
188
202
  schemas[name] = {
189
- sealedFields: Array.isArray(opts.sealedFields) ? opts.sealedFields.slice() : [],
190
- derivedHashes: Object.assign({}, opts.derivedHashes || {}),
191
- hashNamespaces: Object.assign({}, opts.hashNamespaces || {}),
192
- aad: aadOn,
193
- rowIdField: rowIdField,
194
- schemaVersion: schemaVersion,
203
+ sealedFields: Array.isArray(opts.sealedFields) ? opts.sealedFields.slice() : [],
204
+ derivedHashes: derivedHashes,
205
+ hashNamespaces: Object.assign({}, opts.hashNamespaces || {}),
206
+ aad: aadOn,
207
+ rowIdField: rowIdField,
208
+ schemaVersion: schemaVersion,
209
+ derivedHashMode: derivedHashMode,
195
210
  };
196
211
  }
197
212
 
213
+ // Derived-hash digest width for the keyed (hmac-shake256) mode: 32
214
+ // bytes -> 64 hex chars.
215
+ var DERIVED_HASH_BYTES = 32;
216
+
217
+ // Compute the indexed-lookup digest for a derived-hash column.
218
+ // - "salted-sha3" (default): SHA3-512 over <per-deployment salt> + ns
219
+ // + value (128 hex). Deterministic per deployment.
220
+ // - "hmac-shake256": SHAKE256(<vault-sealed MAC key> || ns + value)
221
+ // truncated to 32 bytes (64 hex). The key is a vault-derived secret,
222
+ // NOT a static salt, so an attacker who recovers the salt alone
223
+ // can't correlate two low-entropy plaintexts; the sponge has no
224
+ // length-extension weakness. (b.crypto.hmacSha3 (HMAC-SHA3-512) was
225
+ // considered; SHAKE256(key||msg) is chosen for the fixed-width keyed
226
+ // digest with the same MAC-grade guarantee.) FIPS 202; NIST SP
227
+ // 800-185; GDPR Art. 4(5) pseudonymisation; HIPAA 45 CFR 164.514(b).
228
+ function _computeDerivedHash(spec, tableMode, ns, normalized) {
229
+ var mode = (spec && spec.mode) || tableMode || "salted-sha3";
230
+ if (mode === "hmac-shake256") {
231
+ var macKey = vault.getDerivedHashMacKey();
232
+ return kdf(Buffer.concat([macKey, Buffer.from(ns + normalized, "utf8")]),
233
+ DERIVED_HASH_BYTES).toString("hex");
234
+ }
235
+ return sha3Hash(vault.getDerivedHashSalt().toString("hex") + ns + normalized);
236
+ }
237
+
198
238
  /**
199
239
  * @primitive b.cryptoField.getSchema
200
240
  * @signature b.cryptoField.getSchema(table)
@@ -308,8 +348,7 @@ function computeDerived(table, sourceField, sourceValue) {
308
348
  if (spec.from === sourceField) {
309
349
  var ns = namespaceFor(table, sourceField, s.hashNamespaces);
310
350
  var normalized = spec.normalize ? spec.normalize(sourceValue) : String(sourceValue);
311
- var saltHex = vault.getDerivedHashSalt().toString("hex");
312
- return { field: derivedField, value: sha3Hash(saltHex + ns + normalized) };
351
+ return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
313
352
  }
314
353
  }
315
354
  return null;
@@ -367,12 +406,11 @@ function sealRow(table, row) {
367
406
  }
368
407
  var ns = namespaceFor(table, spec.from, s.hashNamespaces);
369
408
  var normalized = spec.normalize ? spec.normalize(plain) : String(plain);
370
- var saltHex2 = vault.getDerivedHashSalt().toString("hex");
371
- out[derivedField] = sha3Hash(saltHex2 + ns + normalized);
409
+ out[derivedField] = _computeDerivedHash(spec, s.derivedHashMode, ns, normalized);
372
410
  }
373
411
  }
374
412
 
375
- // CRYPTO-1 — AAD-bound table requires the row's identity column to
413
+ // AAD-bound table requires the row's identity column to
376
414
  // be populated BEFORE sealRow runs. Sealing under a placeholder /
377
415
  // missing rowId produces ciphertext that no later unseal can open
378
416
  // because the AAD on read is computed against the row's actual id.
@@ -390,7 +428,7 @@ function sealRow(table, row) {
390
428
  // Seal fields. Plain mode: vault.seal (idempotent — already-sealed
391
429
  // values pass through). AAD mode: vault.aad.seal binds the AEAD tag
392
430
  // to (table, rowId, column, schemaVersion) — cross-row copy of a
393
- // ciphertext fails Poly1305 on read. CRYPTO-1.
431
+ // ciphertext fails Poly1305 on read.
394
432
  for (var i = 0; i < s.sealedFields.length; i++) {
395
433
  var field = s.sealedFields[i];
396
434
  if (out[field] !== undefined && out[field] !== null) {
@@ -573,7 +611,7 @@ function eraseRow(table, row) {
573
611
  out[derivedField] = null;
574
612
  }
575
613
  }
576
- // F-RTBF-4 — `__erasedAt` was previously a plaintext UTC ms integer.
614
+ // `__erasedAt` was previously a plaintext UTC ms integer.
577
615
  // That value alone fingerprints the erasure event (audit-log
578
616
  // exfiltration + cross-tenant correlation: "this row was erased
579
617
  // 2.3s before that one"). Bucket the timestamp to a 1-day floor so
@@ -584,7 +622,7 @@ function eraseRow(table, row) {
584
622
  var dayMs = TIME.days(1);
585
623
  out.__erasedAt = Math.floor(Date.now() / dayMs) * dayMs;
586
624
 
587
- // F-RTBF-2 — under regulatory postures whose POSTURE_DEFAULTS sets
625
+ // Under regulatory postures whose POSTURE_DEFAULTS sets
588
626
  // requireVacuumAfterErase: true (gdpr / dpdp / pipl-cn / lgpd-br /
589
627
  // hipaa), the B-tree index pages freed by the upcoming UPDATE/DELETE
590
628
  // would otherwise linger with sealed-column ciphertext readable
@@ -662,8 +700,7 @@ function lookupHash(table, field, value) {
662
700
  if (spec.from === field) {
663
701
  var ns = namespaceFor(table, field, s.hashNamespaces);
664
702
  var normalized = spec.normalize ? spec.normalize(value) : String(value);
665
- var saltHex = vault.getDerivedHashSalt().toString("hex");
666
- return { field: derivedField, value: sha3Hash(saltHex + ns + normalized) };
703
+ return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
667
704
  }
668
705
  }
669
706
  return null;
@@ -168,7 +168,7 @@ function hashFile(filePath, algorithm) {
168
168
  // `algorithms` entry. Used by hashFilesParallel below; not exported
169
169
  // directly because the common case is the parallel-many shape.
170
170
  //
171
- // CRYPTO-5 hardening (v0.9.58):
171
+ // Hardening (v0.9.58):
172
172
  // - lstat-then-stat so symlinks are detected before open; refused
173
173
  // unless opts.followSymlinks === true (default false — a symlink-
174
174
  // in-input-list attack lets a write-restricted caller hash files
@@ -269,9 +269,8 @@ function _hashFileMulti(filePath, algorithms, opts) {
269
269
  * SBOM regeneration / vendor-data integrity sweeps / release-asset
270
270
  * bundling — situations where N files each need both SHA-256 (legacy
271
271
  * compat) and SHA-3-512 (PQC-first) digests and rolling a worker
272
- * pool by hand has cost a downstream consumer (`hermitstash-sync`
273
- * 2026-05-13) the same two-loop, capture-N-promises, settle-Q boilerplate
274
- * every release.
272
+ * pool by hand means the same two-loop, capture-N-promises, settle-Q
273
+ * boilerplate every release.
275
274
  *
276
275
  * @opts
277
276
  * algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
@@ -333,7 +332,7 @@ function hashFilesParallel(filePaths, opts) {
333
332
  "crypto.hashFilesParallel: opts.onProgress must be a function when supplied"
334
333
  ));
335
334
  }
336
- // CRYPTO-5 — DoS cap. Default 1 GiB per file; operators with larger
335
+ // DoS cap. Default 1 GiB per file; operators with larger
337
336
  // legitimate hashing workloads (firmware images, vendor packs)
338
337
  // override per-call.
339
338
  var maxBytesPerFile = opts.maxBytesPerFile !== undefined
@@ -1202,7 +1201,7 @@ function decrypt(ciphertext, privateKeys, opts) {
1202
1201
  }
1203
1202
  // Audit-emit every legacy decrypt so the migration window is
1204
1203
  // visible. Emit success ONLY on actual decrypt success; emit
1205
- // failure on throw. Codex P2 PR #74 — pre-fix the audit fired
1204
+ // failure on throw. Before this fix, the audit fired
1206
1205
  // before decryptEnvelope() ran, so corrupted 0xE1 blobs / wrong
1207
1206
  // private keys / unsupported KEMs got logged as successful legacy
1208
1207
  // decrypts when the call actually threw, inflating real success
@@ -33,6 +33,7 @@ var { generateToken } = require("./crypto");
33
33
  var safeJson = require("./safe-json");
34
34
  var safeJsonPath = require("./safe-jsonpath");
35
35
  var safeSql = require("./safe-sql");
36
+ var audit = require("./audit");
36
37
 
37
38
  // "@>" / "?" / "?|" / "?&" are JSONB containment + key-existence
38
39
  // operators. Routed through safeJsonPath validation before binding so
@@ -46,7 +47,7 @@ var JSONB_CONTAINMENT_OPS = new Set(["@>"]);
46
47
  var JSONB_KEY_OPS = new Set(["?", "?|", "?&"]);
47
48
 
48
49
  class Query {
49
- constructor(database, tableName) {
50
+ constructor(database, tableName, opts) {
50
51
  // Identifier safety: tableName flows into SQL via interpolation
51
52
  // (parameter placeholders only bind values, not names). Validate at
52
53
  // construction so an attacker-controlled name with embedded `"` or
@@ -84,6 +85,62 @@ class Query {
84
85
  this._orderBy = null;
85
86
  this._limit = null;
86
87
  this._offset = null;
88
+
89
+ // Column-membership gate. `db.from()` passes the table's
90
+ // declared columns + the configured gate mode so an operator-
91
+ // supplied column name that isn't a real column of the table is
92
+ // refused before it interpolates into SQL as an identifier
93
+ // (ORDER-BY / sealed-column-disclosure injection — CWE-89 /
94
+ // CWE-1336). A bare `new Query(db, name)` with no opts leaves the
95
+ // gate disabled (declaredColumns null), so direct/internal
96
+ // construction is unaffected.
97
+ opts = opts || {};
98
+ this._declaredColumns = (opts.declaredColumns instanceof Set) ? opts.declaredColumns
99
+ : (Array.isArray(opts.declaredColumns) ? new Set(opts.declaredColumns) : null);
100
+ this._columnGateMode = opts.columnGateMode || "reject";
101
+ this._allowedColumns = null;
102
+ }
103
+
104
+ // Restrict the operator-allowable columns to an explicit subset
105
+ // (tighter than the schema-declared set). Use when a query is built
106
+ // from request input and must only ever touch a known-safe list.
107
+ // Throws on a non-array or an invalid identifier.
108
+ allowedColumns(cols) {
109
+ if (!Array.isArray(cols) || cols.length === 0) {
110
+ throw new TypeError("allowedColumns(cols): expected a non-empty array of column names");
111
+ }
112
+ cols.forEach(_validateField);
113
+ this._allowedColumns = new Set(cols);
114
+ return this;
115
+ }
116
+
117
+ // Assert `field` is a member of the allowed/declared column set
118
+ // before it is interpolated into SQL as an identifier. The operator
119
+ // `allowedColumns()` set (when present) is ALWAYS enforced; the
120
+ // schema gate respects the configured mode ("reject" default
121
+ // throws | "warn" drop-silent audits + allows | "off" / no declared
122
+ // set skips).
123
+ _assertColumnMember(field, where) {
124
+ if (this._allowedColumns && !this._allowedColumns.has(field)) {
125
+ throw new Error("column '" + field + "' is not in the allowedColumns() set" +
126
+ (where ? " (" + where + ")" : ""));
127
+ }
128
+ if (this._declaredColumns === null || this._columnGateMode === "off") return;
129
+ if (this._declaredColumns.has(field)) return;
130
+ if (this._columnGateMode === "warn") {
131
+ try {
132
+ audit.safeEmit({
133
+ action: "db.query.unknown_column",
134
+ outcome: "failure",
135
+ metadata: { table: this._qualifiedKey, column: field, where: where || null },
136
+ });
137
+ } catch (_e) { /* drop-silent — observability sink, by design */ }
138
+ return;
139
+ }
140
+ throw new Error("column '" + field + "' is not a declared column of '" +
141
+ this._qualifiedKey + "'" + (where ? " (" + where + ")" : "") +
142
+ ". Declared columns: " + Array.from(this._declaredColumns).join(", ") +
143
+ ". Use .allowedColumns([...]) or db.init({ columnGate: 'off' }) to bypass.");
87
144
  }
88
145
 
89
146
  // Quoted SQL form: `"schema"."table"` if schema-qualified, else `"table"`.
@@ -114,7 +171,7 @@ class Query {
114
171
  if (!ALLOWED_OPS.has(op)) {
115
172
  throw new Error("invalid where operator: " + op);
116
173
  }
117
- // D-M4 — JSONB / JSON-path injection guard. Routes operator-
174
+ // JSONB / JSON-path injection guard. Routes operator-
118
175
  // supplied JSONB containment + key-existence values through
119
176
  // safe-jsonpath before they reach the engine. Bound via `?`
120
177
  // placeholder so the value still doesn't interpolate; this is
@@ -171,6 +228,10 @@ class Query {
171
228
  value = lookup.value;
172
229
  }
173
230
  cryptoField && _validateField(field);
231
+ // Gate the post-sealed-rewrite physical column (derived-hash
232
+ // columns are declared physical columns, so the rewrite target
233
+ // passes membership).
234
+ this._assertColumnMember(field, "where");
174
235
  if (op === "IN") {
175
236
  // node:sqlite ? does not support array-binding. Pre-v0.8.18
176
237
  // `where(field, "IN", [1,2,3])` silently bound the entire
@@ -228,10 +289,11 @@ class Query {
228
289
  // text used to build expressions the chainable .where() can't express
229
290
  // (compound OR, row-value comparison for cursor pagination, etc.).
230
291
  // Placeholder count must match params.length.
231
- whereRaw(sql, params) {
292
+ whereRaw(sql, params, opts) {
232
293
  if (typeof sql !== "string" || sql.length === 0) {
233
294
  throw new Error("whereRaw: sql must be a non-empty string");
234
295
  }
296
+ if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "whereRaw");
235
297
  var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
236
298
  // Count `?` placeholders, but skip occurrences inside string
237
299
  // literals ('...' or "..."), line comments (-- to EOL), and
@@ -255,12 +317,15 @@ class Query {
255
317
  throw new Error("select() expects an array of column names");
256
318
  }
257
319
  columns.forEach(_validateField);
320
+ var self = this;
321
+ columns.forEach(function (c) { self._assertColumnMember(c, "select"); });
258
322
  this._select = columns.slice();
259
323
  return this;
260
324
  }
261
325
 
262
326
  orderBy(field, direction) {
263
327
  _validateField(field);
328
+ this._assertColumnMember(field, "orderBy");
264
329
  direction = (direction || "asc").toLowerCase();
265
330
  if (direction !== "asc" && direction !== "desc") {
266
331
  throw new Error("orderBy direction must be 'asc' or 'desc'");
@@ -348,7 +413,7 @@ class Query {
348
413
  // the bound table's sealedFields registration before it lands in the
349
414
  // operator's pipeline. For large result sets (audit exports, backup
350
415
  // table dumps) this avoids materializing the full rowset in memory.
351
- // D-M5 — streamLimit ceiling enforced from the module-level db
416
+ // StreamLimit ceiling enforced from the module-level db
352
417
  // config; per-call opts.streamLimit overrides for one-off bumps.
353
418
  stream(opts) {
354
419
  var sql = "SELECT " + this._projection() + " FROM " + this._quotedTable() +
@@ -455,6 +520,8 @@ class Query {
455
520
  throw new Error("update changes object is empty");
456
521
  }
457
522
  setKeys.forEach(_validateField);
523
+ var selfUpd = this;
524
+ setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
458
525
  var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
459
526
  var setValues = setKeys.map(function (k) { return sealed[k]; });
460
527
 
@@ -498,6 +565,7 @@ class Query {
498
565
  throw new Error("increment(column, delta): column must be a non-empty string");
499
566
  }
500
567
  _validateField(column);
568
+ this._assertColumnMember(column, "increment");
501
569
  if (delta === undefined) delta = 1;
502
570
  if (typeof delta !== "number" || !Number.isFinite(delta) || !Number.isInteger(delta)) {
503
571
  throw new Error("increment(column, delta): delta must be a finite integer (default 1)");
@@ -532,7 +600,7 @@ class Query {
532
600
  if (typeof closure !== "function") {
533
601
  throw new Error("whereGroup(closure): expected function (qb) => ...");
534
602
  }
535
- var sub = new WhereBuilder();
603
+ var sub = new WhereBuilder(this);
536
604
  closure(sub);
537
605
  var built = sub.build();
538
606
  if (!built.sql) return this;
@@ -551,7 +619,7 @@ class Query {
551
619
  throw new Error("orWhere(...): no prior where(...) — start the chain with where(...)");
552
620
  }
553
621
  if (typeof fieldOrObjOrFn === "function") {
554
- var sub = new WhereBuilder();
622
+ var sub = new WhereBuilder(this);
555
623
  fieldOrObjOrFn(sub);
556
624
  var built = sub.build();
557
625
  if (!built.sql) return this;
@@ -562,7 +630,7 @@ class Query {
562
630
  }
563
631
  // For non-closure shapes, build a transient single-leaf Query and
564
632
  // splice it. We compile to a `WhereBuilder` for symmetry.
565
- var sub2 = new WhereBuilder();
633
+ var sub2 = new WhereBuilder(this);
566
634
  if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" && !Array.isArray(fieldOrObjOrFn)) {
567
635
  Object.keys(fieldOrObjOrFn).forEach(function (k) { sub2.eq(k, fieldOrObjOrFn[k]); });
568
636
  } else if (op === undefined) {
@@ -592,6 +660,8 @@ class Query {
592
660
  throw new Error("search(fields, term): fields must be a non-empty array of column names");
593
661
  }
594
662
  fields.forEach(_validateField);
663
+ var selfS = this;
664
+ fields.forEach(function (f) { selfS._assertColumnMember(f, "search"); });
595
665
  if (term === undefined || term === null) return this;
596
666
  if (typeof term !== "string") {
597
667
  throw new Error("search(fields, term): term must be a string");
@@ -684,14 +754,18 @@ class Query {
684
754
  // `.build()` returns `{ sql, params }`. Empty builder → `{ sql: "",
685
755
  // params: [] }`.
686
756
  class WhereBuilder {
687
- constructor() {
757
+ constructor(gate) {
688
758
  this._parts = []; // [{ joiner: "AND"|"OR", sql: "...", params: [...] }]
759
+ // The owning Query, so grouped/OR sub-expressions enforce the
760
+ // same column-membership gate as the top-level chain.
761
+ this._gate = gate || null;
689
762
  }
690
763
  _push(joiner, field, op, value) {
691
764
  if (typeof field !== "string" || field.length === 0) {
692
765
  throw new Error("WhereBuilder: field must be a non-empty string");
693
766
  }
694
767
  _validateField(field);
768
+ if (this._gate) this._gate._assertColumnMember(field, "whereGroup");
695
769
  var qf = '"' + field + '"';
696
770
  if (op === "IN" || op === "NOT IN") {
697
771
  if (!Array.isArray(value) || value.length === 0) {
@@ -723,10 +797,11 @@ class WhereBuilder {
723
797
  orLte(f, v) { return this._push("OR", f, "<=", v); }
724
798
  orIn(f, vs) { return this._push("OR", f, "IN", vs); }
725
799
  orLike(f, v) { return this._push("OR", f, "LIKE", v); }
726
- raw(sql, params) {
800
+ raw(sql, params, opts) {
727
801
  if (typeof sql !== "string" || sql.length === 0) {
728
802
  throw new Error("WhereBuilder.raw: sql must be a non-empty string");
729
803
  }
804
+ if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql, "WhereBuilder.raw");
730
805
  var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
731
806
  if (_countPlaceholders(sql) !== p.length) {
732
807
  throw new Error("WhereBuilder.raw: placeholder count mismatch");
@@ -752,6 +827,53 @@ class WhereBuilder {
752
827
  // Tracks SQL single-quoted, double-quoted, line-comment, and block-
753
828
  // comment state to avoid counting `?` characters that are part of
754
829
  // literal text the SQL engine never interprets as a binding marker.
830
+ // Refuse raw SQL fragments that embed a single-quoted string
831
+ // literal. A whereRaw / WhereBuilder.raw fragment is meant to be a
832
+ // STATIC template whose every value is bound through a `?` placeholder;
833
+ // an embedded `'...'` literal is the signature of operator input
834
+ // concatenated into the query (CWE-89 / CWE-564 — concat into a
835
+ // query builder). Double-quoted identifiers (`"col"`), line comments,
836
+ // and block comments are skipped. Operators with a deliberate static
837
+ // literal pass `{ allowLiterals: true }`. Shares the quote/comment
838
+ // scanning shape with _countPlaceholders.
839
+ function _assertRawNoStringLiteral(sql, where) {
840
+ var i = 0;
841
+ var len = sql.length;
842
+ while (i < len) {
843
+ var ch = sql.charAt(i);
844
+ var next = i + 1 < len ? sql.charAt(i + 1) : "";
845
+ if (ch === '"') {
846
+ i += 1;
847
+ while (i < len) {
848
+ if (sql.charAt(i) === '"') {
849
+ if (sql.charAt(i + 1) === '"') { i += 2; continue; }
850
+ i += 1; break;
851
+ }
852
+ i += 1;
853
+ }
854
+ continue;
855
+ }
856
+ if (ch === "-" && next === "-") {
857
+ while (i < len && sql.charAt(i) !== "\n") i += 1;
858
+ continue;
859
+ }
860
+ if (ch === "/" && next === "*") {
861
+ i += 2;
862
+ while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
863
+ i += 2;
864
+ continue;
865
+ }
866
+ if (ch === "'") {
867
+ throw new safeSql.SafeSqlError(
868
+ where + ": raw SQL must not contain a string literal ('...') — bind every " +
869
+ "value with a ? placeholder, or pass { allowLiterals: true } when the literal " +
870
+ "is static and operator-controlled.",
871
+ "sql/raw-literal");
872
+ }
873
+ i += 1;
874
+ }
875
+ }
876
+
755
877
  function _countPlaceholders(sql) {
756
878
  var count = 0;
757
879
  var i = 0;