@blamejs/core 0.14.26 → 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 (150) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/agent-envelope-mac.js +104 -0
  5. package/lib/agent-event-bus.js +105 -4
  6. package/lib/agent-posture-chain.js +8 -42
  7. package/lib/ai-content-detect.js +9 -10
  8. package/lib/api-key.js +107 -74
  9. package/lib/atomic-file.js +62 -4
  10. package/lib/audit-chain.js +47 -11
  11. package/lib/audit-sign.js +77 -2
  12. package/lib/audit-tools.js +79 -51
  13. package/lib/audit.js +249 -123
  14. package/lib/auth/openid-federation.js +108 -47
  15. package/lib/backup/index.js +13 -10
  16. package/lib/break-glass.js +202 -144
  17. package/lib/cache.js +174 -105
  18. package/lib/chain-writer.js +38 -16
  19. package/lib/cli.js +19 -14
  20. package/lib/cluster-provider-db.js +130 -104
  21. package/lib/cluster-storage.js +119 -22
  22. package/lib/cluster.js +119 -71
  23. package/lib/compliance.js +169 -4
  24. package/lib/consent.js +73 -24
  25. package/lib/constants.js +16 -11
  26. package/lib/crypto-field.js +474 -92
  27. package/lib/db-declare-row-policy.js +35 -22
  28. package/lib/db-file-lifecycle.js +3 -2
  29. package/lib/db-query.js +497 -255
  30. package/lib/db-schema.js +209 -44
  31. package/lib/db.js +176 -95
  32. package/lib/error-page.js +14 -1
  33. package/lib/external-db-migrate.js +229 -139
  34. package/lib/external-db.js +25 -15
  35. package/lib/file-upload.js +52 -7
  36. package/lib/framework-error.js +14 -1
  37. package/lib/framework-files.js +73 -0
  38. package/lib/framework-schema.js +695 -394
  39. package/lib/gate-contract.js +649 -1
  40. package/lib/guard-agent-registry.js +26 -44
  41. package/lib/guard-all.js +1 -0
  42. package/lib/guard-auth.js +42 -112
  43. package/lib/guard-cidr.js +33 -154
  44. package/lib/guard-csv.js +46 -113
  45. package/lib/guard-domain.js +34 -157
  46. package/lib/guard-dsn.js +27 -43
  47. package/lib/guard-email.js +47 -69
  48. package/lib/guard-envelope.js +19 -32
  49. package/lib/guard-event-bus-payload.js +24 -42
  50. package/lib/guard-event-bus-topic.js +25 -43
  51. package/lib/guard-filename.js +42 -106
  52. package/lib/guard-graphql.js +42 -123
  53. package/lib/guard-html.js +53 -108
  54. package/lib/guard-idempotency-key.js +24 -42
  55. package/lib/guard-image.js +46 -103
  56. package/lib/guard-imap-command.js +18 -32
  57. package/lib/guard-jmap.js +16 -30
  58. package/lib/guard-json.js +38 -108
  59. package/lib/guard-jsonpath.js +38 -171
  60. package/lib/guard-jwt.js +49 -179
  61. package/lib/guard-list-id.js +25 -41
  62. package/lib/guard-list-unsubscribe.js +27 -43
  63. package/lib/guard-mail-compose.js +24 -42
  64. package/lib/guard-mail-move.js +26 -44
  65. package/lib/guard-mail-query.js +28 -46
  66. package/lib/guard-mail-reply.js +24 -42
  67. package/lib/guard-mail-sieve.js +24 -42
  68. package/lib/guard-managesieve-command.js +17 -31
  69. package/lib/guard-markdown.js +37 -104
  70. package/lib/guard-message-id.js +26 -45
  71. package/lib/guard-mime.js +39 -151
  72. package/lib/guard-oauth.js +54 -135
  73. package/lib/guard-pdf.js +45 -101
  74. package/lib/guard-pop3-command.js +21 -31
  75. package/lib/guard-posture-chain.js +24 -42
  76. package/lib/guard-regex.js +33 -107
  77. package/lib/guard-saga-config.js +24 -42
  78. package/lib/guard-shell.js +42 -172
  79. package/lib/guard-smtp-command.js +48 -54
  80. package/lib/guard-snapshot-envelope.js +24 -42
  81. package/lib/guard-sql.js +1491 -0
  82. package/lib/guard-stream-args.js +24 -43
  83. package/lib/guard-svg.js +47 -65
  84. package/lib/guard-template.js +35 -172
  85. package/lib/guard-tenant-id.js +26 -45
  86. package/lib/guard-time.js +32 -154
  87. package/lib/guard-trace-context.js +25 -44
  88. package/lib/guard-uuid.js +32 -153
  89. package/lib/guard-xml.js +38 -113
  90. package/lib/guard-yaml.js +51 -163
  91. package/lib/http-client.js +37 -9
  92. package/lib/inbox.js +120 -107
  93. package/lib/legal-hold.js +107 -50
  94. package/lib/log-stream-cloudwatch.js +47 -31
  95. package/lib/log-stream-otlp.js +32 -18
  96. package/lib/mail-crypto-smime.js +2 -6
  97. package/lib/mail-greylist.js +2 -6
  98. package/lib/mail-helo.js +2 -6
  99. package/lib/mail-journal.js +85 -64
  100. package/lib/mail-rbl.js +2 -6
  101. package/lib/mail-scan.js +2 -6
  102. package/lib/mail-server-jmap.js +117 -12
  103. package/lib/mail-spam-score.js +2 -6
  104. package/lib/mail-store.js +287 -154
  105. package/lib/middleware/body-parser.js +71 -25
  106. package/lib/middleware/csrf-protect.js +19 -8
  107. package/lib/middleware/fetch-metadata.js +17 -7
  108. package/lib/middleware/idempotency-key.js +54 -38
  109. package/lib/middleware/rate-limit.js +102 -32
  110. package/lib/middleware/security-headers.js +21 -5
  111. package/lib/migrations.js +108 -66
  112. package/lib/network-heartbeat.js +7 -0
  113. package/lib/nonce-store.js +31 -9
  114. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  115. package/lib/object-store/azure-blob.js +57 -3
  116. package/lib/object-store/sigv4.js +10 -0
  117. package/lib/observability.js +87 -0
  118. package/lib/otel-export.js +25 -1
  119. package/lib/outbox.js +136 -82
  120. package/lib/parsers/safe-xml.js +47 -7
  121. package/lib/pqc-agent.js +44 -0
  122. package/lib/pubsub-cluster.js +42 -20
  123. package/lib/queue-local.js +202 -139
  124. package/lib/queue-redis.js +9 -1
  125. package/lib/queue-sqs.js +6 -0
  126. package/lib/redact.js +68 -11
  127. package/lib/redis-client.js +160 -31
  128. package/lib/retention.js +82 -39
  129. package/lib/router.js +212 -5
  130. package/lib/safe-dns.js +29 -45
  131. package/lib/safe-ical.js +18 -33
  132. package/lib/safe-icap.js +27 -43
  133. package/lib/safe-sieve.js +21 -40
  134. package/lib/safe-sql.js +124 -3
  135. package/lib/safe-vcard.js +18 -33
  136. package/lib/scheduler.js +35 -12
  137. package/lib/seeders.js +122 -74
  138. package/lib/session-stores.js +42 -14
  139. package/lib/session.js +109 -72
  140. package/lib/sql.js +3885 -0
  141. package/lib/ssrf-guard.js +51 -4
  142. package/lib/static.js +177 -34
  143. package/lib/subject.js +55 -17
  144. package/lib/vault/index.js +3 -2
  145. package/lib/vault/passphrase-ops.js +3 -2
  146. package/lib/vault/rotate.js +104 -64
  147. package/lib/vendor-data.js +2 -0
  148. package/lib/websocket.js +35 -5
  149. package/package.json +1 -1
  150. package/sbom.cdx.json +6 -6
