@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
@@ -49,6 +49,7 @@ var requestHelpers = require("./request-helpers");
49
49
  var safeAsync = require("./safe-async");
50
50
  var safeJson = require("./safe-json");
51
51
  var safeSql = require("./safe-sql");
52
+ var sql = require("./sql");
52
53
  var totp = require("./totp");
53
54
  var validateOpts = require("./validate-opts");
54
55
  var { defineClass } = require("./framework-error");
@@ -86,6 +87,34 @@ var DEFAULT_AUDIT_REASON = "cleartext";
86
87
  var ALLOWED_FACTORS = ["totp", "passkey"];
87
88
  var ALLOWED_REASON_STORAGE = ["cleartext", "hmac", "both"];
88
89
 
90
+ // cryptoField REGISTRY KEYS for the two break-glass framework tables. These
91
+ // are the names db.js's FRAMEWORK_SCHEMA registered the tables under, so
92
+ // seal / unseal / computeDerived must key off the byte-identical literal —
93
+ // resolving them through frameworkSchema.tableName would diverge the seal-side
94
+ // key from the registration under a custom prefix and break decryption. (SQL
95
+ // composed via b.sql passes the SAME bare logical name so clusterStorage can
96
+ // rewrite the table reference; these constants cover only the cryptoField
97
+ // keying.) allow:hand-rolled-sql — cryptoField registry keys, not SQL text.
98
+ var POLICIES_TABLE = "_blamejs_break_glass_policies"; // allow:hand-rolled-sql
99
+ var GRANTS_TABLE = "_blamejs_break_glass_grants"; // allow:hand-rolled-sql
100
+
101
+ // b.sql opts for every statement break-glass dispatches through
102
+ // clusterStorage. Thread the ACTIVE backend dialect (clusterStorage.dialect()
103
+ // — "sqlite" single-node, "postgres" | "mysql" in cluster mode) so the
104
+ // emitted identifier quoting + dialect idioms (ON CONFLICT vs ON DUPLICATE
105
+ // KEY) match the backend the SQL dispatches to. Defaulting to "sqlite" works
106
+ // on Postgres only by accident (both double-quote identifiers) and emits the
107
+ // wrong quoting on MySQL. clusterStorage.execute still rewrites framework
108
+ // table names + translates `?` placeholders at dispatch; this controls only
109
+ // the builder-side quoting + idiom selection.
110
+ // _sqlOpts() — framework tables (policies / grants); name resolved bare,
111
+ // clusterStorage rewrites the prefix.
112
+ // _appSqlOpts() — the operator's glass-locked app table; quoteName so b.sql
113
+ // quotes the (validated) identifier, and it is NOT
114
+ // framework-rewritten.
115
+ function _sqlOpts() { return { dialect: clusterStorage.dialect() }; }
116
+ function _appSqlOpts() { return { dialect: clusterStorage.dialect(), quoteName: true }; }
117
+
89
118
  // In-memory policy cache. Cluster-shared via the policies table; the
90
119
  // cache short-circuits the DB roundtrip on the unsealRow hot path.
91
120
  // Populated on first access per-table; invalidated on policy.set/delete.