@@ -50,6 +50,7 @@ var lazyRequire = require("./lazy-require");
50
50
  var numericBounds = require("./numeric-bounds");
51
51
  var safeJson = require("./safe-json");
52
52
  var safeSql = require("./safe-sql");
53
+ var sql = require("./sql");
53
54
  var scheduler = require("./scheduler");
54
55
  var { QueueError } = require("./framework-error");
55
56
 
@@ -59,10 +60,16 @@ var _err = QueueError.factory;
59
60
  // COLUMN→seal map registered in db.js's FRAMEWORK_SCHEMA, NOT the
60
61
  // physical table the SQL writes to. An operator who points the backend
61
62
  // at their own table still seals payload + lastError through this map,
62
- // so a bring-your-own table inherits the same at-rest protection.
63
+ // so a bring-your-own table inherits the same at-rest protection. The KEY
64
+ // must stay byte-identical to db.js's registerTable literal.
65
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not a SQL table.
63
66
  var SEAL_TABLE = "_blamejs_jobs";
64
67
 
65
- // Default physical table for the local backend.
68
+ // Default LOGICAL table for the local backend. Passed BARE to b.sql so
69
+ // clusterStorage.resolveTables rewrites it to the configured cluster name
70
+ // (applying the configurable prefix); a custom config.table is quoted at
71
+ // build time instead. b.sql owns the quoting; this is the logical name.
72
+ // allow:hand-rolled-sql — framework logical jobs-table name handed to b.sql, not a SQL literal.
66
73
  var DEFAULT_TABLE = "_blamejs_jobs";
67
74
 
68
75
  // vault is lazy-required because some flows (sealed lastError) only
@@ -111,14 +118,6 @@ var LEASE_RETURN_COLS = [
111
118
  "repeatCron", "repeatTimezone", "flowId", "flowChildName",
112
119
  ];
113
120
 
114
- function _quotedList(cols) {
115
- return cols.map(function (c) { return '"' + c + '"'; }).join(", ");
116
- }
117
-
118
- function _placeholders(cols) {
119
- return cols.map(function () { return "?"; }).join(", ");
120
- }
121
-
122
121
  function _shapeLeasedRow(raw) {
123
122
  // raw is a row coming back from RETURNING — payload is sealed if
124
123
  // present. Run through cryptoField's unseal pipeline so the caller
@@ -162,12 +161,19 @@ function _resolveStore(handle) {
162
161
  return handle;
163
162
  }
164
163
 
165
- // Compose the physical table reference from config.table + config.schema,
166
- // quoting each identifier through b.safeSql so an operator-supplied name
167
- // cannot interpolate SQL through the identifier slot (CWE-89). Returns
168
- // the bare default name unquoted when no custom table/schema is given so
169
- // the framework's cluster-mode table rewrite (resolveTables) still fires
170
- // on the default jobs table; any custom name is fully validated + quoted.
164
+ // Resolve the b.sql table-builder options from config.table + config.schema.
165
+ // Every SQL statement is composed through b.sql, which quotes identifiers
166
+ // through b.safeSql so an operator-supplied name cannot interpolate SQL
167
+ // through the identifier slot (CWE-89). The DEFAULT logical table is passed
168
+ // BARE (quoteName off) so the framework's cluster-mode rewrite
169
+ // (clusterStorage.resolveTables) still fires on the jobs table and applies
170
+ // the configurable prefix; any custom table/schema is validated + quoted at
171
+ // build time instead (no rewrite — it is the operator's own table).
172
+ //
173
+ // Returns { name, opts } where `opts` is spread into every b.sql verb call:
174
+ // default → { dialect: "sqlite" } (bare name, rewritten)
175
+ // custom → { dialect: "sqlite", quoteName: true } (quoted, no rewrite)
176
+ // custom+schema → adds { schema } (b.sql emits the quoted schema.table form)
171
177
  function _resolveTableRef(config) {
172
178
  var table = config.table !== undefined && config.table !== null
173
179
  ? config.table : DEFAULT_TABLE;
@@ -183,23 +189,33 @@ function _resolveTableRef(config) {
183
189
  var usingDefault = (table === DEFAULT_TABLE) &&
184
190
  (schema === undefined || schema === null);
185
191
  if (usingDefault) {
186
- // Byte-identical default SQL unquoted bare name so cluster-mode
187
- // resolveTables continues to recognize and rewrite the jobs table.
188
- return DEFAULT_TABLE;
192
+ // Bare default — b.sql leaves it unquoted so cluster-mode resolveTables
193
+ // recognizes + rewrites the jobs table (and applies the prefix).
194
+ return { name: DEFAULT_TABLE, opts: { dialect: "sqlite" } };
189
195
  }
190
- // Any custom table/schema is validated + dialect-quoted. validateIdentifier
191
- // / quoteQualified THROW (SafeSqlError) on a bad identifier; surface that
192
- // as the queue's config-time error so the operator catches the typo at
193
- // boot rather than on first enqueue.
196
+ // Any custom table/schema is validated + dialect-quoted by b.sql at build
197
+ // time. validateIdentifier (run inside b.sql's TableRef) THROWs
198
+ // (SqlBuilderError / SafeSqlError) on a bad identifier; surface that as the
199
+ // queue's config-time error so the operator catches the typo at boot rather
200
+ // than on first enqueue.
201
+ var opts = { dialect: "sqlite", quoteName: true };
202
+ if (schema !== undefined && schema !== null && schema !== "") opts.schema = schema;
194
203
  try {
195
- if (schema !== undefined && schema !== null && schema !== "") {
196
- return safeSql.quoteQualified([schema, table]);
197
- }
198
- return safeSql.quoteIdentifier(table);
204
+ // Validate the custom identifier(s) at config time with the STRICTER
205
+ // policy (allowReserved off — a reserved word like `select` is refused
206
+ // for a bring-your-own queue table, matching the prior
207
+ // quoteIdentifier / quoteQualified contract). b.sql then quotes the
208
+ // already-validated name at build time. validateIdentifier THROWs
209
+ // (SafeSqlError) on a bad shape / reserved word / injection-shaped
210
+ // schema, surfaced here as the queue's config-time error so the
211
+ // operator catches the typo at boot rather than on first enqueue.
212
+ safeSql.validateIdentifier(table);
213
+ if (opts.schema) safeSql.validateIdentifier(opts.schema);
199
214
  } catch (e) {
200
215
  throw _err("INVALID_TABLE",
201
216
  "queue local table/schema failed identifier validation: " + e.message, true);
202
217
  }
218
+ return { name: table, opts: opts };
203
219
  }
204
220
 
205
221
  function create(config) {
@@ -208,11 +224,20 @@ function create(config) {
208
224
  // exactly: cluster-storage dispatch to the framework's main DB
209
225
  // (single-node) / external-db (cluster), table "_blamejs_jobs".
210
226
  var store = _resolveStore(config.db);
211
- // qTable holds the physical table reference already validated +
212
- // dialect-quoted by _resolveTableRef (via safeSql.quoteIdentifier /
213
- // quoteQualified, or the framework's bare default name). The `q` prefix
214
- // marks it as a safe-to-interpolate identifier so it is never re-quoted.
215
- var qTable = _resolveTableRef(config);
227
+ // ref = { name, opts } for every b.sql verb call — the bare default
228
+ // jobs table (clusterStorage rewrites it + applies the configurable
229
+ // prefix) or a validated + quoted custom table/schema. Small helpers
230
+ // open each verb builder pre-bound to this table so the table reference
231
+ // is resolved in exactly one place.
232
+ var ref = _resolveTableRef(config);
233
+ function _select() { return sql.select(ref.name, ref.opts); }
234
+ function _insert() { return sql.insert(ref.name, ref.opts); }
235
+ function _update() { return sql.update(ref.name, ref.opts); }
236
+ function _delete() { return sql.delete(ref.name, ref.opts); }
237
+ // Quoted column expression for a setRaw RHS that references the column's
238
+ // own pre-update value (attempts/availableAt). dialect-sqlite quoting is
239
+ // the double-quote form clusterStorage's Postgres path keeps.
240
+ function _qc(col) { return safeSql.quoteIdentifier(col, "sqlite", { allowReserved: true }); }
216
241
 
217
242
  async function enqueue(queueName, payload, opts) {
218
243
  cluster.requireLeader();
@@ -280,13 +305,16 @@ function create(config) {
280
305
  dependsOn: dependsOn,
281
306
  };
282
307
  var sealed = cryptoField.sealRow(SEAL_TABLE, row);
283
- var values = JOB_COLS.map(function (c) { return c in sealed ? sealed[c] : null; });
284
-
285
- await store.execute(
286
- "INSERT INTO " + qTable + " (" + _quotedList(JOB_COLS) + ") " +
287
- "VALUES (" + _placeholders(JOB_COLS) + ")",
288
- values
289
- );
308
+ // Build the full column→value map in JOB_COLS order (a missing sealed
309
+ // column binds NULL, matching the prior positional-values shape). b.sql
310
+ // quotes every column + binds every value as a placeholder.
311
+ var insertRow = {};
312
+ for (var ci = 0; ci < JOB_COLS.length; ci++) {
313
+ var col = JOB_COLS[ci];
314
+ insertRow[col] = col in sealed ? sealed[col] : null;
315
+ }
316
+ var insertBuilt = _insert().columns(JOB_COLS).values(insertRow).toSql();
317
+ await store.execute(insertBuilt.sql, insertBuilt.params);
290
318
  return {
291
319
  jobId: row._id,
292
320
  queueName: queueName,
@@ -307,21 +335,28 @@ function create(config) {
307
335
  // rows that still match status='pending' after the lock acquires
308
336
  // (Postgres EvalPlanQual; SQLite is single-writer so the same row
309
337
  // can't be picked twice). RETURNING hands back the leased columns
310
- // so we don't need a separate SELECT after the UPDATE.
311
- var sql =
312
- "UPDATE " + qTable + " " +
313
- "SET status = 'inflight', leasedAt = ?, leaseExpiresAt = ?, attempts = attempts + 1 " +
314
- "WHERE _id IN (" +
315
- " SELECT _id FROM " + qTable + " " +
316
- " WHERE queueName = ? AND status = 'pending' AND availableAt <= ? " +
317
- " ORDER BY priority DESC, availableAt ASC, enqueuedAt ASC " +
318
- " LIMIT ?" +
319
- ") " +
320
- "RETURNING " + _quotedList(LEASE_RETURN_COLS);
321
- var result = await store.execute(
322
- sql,
323
- [nowMs, leaseExpiresAt, queueName, nowMs, maxRows]
324
- );
338
+ // so we don't need a separate SELECT after the UPDATE. maxRows is a
339
+ // framework-computed integer emitted inline via b.sql's .limit() (a
340
+ // bound LIMIT param has no portable form across the subquery path);
341
+ // attempts = attempts + 1 is a setRaw over the column's own value.
342
+ var leaseInner = _select()
343
+ .columns(["_id"])
344
+ .where("queueName", queueName)
345
+ .where("status", "pending")
346
+ .whereOp("availableAt", "<=", nowMs)
347
+ .orderBy("priority", "desc")
348
+ .orderBy("availableAt", "asc")
349
+ .orderBy("enqueuedAt", "asc")
350
+ .limit(maxRows);
351
+ var leaseBuilt = _update()
352
+ .set("status", "inflight")
353
+ .set("leasedAt", nowMs)
354
+ .set("leaseExpiresAt", leaseExpiresAt)
355
+ .setRaw("attempts", _qc("attempts") + " + 1", [])
356
+ .whereIn("_id", leaseInner)
357
+ .returning(LEASE_RETURN_COLS)
358
+ .toSql();
359
+ var result = await store.execute(leaseBuilt.sql, leaseBuilt.params);
325
360
  var leased = [];
326
361
  for (var i = 0; i < result.rows.length; i++) {
327
362
  leased.push(_shapeLeasedRow(result.rows[i]));
@@ -340,11 +375,12 @@ function create(config) {
340
375
  "extendLease: additionalMs must be a positive number", true);
341
376
  }
342
377
  var newExpiry = Date.now() + additionalMs;
343
- var result = await store.execute(
344
- "UPDATE " + qTable + " SET leaseExpiresAt = ? " +
345
- "WHERE _id = ? AND status = 'inflight'",
346
- [newExpiry, jobId]
347
- );
378
+ var built = _update()
379
+ .set("leaseExpiresAt", newExpiry)
380
+ .where("_id", jobId)
381
+ .where("status", "inflight")
382
+ .toSql();
383
+ var result = await store.execute(built.sql, built.params);
348
384
  return (result.rowCount || 0) > 0;
349
385
  }
350
386
 
@@ -355,19 +391,22 @@ function create(config) {
355
391
  // the status flip. Single SELECT + UPDATE pair under the same
356
392
  // jobId — race-free under SQLite (single-writer); cluster-storage
357
393
  // dispatches both calls to the same backend.
358
- var rowRes = await store.execute(
359
- "SELECT _id, queueName, payload, repeatCron, repeatTimezone, " +
360
- " flowId, flowChildName, priority, classification, traceId " +
361
- "FROM " + qTable + " WHERE _id = ?",
362
- [jobId]
363
- );
394
+ var rowBuilt = _select()
395
+ .columns(["_id", "queueName", "payload", "repeatCron", "repeatTimezone",
396
+ "flowId", "flowChildName", "priority", "classification", "traceId"])
397
+ .where("_id", jobId)
398
+ .toSql();
399
+ var rowRes = await store.execute(rowBuilt.sql, rowBuilt.params);
364
400
  var row = (rowRes && rowRes.rows && rowRes.rows[0]) || null;
365
401
 
366
- await store.execute(
367
- "UPDATE " + qTable + " SET status = 'done', finishedAt = ?, leaseExpiresAt = NULL " +
368
- "WHERE _id = ? AND status = 'inflight'",
369
- [nowMs, jobId]
370
- );
402
+ var doneBuilt = _update()
403
+ .set("status", "done")
404
+ .set("finishedAt", nowMs)
405
+ .set("leaseExpiresAt", null)
406
+ .where("_id", jobId)
407
+ .where("status", "inflight")
408
+ .toSql();
409
+ await store.execute(doneBuilt.sql, doneBuilt.params);
371
410
 
372
411
  // Repeat-in-queue: cron-recurring job re-enqueues itself for the
373
412
  // next firing time. Failures (which take the fail() path) don't
@@ -404,11 +443,13 @@ function create(config) {
404
443
  }
405
444
 
406
445
  async function _maybeReleaseFlowChildren(flowId, completedJobId, completedChildName, nowMs) {
407
- var siblingsRes = await store.execute(
408
- "SELECT _id, dependsOn, flowChildName, status, availableAt FROM " + qTable + " " +
409
- "WHERE flowId = ? AND status = 'pending' AND availableAt > ?",
410
- [flowId, nowMs]
411
- );
446
+ var siblingsBuilt = _select()
447
+ .columns(["_id", "dependsOn", "flowChildName", "status", "availableAt"])
448
+ .where("flowId", flowId)
449
+ .where("status", "pending")
450
+ .whereOp("availableAt", ">", nowMs)
451
+ .toSql();
452
+ var siblingsRes = await store.execute(siblingsBuilt.sql, siblingsBuilt.params);
412
453
  var siblings = (siblingsRes && siblingsRes.rows) || [];
413
454
  for (var i = 0; i < siblings.length; i++) {
414
455
  var sib = siblings[i];
@@ -424,19 +465,25 @@ function create(config) {
424
465
  var dep = deps[d];
425
466
  // Quick path: just-completed job matches by id or child name.
426
467
  if (dep === completedJobId || (completedChildName && dep === completedChildName)) continue;
427
- // Otherwise SELECT to confirm done.
428
- var depRes = await store.execute(
429
- "SELECT 1 FROM " + qTable + " WHERE flowId = ? AND status = 'done' AND " +
430
- " (_id = ? OR flowChildName = ?) LIMIT 1",
431
- [flowId, dep, dep]
432
- );
468
+ // Otherwise SELECT to confirm done. The (_id = ? OR flowChildName = ?)
469
+ // disjunction is a whereGroup so it AND-composes at one precedence
470
+ // level with the flowId + status equalities.
471
+ var depBuilt = _select()
472
+ .columns(["_id"])
473
+ .where("flowId", flowId)
474
+ .where("status", "done")
475
+ .whereGroup(function (g) { g.where("_id", dep).orWhere("flowChildName", dep); })
476
+ .limit(1)
477
+ .toSql();
478
+ var depRes = await store.execute(depBuilt.sql, depBuilt.params);
433
479
  if (!depRes || !depRes.rows || depRes.rows.length === 0) { allDone = false; break; }
434
480
  }
435
481
  if (allDone) {
436
- await store.execute(
437
- "UPDATE " + qTable + " SET availableAt = ? WHERE _id = ?",
438
- [nowMs, sib._id]
439
- );
482
+ var releaseBuilt = _update()
483
+ .set("availableAt", nowMs)
484
+ .where("_id", sib._id)
485
+ .toSql();
486
+ await store.execute(releaseBuilt.sql, releaseBuilt.params);
440
487
  }
441
488
  }
442
489
  }
@@ -452,36 +499,46 @@ function create(config) {
452
499
  // row's current attempts/maxAttempts. CASE expressions split the
453
500
  // status / availableAt / finishedAt updates per branch — same
454
501
  // semantics as the previous SELECT-then-UPDATE-in-transaction
455
- // path, but no cross-dialect transaction primitive needed.
456
- await store.execute(
457
- "UPDATE " + qTable + " SET " +
458
- " status = CASE WHEN attempts < maxAttempts THEN 'pending' ELSE 'failed' END, " +
459
- " lastError = ?, " +
460
- " leaseExpiresAt = NULL, " +
461
- " availableAt = CASE WHEN attempts < maxAttempts THEN ? ELSE availableAt END, " +
462
- " finishedAt = CASE WHEN attempts < maxAttempts THEN NULL ELSE ? END " +
463
- "WHERE _id = ?",
464
- [sealedErr, nowMs + retryDelayMs, nowMs, jobId]
465
- );
502
+ // path, but no cross-dialect transaction primitive needed. Each CASE
503
+ // is a b.sql setRaw value-expression (guarded by b.guardSql) over the
504
+ // row's own columns; the branch values bind as `?` placeholders (the
505
+ // prior 'pending'/'failed' SQL literals now bind, which keeps the raw
506
+ // fragment literal-free).
507
+ var attemptsLt = _qc("attempts") + " < " + _qc("maxAttempts");
508
+ var failBuilt = _update()
509
+ .setRaw("status", "CASE WHEN " + attemptsLt + " THEN ? ELSE ? END", ["pending", "failed"])
510
+ .set("lastError", sealedErr)
511
+ .set("leaseExpiresAt", null)
512
+ .setRaw("availableAt", "CASE WHEN " + attemptsLt + " THEN ? ELSE " + _qc("availableAt") + " END",
513
+ [nowMs + retryDelayMs])
514
+ .setRaw("finishedAt", "CASE WHEN " + attemptsLt + " THEN NULL ELSE ? END", [nowMs])
515
+ .where("_id", jobId)
516
+ .toSql();
517
+ await store.execute(failBuilt.sql, failBuilt.params);
466
518
  return true;
467
519
  }
468
520
 
469
521
  async function sweepExpired() {
470
522
  cluster.requireLeader();
471
- var result = await store.execute(
472
- "UPDATE " + qTable + " SET status = 'pending', leaseExpiresAt = NULL " +
473
- "WHERE status = 'inflight' AND leaseExpiresAt < ?",
474
- [Date.now()]
475
- );
523
+ var built = _update()
524
+ .set("status", "pending")
525
+ .set("leaseExpiresAt", null)
526
+ .where("status", "inflight")
527
+ .whereOp("leaseExpiresAt", "<", Date.now())
528
+ .toSql();
529
+ var result = await store.execute(built.sql, built.params);
476
530
  return result.rowCount || 0;
477
531
  }
478
532
 
479
533
  async function size(queueName) {
480
- var row = await store.executeOne(
481
- "SELECT COUNT(*) AS n FROM " + qTable + " " +
482
- "WHERE queueName = ? AND (status = 'pending' OR status = 'inflight')",
483
- [queueName]
484
- );
534
+ // (status = 'pending' OR status = 'inflight') is an IN-list over the two
535
+ // active states b.sql expands it to (?, ?) bound placeholders.
536
+ var built = _select()
537
+ .count("*", "n")
538
+ .where("queueName", queueName)
539
+ .whereIn("status", ["pending", "inflight"])
540
+ .toSql();
541
+ var row = await store.executeOne(built.sql, built.params);
485
542
  return row ? Number(row.n) : 0;
486
543
  }
487
544
 
@@ -503,14 +560,15 @@ function create(config) {
503
560
  }
504
561
  limit = opts.limit;
505
562
  }
506
- var rows = await store.executeAll(
507
- "SELECT _id, queueName, payload, status, enqueuedAt, finishedAt, " +
508
- " attempts, maxAttempts, lastError, traceId, classification " +
509
- "FROM " + qTable + " " +
510
- "WHERE queueName = ? AND status = 'failed' " +
511
- "ORDER BY finishedAt DESC LIMIT ?",
512
- [queueName, limit]
513
- );
563
+ var built = _select()
564
+ .columns(["_id", "queueName", "payload", "status", "enqueuedAt", "finishedAt",
565
+ "attempts", "maxAttempts", "lastError", "traceId", "classification"])
566
+ .where("queueName", queueName)
567
+ .where("status", "failed")
568
+ .orderBy("finishedAt", "desc")
569
+ .limit(limit)
570
+ .toSql();
571
+ var rows = await store.executeAll(built.sql, built.params);
514
572
  return rows.map(function (row) {
515
573
  var unsealed = cryptoField.unsealRow(SEAL_TABLE, row);
516
574
  return {
@@ -532,36 +590,39 @@ function create(config) {
532
590
  async function dlqRetry(jobId) {
533
591
  cluster.requireLeader();
534
592
  var nowMs = Date.now();
535
- var result = await store.execute(
536
- "UPDATE " + qTable + " SET " +
537
- " status = 'pending', " +
538
- " attempts = 0, " +
539
- " availableAt = ?, " +
540
- " finishedAt = NULL, " +
541
- " leasedAt = NULL, " +
542
- " leaseExpiresAt = NULL, " +
543
- " lastError = NULL " +
544
- "WHERE _id = ? AND status = 'failed'",
545
- [nowMs, jobId]
546
- );
593
+ // NULL resets bind as null params (the prior SQL-literal NULLs); the
594
+ // string-literal statuses bind too.
595
+ var built = _update()
596
+ .set({
597
+ status: "pending",
598
+ attempts: 0,
599
+ availableAt: nowMs,
600
+ finishedAt: null,
601
+ leasedAt: null,
602
+ leaseExpiresAt: null,
603
+ lastError: null,
604
+ })
605
+ .where("_id", jobId)
606
+ .where("status", "failed")
607
+ .toSql();
608
+ var result = await store.execute(built.sql, built.params);
547
609
  return (result.rowCount || 0) > 0;
548
610
  }
549
611
 
550
612
  async function dlqSize(queueName) {
551
- var row = await store.executeOne(
552
- "SELECT COUNT(*) AS n FROM " + qTable + " " +
553
- "WHERE queueName = ? AND status = 'failed'",
554
- [queueName]
555
- );
613
+ var built = _select()
614
+ .count("*", "n")
615
+ .where("queueName", queueName)
616
+ .where("status", "failed")
617
+ .toSql();
618
+ var row = await store.executeOne(built.sql, built.params);
556
619
  return row ? Number(row.n) : 0;
557
620
  }
558
621
 
559
622
  async function purge(queueName) {
560
623
  cluster.requireLeader();
561
- var result = await store.execute(
562
- "DELETE FROM " + qTable + " WHERE queueName = ?",
563
- [queueName]
564
- );
624
+ var built = _delete().where("queueName", queueName).toSql();
625
+ var result = await store.execute(built.sql, built.params);
565
626
  return result.rowCount || 0;
566
627
  }
567
628
 
@@ -575,10 +636,12 @@ function create(config) {
575
636
  // to JSON for the dependsOn column.
576
637
  async function patchFlowDeps(jobId, depIds) {
577
638
  cluster.requireLeader();
578
- var result = await store.execute(
579
- "UPDATE " + qTable + " SET dependsOn = ?, availableAt = ? WHERE _id = ?",
580
- [JSON.stringify(depIds), FLOW_BLOCKED_AVAILABLE_AT, jobId]
581
- );
639
+ var built = _update()
640
+ .set("dependsOn", JSON.stringify(depIds))
641
+ .set("availableAt", FLOW_BLOCKED_AVAILABLE_AT)
642
+ .where("_id", jobId)
643
+ .toSql();
644
+ var result = await store.execute(built.sql, built.params);
582
645
  return (result.rowCount || 0) > 0;
583
646
  }
584
647
 
@@ -310,7 +310,10 @@ function create(opts) {
310
310
  // contract queue-local returns from _shapeLeasedRow.
311
311
  function _shapeLeasedRow(jobId, raw) {
312
312
  if (!raw) return null;
313
- // Pretend it's a "_blamejs_jobs" row so cryptoField unseals correctly.
313
+ // The cryptoField seal-table registry KEY (matches db.js's registerTable
314
+ // literal), not a SQL table name; this adapter holds no SQL (Redis
315
+ // ZSET/HASH ops). Keep it byte-identical so payload + lastError unseal.
316
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
314
317
  var unsealed = cryptoField.unsealRow("_blamejs_jobs", raw);
315
318
  return {
316
319
  jobId: jobId,
@@ -368,6 +371,9 @@ function create(opts) {
368
371
  dependsOn: Array.isArray(opts2.dependsOn) && opts2.dependsOn.length > 0
369
372
  ? JSON.stringify(opts2.dependsOn) : null,
370
373
  };
374
+ // cryptoField seal-table registry KEY (db.js registers payload + lastError
375
+ // under this literal), not a SQL table; this Redis adapter holds no SQL.
376
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
371
377
  var sealed = cryptoField.sealRow("_blamejs_jobs", row);
372
378
 
373
379
  // Pipeline: HSET job + ZADD ready + SADD queues + (if flowId)
@@ -466,6 +472,7 @@ function create(opts) {
466
472
 
467
473
  if (raw.repeatCron) {
468
474
  try {
475
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
469
476
  var unsealed = cryptoField.unsealRow("_blamejs_jobs", raw);
470
477
  var cron = scheduler.parseCron(unsealed.repeatCron);
471
478
  var nextMs = scheduler.nextCronFire(
@@ -689,6 +696,7 @@ function create(opts) {
689
696
  for (var i = 0; i < idStrs.length; i++) {
690
697
  var raw = _decodeHash(hashes[i]);
691
698
  if (!raw) continue;
699
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
692
700
  var unsealed = cryptoField.unsealRow("_blamejs_jobs", raw);
693
701
  out.push({
694
702
  jobId: idStrs[i],
package/lib/queue-sqs.js CHANGED
@@ -175,6 +175,11 @@ function create(opts) {
175
175
  enqueueOpts = enqueueOpts || {};
176
176
  var queueUrl = queueUrlResolver(queueName);
177
177
  var jobId = generateToken(C.BYTES.bytes(16));
178
+ // The cryptoField seal-table registry KEY (matches db.js's registerTable
179
+ // literal), not a SQL table name; this SQS adapter holds no SQL
180
+ // (AWSJsonProtocol over HTTPS). Keep it byte-identical so the sealed
181
+ // message body unseals under the same schema on receive.
182
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
178
183
  var sealed = cryptoField.sealRow("_blamejs_jobs", {
179
184
  _id: jobId,
180
185
  queueName: queueName,
@@ -222,6 +227,7 @@ function create(opts) {
222
227
  var sealed;
223
228
  try { sealed = safeJson.parse(m.Body); }
224
229
  catch (_e) { continue; }
230
+ // allow:hand-rolled-sql — cryptoField seal-table registry KEY, not SQL.
225
231
  var unsealed = cryptoField.unsealRow("_blamejs_jobs", sealed);
226
232
  var payload;
227
233
  try {