@@ -157,10 +186,14 @@ async function _ensureDek(table) {
157
186
  // DEK is vault-sealed and stored in the policy row's `dekSealed`
158
187
  // column. Generated lazily on first use of cryptographic-mode for
159
188
  // the table. Cached in-memory after first read.
160
- var rows = await clusterStorage.executeAll(
161
- "SELECT dekSealed FROM _blamejs_break_glass_policies WHERE tableName = ?",
162
- [table]
163
- );
189
+ // The policy table is external-only; its LOGICAL name IS the
190
+ // `_blamejs_`-prefixed name (self-mapped in LOCAL_TO_EXTERNAL), passed
191
+ // bare to b.sql so clusterStorage rewrites + placeholderizes.
192
+ var dekReadBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
193
+ .columns(["dekSealed"])
194
+ .where("tableName", table)
195
+ .toSql();
196
+ var rows = await clusterStorage.executeAll(dekReadBuilt.sql, dekReadBuilt.params);
164
197
  if (!rows || rows.length === 0) {
165
198
  throw new BreakGlassError("breakglass/policy-not-set",
166
199
  "_ensureDek: no policy for table '" + table + "'", true);
@@ -172,10 +205,11 @@ async function _ensureDek(table) {
172
205
  } else {
173
206
  dek = generateBytes(DEK_BYTES);
174
207
  var sealedDek = vault().seal(dek.toString("base64"));
175
- await clusterStorage.execute(
176
- "UPDATE _blamejs_break_glass_policies SET dekSealed = ? WHERE tableName = ?",
177
- [sealedDek, table]
178
- );
208
+ var dekUpdBuilt = sql.update("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
209
+ .set({ dekSealed: sealedDek })
210
+ .where("tableName", table)
211
+ .toSql();
212
+ await clusterStorage.execute(dekUpdBuilt.sql, dekUpdBuilt.params);
179
213
  }
180
214
  dekCache.set(table, dek);
181
215
  return dek;
@@ -347,14 +381,16 @@ async function migrate(table, opts) {
347
381
  var lastId = "";
348
382
  // Iterate via _id-keyset paging so we don't load the whole table into memory.
349
383
  while (true) {
350
- // table is already validated as a safe identifier shape via
351
- // _validatePolicySet wrap in "..." per the framework's
352
- // identifier-quoting convention.
353
- var qTable = '"' + table + '"';
354
- var rows = await clusterStorage.executeAll(
355
- "SELECT * FROM " + qTable + " WHERE _id > ? ORDER BY _id ASC LIMIT ?",
356
- [lastId, batchSize]
357
- );
384
+ // `table` is an operator app table (already validated as a safe
385
+ // identifier via _validatePolicySet). quoteName:true makes b.sql quote
386
+ // the name (reserved-word / case-sensitive safe); it is NOT a framework
387
+ // table, so clusterStorage's resolveTables leaves it untouched.
388
+ var pageBuilt = sql.select(table, _appSqlOpts())
389
+ .whereOp("_id", ">", lastId)
390
+ .orderBy("_id", "asc")
391
+ .limit(batchSize)
392
+ .toSql();
393
+ var rows = await clusterStorage.executeAll(pageBuilt.sql, pageBuilt.params);
358
394
  if (!rows || rows.length === 0) break;
359
395
  for (var i = 0; i < rows.length; i++) {
360
396
  totalRows++;
@@ -378,16 +414,16 @@ async function migrate(table, opts) {
378
414
  // the cell ciphertext stays as a literal string, not double-sealed.
379
415
  var setCols = Object.keys(update).filter(function (k) { return k !== "_id"; });
380
416
  if (setCols.length > 0) {
381
- // Column names came from the validated policy.columns
382
- // also wrap each in "..." for the same identifier-quoting
383
- // convention.
384
- var setSql = setCols.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
385
- var vals = setCols.map(function (k) { return update[k]; });
386
- vals.push(row._id);
387
- await clusterStorage.execute(
388
- "UPDATE " + qTable + " SET " + setSql + " WHERE _id = ?",
389
- vals
390
- );
417
+ // Column names came from the validated policy.columns. b.sql
418
+ // quotes every SET target + binds every value; the operator app
419
+ // table is quoted (quoteName) and not framework-rewritten.
420
+ var setMap = {};
421
+ for (var sc = 0; sc < setCols.length; sc++) setMap[setCols[sc]] = update[setCols[sc]];
422
+ var updBuilt = sql.update(table, _appSqlOpts())
423
+ .set(setMap)
424
+ .where("_id", row._id)
425
+ .toSql();
426
+ await clusterStorage.execute(updBuilt.sql, updBuilt.params);
391
427
  migratedRows++;
392
428
  }
393
429
  } else {
@@ -662,17 +698,20 @@ async function policySet(table, opts, callerOpts) {
662
698
  auditReasonStorage: validated.auditReasonStorage,
663
699
  updatedAt: Date.now(),
664
700
  };
665
- var sealed = cryptoField.sealRow("_blamejs_break_glass_policies", policyRow);
666
- // UPSERT both Postgres and SQLite support ON CONFLICT.
701
+ var sealed = cryptoField.sealRow(POLICIES_TABLE, policyRow);
702
+ // UPSERT via b.sql ON CONFLICT(tableName) DO UPDATE (Postgres + SQLite).
703
+ // BARE logical framework table — clusterStorage rewrites + placeholderizes;
704
+ // b.sql quotes every column + binds every sealed value. The conflict key
705
+ // (tableName) is excluded from the DO UPDATE set.
667
706
  var keys = Object.keys(sealed);
668
- var cols = keys.join(", ");
669
- var qs = keys.map(function () { return "?"; }).join(", ");
670
- var setSql = keys.filter(function (k) { return k !== "tableName"; })
671
- .map(function (k) { return k + " = excluded." + k; }).join(", ");
672
- var sql = "INSERT INTO _blamejs_break_glass_policies (" + cols + ") " +
673
- "VALUES (" + qs + ") " +
674
- "ON CONFLICT (tableName) DO UPDATE SET " + setSql;
675
- await clusterStorage.execute(sql, keys.map(function (k) { return sealed[k]; }));
707
+ var setCols = keys.filter(function (k) { return k !== "tableName"; });
708
+ var policyBuilt = sql.upsert("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
709
+ .columns(keys)
710
+ .values(sealed)
711
+ .onConflict(["tableName"])
712
+ .doUpdateFromExcluded(setCols)
713
+ .toSql();
714
+ await clusterStorage.execute(policyBuilt.sql, policyBuilt.params);
676
715
  policyCache.delete(table);
677
716
 
678
717
  audit.safeEmit({
@@ -715,15 +754,15 @@ async function policyGet(table) {
715
754
  _requireInit();
716
755
  if (typeof table !== "string" || table.length === 0) return null;
717
756
  if (policyCache.has(table)) return policyCache.get(table);
718
- var rows = await clusterStorage.executeAll(
719
- "SELECT * FROM _blamejs_break_glass_policies WHERE tableName = ?",
720
- [table]
721
- );
757
+ var getBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
758
+ .where("tableName", table)
759
+ .toSql();
760
+ var rows = await clusterStorage.executeAll(getBuilt.sql, getBuilt.params);
722
761
  if (!rows || rows.length === 0) {
723
762
  policyCache.set(table, null);
724
763
  return null;
725
764
  }
726
- var unsealed = cryptoField.unsealRow("_blamejs_break_glass_policies", rows[0]);
765
+ var unsealed = cryptoField.unsealRow(POLICIES_TABLE, rows[0]);
727
766
  var policy = {
728
767
  table: unsealed.tableName,
729
768
  columns: safeJson.parse(unsealed.columnsJson, { maxBytes: C.BYTES.kib(64) }),
@@ -767,9 +806,11 @@ async function policyGet(table) {
767
806
  */
768
807
  async function policyList() {
769
808
  _requireInit();
770
- var rows = await clusterStorage.executeAll(
771
- "SELECT tableName FROM _blamejs_break_glass_policies ORDER BY tableName"
772
- );
809
+ var listBuilt = sql.select("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
810
+ .columns(["tableName"])
811
+ .orderBy("tableName", "asc")
812
+ .toSql();
813
+ var rows = await clusterStorage.executeAll(listBuilt.sql, listBuilt.params);
773
814
  var out = [];
774
815
  for (var i = 0; i < (rows || []).length; i++) {
775
816
  var p = await policyGet(rows[i].tableName);
@@ -801,10 +842,10 @@ async function policyDelete(table, callerOpts) {
801
842
  throw new BreakGlassError("breakglass/bad-policy",
802
843
  "policy.delete: table must be a non-empty string");
803
844
  }
804
- await clusterStorage.execute(
805
- "DELETE FROM _blamejs_break_glass_policies WHERE tableName = ?",
806
- [table]
807
- );
845
+ var delBuilt = sql.delete("_blamejs_break_glass_policies", _sqlOpts()) // allow:hand-rolled-sql
846
+ .where("tableName", table)
847
+ .toSql();
848
+ await clusterStorage.execute(delBuilt.sql, delBuilt.params);
808
849
  policyCache.delete(table);
809
850
  audit.safeEmit({
810
851
  action: "breakglass.policy.delete",
@@ -1100,14 +1141,13 @@ async function grant(opts) {
1100
1141
  ip: ipFromReq,
1101
1142
  kwGrantHalf: null,
1102
1143
  };
1103
- var sealed = cryptoField.sealRow("_blamejs_break_glass_grants", grantRow);
1104
- var keys = Object.keys(sealed);
1105
- var cols = keys.join(", ");
1106
- var qs = keys.map(function () { return "?"; }).join(", ");
1107
- await clusterStorage.execute(
1108
- "INSERT INTO _blamejs_break_glass_grants (" + cols + ") VALUES (" + qs + ")",
1109
- keys.map(function (k) { return sealed[k]; })
1110
- );
1144
+ var sealed = cryptoField.sealRow(GRANTS_TABLE, grantRow);
1145
+ // BARE logical framework table — clusterStorage rewrites + placeholderizes;
1146
+ // b.sql quotes every column + binds every sealed value.
1147
+ var grantInsBuilt = sql.insert("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1148
+ .values(sealed)
1149
+ .toSql();
1150
+ await clusterStorage.execute(grantInsBuilt.sql, grantInsBuilt.params);
1111
1151
 
1112
1152
  // Audit
1113
1153
  var reasonForAudit = _reasonForAudit(reason, policy.auditReasonStorage);
@@ -1273,16 +1313,16 @@ async function unsealRow(grantHandle, table, rowId, opts) {
1273
1313
  throw new BreakGlassError("breakglass/bad-grant-opts",
1274
1314
  "unsealRow: rowId is required");
1275
1315
  }
1276
- var grantRows = await clusterStorage.executeAll(
1277
- "SELECT * FROM _blamejs_break_glass_grants WHERE _id = ?",
1278
- [grantHandle.id]
1279
- );
1316
+ var grantReadBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1317
+ .where("_id", grantHandle.id)
1318
+ .toSql();
1319
+ var grantRows = await clusterStorage.executeAll(grantReadBuilt.sql, grantReadBuilt.params);
1280
1320
  if (!grantRows || grantRows.length === 0) {
1281
1321
  throw new BreakGlassError("breakglass/grant-revoked",
1282
1322
  "unsealRow: grant " + grantHandle.id + " not found (deleted or never issued)", true);
1283
1323
  }
1284
1324
  var sealedGrant = grantRows[0];
1285
- var grantRow = cryptoField.unsealRow("_blamejs_break_glass_grants", sealedGrant);
1325
+ var grantRow = cryptoField.unsealRow(GRANTS_TABLE, sealedGrant);
1286
1326
 
1287
1327
  // Table mismatch
1288
1328
  if (grantRow.scopeTable !== table) {
@@ -1343,30 +1383,41 @@ async function unsealRow(grantHandle, table, rowId, opts) {
1343
1383
  // grant should not be consumed. Without this ordering, a single
1344
1384
  // typo against `maxRowsPerGrant: 1` (the default) exhausts the
1345
1385
  // grant and forces the operator to re-do the step-up ceremony.
1346
- var rows = await clusterStorage.executeAll(
1347
- "SELECT * FROM " + '"' + table + '"' + " WHERE _id = ?",
1348
- [String(rowId)]
1349
- );
1386
+ // Operator app table (validated identifier) — quoteName quotes it; it is
1387
+ // not framework-rewritten.
1388
+ var rowReadBuilt = sql.select(table, _appSqlOpts())
1389
+ .where("_id", String(rowId))
1390
+ .toSql();
1391
+ var rows = await clusterStorage.executeAll(rowReadBuilt.sql, rowReadBuilt.params);
1350
1392
  if (!rows || rows.length === 0) {
1351
1393
  throw new BreakGlassError("breakglass/row-not-found",
1352
1394
  "unsealRow: " + table + "[" + rowId + "] not found", true);
1353
1395
  }
1354
1396
 
1355
- // Increment rowsConsumed (atomic UPDATE with WHERE rowsConsumed < cap
1356
- // so concurrent unseals can't both pass the runtime check above).
1357
- var updateRes = await clusterStorage.execute(
1358
- "UPDATE _blamejs_break_glass_grants " +
1359
- "SET rowsConsumed = rowsConsumed + 1 " +
1360
- "WHERE _id = ? AND rowsConsumed < maxRowsPerGrant AND " +
1361
- "(revokedAt IS NULL) AND expiresAt > ?",
1362
- [grantHandle.id, Date.now()]
1363
- );
1397
+ // Increment rowsConsumed (atomic UPDATE with WHERE rowsConsumed < cap so
1398
+ // concurrent unseals can't both pass the runtime check above). The
1399
+ // rowsConsumed+1 RHS + the rowsConsumed<maxRowsPerGrant column comparison
1400
+ // are guarded raw fragments (b.guardSql + placeholder/literal scan). The
1401
+ // identifier quoting in those raw fragments is dialect-aware (backticks on
1402
+ // MySQL, double-quotes on PG/SQLite) so the column references resolve as
1403
+ // identifiers, not string literals, on the active backend.
1404
+ var incDialect = clusterStorage.dialect();
1405
+ var incBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1406
+ .setRaw("rowsConsumed", safeSql.quoteIdentifier("rowsConsumed", incDialect) + " + 1", [])
1407
+ .where("_id", grantHandle.id)
1408
+ .whereRaw(safeSql.quoteIdentifier("rowsConsumed", incDialect) + " < " +
1409
+ safeSql.quoteIdentifier("maxRowsPerGrant", incDialect), [])
1410
+ .whereNull("revokedAt")
1411
+ .whereOp("expiresAt", ">", Date.now())
1412
+ .toSql();
1413
+ var updateRes = await clusterStorage.execute(incBuilt.sql, incBuilt.params);
1364
1414
  // executeAll-style result; some backends return rowsAffected, others a count.
1365
1415
  // Re-query to confirm the increment landed and get the post-increment counter.
1366
- var postRows = await clusterStorage.executeAll(
1367
- "SELECT rowsConsumed, revokedAt, expiresAt FROM _blamejs_break_glass_grants WHERE _id = ?",
1368
- [grantHandle.id]
1369
- );
1416
+ var postReadBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1417
+ .columns(["rowsConsumed", "revokedAt", "expiresAt"])
1418
+ .where("_id", grantHandle.id)
1419
+ .toSql();
1420
+ var postRows = await clusterStorage.executeAll(postReadBuilt.sql, postReadBuilt.params);
1370
1421
  if (!postRows || postRows.length === 0) {
1371
1422
  throw new BreakGlassError("breakglass/grant-revoked",
1372
1423
  "unsealRow: grant " + grantHandle.id + " disappeared during unseal", true);
@@ -1469,11 +1520,14 @@ async function revoke(grantId, opts) {
1469
1520
  }
1470
1521
  opts = opts || {};
1471
1522
  var nowMs = Date.now();
1472
- await clusterStorage.execute(
1473
- "UPDATE _blamejs_break_glass_grants SET revokedAt = ? " +
1474
- "WHERE _id = ? AND revokedAt IS NULL",
1475
- [nowMs, grantId]
1476
- );
1523
+ // revokedAt IS NULL keeps the revoke idempotent (already-revoked grants
1524
+ // keep their original timestamp).
1525
+ var revBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1526
+ .set({ revokedAt: nowMs })
1527
+ .where("_id", grantId)
1528
+ .whereNull("revokedAt")
1529
+ .toSql();
1530
+ await clusterStorage.execute(revBuilt.sql, revBuilt.params);
1477
1531
  audit.safeEmit({
1478
1532
  action: "breakglass.grant.revoked",
1479
1533
  outcome: "success",
@@ -1516,19 +1570,25 @@ async function listActive(opts) {
1516
1570
  // Use cryptoField's computeDerived so the hash matches the table's
1517
1571
  // hashNamespace prefix — raw sha3Hash would produce a different value.
1518
1572
  var derived = cryptoField.computeDerived(
1519
- "_blamejs_break_glass_grants", "issuedToActorId", actorId
1573
+ GRANTS_TABLE, "issuedToActorId", actorId
1520
1574
  );
1521
1575
  if (!derived) return [];
1522
1576
  var nowMs = Date.now();
1523
- var rows = await clusterStorage.executeAll(
1524
- "SELECT * FROM _blamejs_break_glass_grants " +
1525
- "WHERE issuedToActorHash = ? AND (revokedAt IS NULL) AND expiresAt > ? AND rowsConsumed < maxRowsPerGrant " +
1526
- "ORDER BY issuedAt DESC",
1527
- [derived.value, nowMs]
1528
- );
1577
+ // rowsConsumed < maxRowsPerGrant is a column-to-column comparison (guarded
1578
+ // raw fragment); every other predicate is structured.
1579
+ var laDialect = clusterStorage.dialect();
1580
+ var laBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1581
+ .where("issuedToActorHash", derived.value)
1582
+ .whereNull("revokedAt")
1583
+ .whereOp("expiresAt", ">", nowMs)
1584
+ .whereRaw(safeSql.quoteIdentifier("rowsConsumed", laDialect) + " < " +
1585
+ safeSql.quoteIdentifier("maxRowsPerGrant", laDialect), [])
1586
+ .orderBy("issuedAt", "desc")
1587
+ .toSql();
1588
+ var rows = await clusterStorage.executeAll(laBuilt.sql, laBuilt.params);
1529
1589
  var out = [];
1530
1590
  for (var i = 0; i < (rows || []).length; i++) {
1531
- var u = cryptoField.unsealRow("_blamejs_break_glass_grants", rows[i]);
1591
+ var u = cryptoField.unsealRow(GRANTS_TABLE, rows[i]);
1532
1592
  out.push({
1533
1593
  id: u._id,
1534
1594
  scopeTable: u.scopeTable,
@@ -1644,11 +1704,12 @@ async function unsealRowAsService(req, table, rowId, opts) {
1644
1704
  }
1645
1705
 
1646
1706
  // Fetch + unseal the row (Model A or Model B path, same as
1647
- // operator-initiated unsealRow).
1648
- var rows = await clusterStorage.executeAll(
1649
- "SELECT * FROM " + '"' + table + '"' + " WHERE _id = ?",
1650
- [String(rowId)]
1651
- );
1707
+ // operator-initiated unsealRow). Operator app table — quoteName quotes it;
1708
+ // it is not framework-rewritten.
1709
+ var svcRowBuilt = sql.select(table, _appSqlOpts())
1710
+ .where("_id", String(rowId))
1711
+ .toSql();
1712
+ var rows = await clusterStorage.executeAll(svcRowBuilt.sql, svcRowBuilt.params);
1652
1713
  if (!rows || rows.length === 0) {
1653
1714
  throw new BreakGlassError("breakglass/row-not-found",
1654
1715
  "unsealRowAsService: " + table + "[" + rowId + "] not found", true);
@@ -1722,24 +1783,22 @@ async function listActiveAll(opts) {
1722
1783
  _requireInit();
1723
1784
  opts = opts || {};
1724
1785
  var nowMs = Date.now();
1725
- var clauses = ["(revokedAt IS NULL)", "expiresAt > ?", "rowsConsumed < maxRowsPerGrant"];
1726
- var params = [nowMs];
1727
- if (opts.table) {
1728
- clauses.push("scopeTable = ?");
1729
- params.push(opts.table);
1730
- }
1731
- if (opts.since) {
1732
- clauses.push("issuedAt >= ?");
1733
- params.push(opts.since);
1734
- }
1735
- var rows = await clusterStorage.executeAll(
1736
- "SELECT * FROM _blamejs_break_glass_grants WHERE " + clauses.join(" AND ") +
1737
- " ORDER BY issuedAt DESC",
1738
- params
1739
- );
1786
+ // rowsConsumed < maxRowsPerGrant is a column-to-column comparison (guarded
1787
+ // raw fragment); the rest are structured predicates.
1788
+ var laaDialect = clusterStorage.dialect();
1789
+ var laaQb = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1790
+ .whereNull("revokedAt")
1791
+ .whereOp("expiresAt", ">", nowMs)
1792
+ .whereRaw(safeSql.quoteIdentifier("rowsConsumed", laaDialect) + " < " +
1793
+ safeSql.quoteIdentifier("maxRowsPerGrant", laaDialect), []);
1794
+ if (opts.table) laaQb.where("scopeTable", opts.table);
1795
+ if (opts.since) laaQb.whereOp("issuedAt", ">=", opts.since);
1796
+ laaQb.orderBy("issuedAt", "desc");
1797
+ var laaBuilt = laaQb.toSql();
1798
+ var rows = await clusterStorage.executeAll(laaBuilt.sql, laaBuilt.params);
1740
1799
  var out = [];
1741
1800
  for (var i = 0; i < (rows || []).length; i++) {
1742
- var u = cryptoField.unsealRow("_blamejs_break_glass_grants", rows[i]);
1801
+ var u = cryptoField.unsealRow(GRANTS_TABLE, rows[i]);
1743
1802
  out.push({
1744
1803
  id: u._id,
1745
1804
  issuedToActorId: u.issuedToActorId,
@@ -1796,31 +1855,28 @@ async function revokeAll(criteria, opts) {
1796
1855
  "revokeAll: at least one of { actorId, table } is required (refusing to mass-revoke without scope)");
1797
1856
  }
1798
1857
  opts = opts || {};
1799
- var clauses = ["revokedAt IS NULL"];
1800
- var params = [];
1801
- if (criteria.actorId) {
1802
- var derived = cryptoField.computeDerived(
1803
- "_blamejs_break_glass_grants", "issuedToActorId", criteria.actorId
1804
- );
1805
- if (derived) {
1806
- clauses.push("issuedToActorHash = ?");
1807
- params.push(derived.value);
1808
- }
1809
- }
1810
- if (criteria.table) {
1811
- clauses.push("scopeTable = ?");
1812
- params.push(criteria.table);
1858
+ // The SELECT (snapshot ids) and UPDATE (apply revoke) share one predicate
1859
+ // set; applyRevokeCriteria replays it onto either builder so the WHERE can
1860
+ // never drift between the two.
1861
+ var derived = criteria.actorId
1862
+ ? cryptoField.computeDerived(GRANTS_TABLE, "issuedToActorId", criteria.actorId)
1863
+ : null;
1864
+ function applyRevokeCriteria(qb) {
1865
+ qb.whereNull("revokedAt");
1866
+ if (criteria.actorId && derived) qb.where("issuedToActorHash", derived.value);
1867
+ if (criteria.table) qb.where("scopeTable", criteria.table);
1868
+ return qb;
1813
1869
  }
1814
1870
  // Snapshot the to-be-revoked grant ids first so audit captures specifics.
1815
- var ids = await clusterStorage.executeAll(
1816
- "SELECT _id FROM _blamejs_break_glass_grants WHERE " + clauses.join(" AND "),
1817
- params
1818
- );
1871
+ var idSelBuilt = applyRevokeCriteria(
1872
+ sql.select("_blamejs_break_glass_grants", _sqlOpts()).columns(["_id"]) // allow:hand-rolled-sql
1873
+ ).toSql();
1874
+ var ids = await clusterStorage.executeAll(idSelBuilt.sql, idSelBuilt.params);
1819
1875
  var nowMs = Date.now();
1820
- await clusterStorage.execute(
1821
- "UPDATE _blamejs_break_glass_grants SET revokedAt = ? WHERE " + clauses.join(" AND "),
1822
- [nowMs].concat(params)
1823
- );
1876
+ var revAllBuilt = applyRevokeCriteria(
1877
+ sql.update("_blamejs_break_glass_grants", _sqlOpts()).set({ revokedAt: nowMs }) // allow:hand-rolled-sql
1878
+ ).toSql();
1879
+ await clusterStorage.execute(revAllBuilt.sql, revAllBuilt.params);
1824
1880
  audit.safeEmit({
1825
1881
  action: "breakglass.admin.revokeall",
1826
1882
  outcome: "success",
@@ -1841,11 +1897,12 @@ async function revokeAll(criteria, opts) {
1841
1897
  async function _sweepExpired(opts) {
1842
1898
  opts = opts || {};
1843
1899
  var nowMs = Date.now();
1844
- var expired = await clusterStorage.executeAll(
1845
- "SELECT _id, issuedToActorId, scopeTable, rowsConsumed FROM _blamejs_break_glass_grants " +
1846
- "WHERE revokedAt IS NULL AND expiresAt <= ?",
1847
- [nowMs]
1848
- );
1900
+ var expiredBuilt = sql.select("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1901
+ .columns(["_id", "issuedToActorId", "scopeTable", "rowsConsumed"])
1902
+ .whereNull("revokedAt")
1903
+ .whereOp("expiresAt", "<=", nowMs)
1904
+ .toSql();
1905
+ var expired = await clusterStorage.executeAll(expiredBuilt.sql, expiredBuilt.params);
1849
1906
  for (var i = 0; i < (expired || []).length; i++) {
1850
1907
  var row = expired[i];
1851
1908
  audit.safeEmit({
@@ -1855,11 +1912,12 @@ async function _sweepExpired(opts) {
1855
1912
  metadata: { grantId: row._id, table: row.scopeTable, rowsConsumed: Number(row.rowsConsumed) },
1856
1913
  });
1857
1914
  }
1858
- await clusterStorage.execute(
1859
- "UPDATE _blamejs_break_glass_grants SET revokedAt = ? " +
1860
- "WHERE revokedAt IS NULL AND expiresAt <= ?",
1861
- [nowMs, nowMs]
1862
- );
1915
+ var sweepUpdBuilt = sql.update("_blamejs_break_glass_grants", _sqlOpts()) // allow:hand-rolled-sql
1916
+ .set({ revokedAt: nowMs })
1917
+ .whereNull("revokedAt")
1918
+ .whereOp("expiresAt", "<=", nowMs)
1919
+ .toSql();
1920
+ await clusterStorage.execute(sweepUpdBuilt.sql, sweepUpdBuilt.params);
1863
1921
  return { expired: (expired || []).length };
1864
1922
  }
1865
1923