@blamejs/core 0.17.24 → 0.18.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.
@@ -76,6 +76,27 @@ function _validateTableName(name, label) {
76
76
  return name;
77
77
  }
78
78
 
79
+ // The dispatcher's endpoints/deliveries tables are operator-supplied names
80
+ // executed against a concrete externalDb handle (never b.clusterStorage —
81
+ // nothing here rewrites bare table names), so every b.sql builder quotes the
82
+ // table name by construction (quoteName: true). A quoted identifier is what
83
+ // makes a reserved-word or case-sensitive operator table (a table literally
84
+ // named "from") emit valid SQL — parity with b.db.from()'s allowReserved. Same
85
+ // posture as b.mailStore against its concrete sqlite handle.
86
+ function _tableOpts(dialect) { return { dialect: dialect, quoteName: true }; }
87
+
88
+ // PostgreSQL folds UNQUOTED identifiers to lowercase, so every pre-0.18 deployment
89
+ // (whose DDL and queries emitted the table name unquoted) already has a
90
+ // lowercase-folded table. Now that the name is always quoted (quoteName above), a
91
+ // mixed-case custom endpoints/deliveries table would target a NEW case-sensitive
92
+ // table and strand the existing rows. Fold to lowercase on postgres BEFORE quoting
93
+ // so a legacy config keeps resolving to its existing table; reserved words (already
94
+ // lowercase) still quote correctly. sqlite is case-insensitive (no fold); MySQL
95
+ // folding is server-configured and applied equally to the pre-quote name.
96
+ function _foldTableForDialect(name, dialect) {
97
+ return dialect === "postgres" ? String(name).toLowerCase() : name;
98
+ }
99
+
79
100
  function _sqlDialect(externalDb) {
80
101
  var d = externalDb && externalDb.dialect;
81
102
  if (d === "postgres" || d === "postgresql") return "postgres";
@@ -190,12 +211,16 @@ function dispatcher(opts) {
190
211
  dnsLookup: "optional-function",
191
212
  }, "webhook.dispatcher", WebhookDispatcherError, "webhook-dispatcher/bad-opts");
192
213
  var externalDb = opts.externalDb;
193
- var endpointsTable = _validateTableName(
214
+ // Fold operator table names for the backend's identifier rules BEFORE they are
215
+ // quoted downstream, so a legacy mixed-case config still targets its existing
216
+ // (folded) table on postgres.
217
+ var _createDialect = _sqlDialect(externalDb);
218
+ var endpointsTable = _foldTableForDialect(_validateTableName(
194
219
  opts.endpointsTable || frameworkSchema.tableName("webhook_endpoints"),
195
- "dispatcher: endpointsTable");
196
- var deliveriesTable = _validateTableName(
220
+ "dispatcher: endpointsTable"), _createDialect);
221
+ var deliveriesTable = _foldTableForDialect(_validateTableName(
197
222
  opts.deliveriesTable || frameworkSchema.tableName("webhook_deliveries"),
198
- "dispatcher: deliveriesTable");
223
+ "dispatcher: deliveriesTable"), _createDialect);
199
224
 
200
225
  var maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
201
226
  var batchSize = opts.batchSize || DEFAULT_BATCH_SIZE;
@@ -286,9 +311,9 @@ function dispatcher(opts) {
286
311
  { name: "secret_sealed", type: "TEXT", notNull: true },
287
312
  { name: "disabled", type: "INTEGER", notNull: true, default: 0 },
288
313
  { name: "created_at", type: tsType, notNull: true },
289
- ], { dialect: dialect }), dialect);
314
+ ], _tableOpts(dialect)), dialect);
290
315
  var endpointsIdx = sql.toExternalSql(sql.createIndex(endpointsTable + "_eid_idx",
291
- endpointsTable, ["endpoint_id"], { dialect: dialect }), dialect);
316
+ endpointsTable, ["endpoint_id"], _tableOpts(dialect)), dialect);
292
317
 
293
318
  var deliveriesDdl = sql.toExternalSql(sql.createTable(deliveriesTable, [
294
319
  { name: "id", serial: true },
@@ -306,14 +331,14 @@ function dispatcher(opts) {
306
331
  { name: "response_status", type: "INTEGER" },
307
332
  { name: "last_error", type: "TEXT" },
308
333
  { name: "created_at", type: tsType, notNull: true },
309
- ], { dialect: dialect }), dialect);
334
+ ], _tableOpts(dialect)), dialect);
310
335
  // Index on the due-pending pool the retry poller scans. Postgres/SQLite
311
336
  // get a PARTIAL index (status = 'pending'); MySQL has no partial indexes
312
337
  // (sql.createIndex refuses `where` for the mysql dialect), so it gets a
313
338
  // plain index on next_attempt_at — the processRetries query still filters
314
339
  // status = 'pending', so correctness is unchanged, only the index is a
315
340
  // touch less selective.
316
- var deliveriesIdxOpts = { dialect: dialect };
341
+ var deliveriesIdxOpts = _tableOpts(dialect);
317
342
  if (dialect !== "mysql") deliveriesIdxOpts.where = "status = 'pending'";
318
343
  var deliveriesIdx = sql.toExternalSql(sql.createIndex(deliveriesTable + "_pending_idx",
319
344
  deliveriesTable, ["next_attempt_at"], deliveriesIdxOpts), dialect);
@@ -342,7 +367,7 @@ function dispatcher(opts) {
342
367
 
343
368
  var dialect = _sqlDialect(externalDb);
344
369
  var sealedSecret = vault().seal(ep.secret);
345
- var stmt = sql.insert(endpointsTable, { dialect: dialect })
370
+ var stmt = sql.insert(endpointsTable, _tableOpts(dialect))
346
371
  .values({
347
372
  endpoint_id: ep.endpointId,
348
373
  url: ep.url,
@@ -362,7 +387,7 @@ function dispatcher(opts) {
362
387
  validateOpts.requireNonEmptyString(endpointId, "removeEndpoint: endpointId",
363
388
  WebhookDispatcherError, "webhook-dispatcher/bad-opts");
364
389
  var dialect = _sqlDialect(externalDb);
365
- var stmt = sql.delete(endpointsTable, { dialect: dialect })
390
+ var stmt = sql.delete(endpointsTable, _tableOpts(dialect))
366
391
  .where("endpoint_id", endpointId)
367
392
  .toExternalSql(dialect);
368
393
  await externalDb.query(stmt.sql, stmt.params);
@@ -371,7 +396,7 @@ function dispatcher(opts) {
371
396
 
372
397
  async function listEndpoints() {
373
398
  var dialect = _sqlDialect(externalDb);
374
- var stmt = sql.select(endpointsTable, { dialect: dialect })
399
+ var stmt = sql.select(endpointsTable, _tableOpts(dialect))
375
400
  .columns(["endpoint_id", "url", "event_types", "disabled", "created_at"])
376
401
  .toExternalSql(dialect);
377
402
  var res = await externalDb.query(stmt.sql, stmt.params);
@@ -402,7 +427,7 @@ function dispatcher(opts) {
402
427
 
403
428
  async function _loadEndpointRow(endpointId) {
404
429
  var dialect = _sqlDialect(externalDb);
405
- var stmt = sql.select(endpointsTable, { dialect: dialect })
430
+ var stmt = sql.select(endpointsTable, _tableOpts(dialect))
406
431
  .columns(["endpoint_id", "url", "secret_sealed", "disabled"])
407
432
  .where("endpoint_id", endpointId)
408
433
  .limit(1)
@@ -432,7 +457,7 @@ function dispatcher(opts) {
432
457
  // and double-deliver it. The inline _attemptDelivery transitions it to
433
458
  // delivered / pending(+backoff) / dead; if this process dies mid-POST,
434
459
  // _reapStaleInflight reclaims it after claimReclaimMs.
435
- var insertStmt = sql.insert(deliveriesTable, { dialect: dialect })
460
+ var insertStmt = sql.insert(deliveriesTable, _tableOpts(dialect))
436
461
  .values({
437
462
  delivery_id: deliveryId,
438
463
  endpoint_id: ep.endpointId,
@@ -461,7 +486,7 @@ function dispatcher(opts) {
461
486
 
462
487
  async function _loadDelivery(deliveryId) {
463
488
  var dialect = _sqlDialect(externalDb);
464
- var stmt = sql.select(deliveriesTable, { dialect: dialect })
489
+ var stmt = sql.select(deliveriesTable, _tableOpts(dialect))
465
490
  .columns(["delivery_id", "endpoint_id", "url", "event_type", "payload",
466
491
  "idempotency_id", "status", "attempts"])
467
492
  .where("delivery_id", deliveryId)
@@ -536,7 +561,7 @@ function dispatcher(opts) {
536
561
 
537
562
  async function _markDelivered(deliveryId, attemptNo, status) {
538
563
  var dialect = _sqlDialect(externalDb);
539
- var stmt = sql.update(deliveriesTable, { dialect: dialect })
564
+ var stmt = sql.update(deliveriesTable, _tableOpts(dialect))
540
565
  .set({
541
566
  status: "delivered",
542
567
  attempts: attemptNo,
@@ -558,7 +583,7 @@ function dispatcher(opts) {
558
583
  }
559
584
  var dialect = _sqlDialect(externalDb);
560
585
  var nextAt = new Date(clock() + _backoffMs(attemptNo));
561
- var stmt = sql.update(deliveriesTable, { dialect: dialect })
586
+ var stmt = sql.update(deliveriesTable, _tableOpts(dialect))
562
587
  .set({
563
588
  status: "pending",
564
589
  attempts: attemptNo,
@@ -574,7 +599,7 @@ function dispatcher(opts) {
574
599
 
575
600
  async function _markDead(deliveryId, attemptNo, errMsg) {
576
601
  var dialect = _sqlDialect(externalDb);
577
- var stmt = sql.update(deliveriesTable, { dialect: dialect })
602
+ var stmt = sql.update(deliveriesTable, _tableOpts(dialect))
578
603
  .set({ status: "dead", attempts: attemptNo, last_error: errMsg, claimed_at: null })
579
604
  .where("delivery_id", deliveryId)
580
605
  .toExternalSql(dialect);
@@ -590,7 +615,7 @@ function dispatcher(opts) {
590
615
  async function _reapStaleInflight() {
591
616
  var dialect = _sqlDialect(externalDb);
592
617
  var cutoff = new Date(clock() - claimReclaimMs);
593
- var stmt = sql.update(deliveriesTable, { dialect: dialect })
618
+ var stmt = sql.update(deliveriesTable, _tableOpts(dialect))
594
619
  .set({ status: "pending", claimed_at: null })
595
620
  .whereRaw("status = 'in-flight'", [], { allowLiterals: true })
596
621
  .whereRaw("(claimed_at IS NULL OR claimed_at <= ?)", [cutoff])
@@ -625,7 +650,7 @@ function dispatcher(opts) {
625
650
  var supportsSkipLocked = _supportsForUpdateSkipLocked();
626
651
  var claimed = await externalDb.transaction(async function (xdb) {
627
652
  var nowDate = _nowDate();
628
- var selBuilder = sql.select(deliveriesTable, { dialect: dialect })
653
+ var selBuilder = sql.select(deliveriesTable, _tableOpts(dialect))
629
654
  .columns(["delivery_id"])
630
655
  .whereRaw("status = 'pending'", [], { allowLiterals: true })
631
656
  .whereRaw("next_attempt_at <= ?", [nowDate])
@@ -636,7 +661,7 @@ function dispatcher(opts) {
636
661
  var rows = await xdb.query(sel.sql, sel.params);
637
662
  var ids = ((rows && rows.rows) || []).map(function (r) { return r.delivery_id; });
638
663
  if (ids.length === 0) return [];
639
- var mark = sql.update(deliveriesTable, { dialect: dialect })
664
+ var mark = sql.update(deliveriesTable, _tableOpts(dialect))
640
665
  .set({ status: "in-flight", claimed_at: _nowDate() })
641
666
  .whereRaw("status = 'pending'", [], { allowLiterals: true })
642
667
  .whereInArray("delivery_id", ids)
@@ -648,7 +673,7 @@ function dispatcher(opts) {
648
673
  // sqlite / other: no row lock, so re-read which rows WE flipped. The
649
674
  // single writer serializes the gated UPDATE, so the in-flight rows in our
650
675
  // id set are ours.
651
- var after = sql.select(deliveriesTable, { dialect: dialect })
676
+ var after = sql.select(deliveriesTable, _tableOpts(dialect))
652
677
  .columns(["delivery_id"])
653
678
  .whereRaw("status = 'in-flight'", [], { allowLiterals: true })
654
679
  .whereInArray("delivery_id", ids)
@@ -688,7 +713,7 @@ function dispatcher(opts) {
688
713
  async function _listDeliveries(filter) {
689
714
  filter = filter || {};
690
715
  var dialect = _sqlDialect(externalDb);
691
- var builder = sql.select(deliveriesTable, { dialect: dialect }).columns(DELIVERY_VIEW_COLS);
716
+ var builder = sql.select(deliveriesTable, _tableOpts(dialect)).columns(DELIVERY_VIEW_COLS);
692
717
  if (filter.endpointId) builder.where("endpoint_id", filter.endpointId);
693
718
  if (filter.status) builder.where("status", filter.status);
694
719
  builder.orderBy("id").limit(filter.limit || DEFAULT_BATCH_SIZE);
@@ -699,7 +724,7 @@ function dispatcher(opts) {
699
724
 
700
725
  async function _getDelivery(deliveryId) {
701
726
  var dialect = _sqlDialect(externalDb);
702
- var stmt = sql.select(deliveriesTable, { dialect: dialect })
727
+ var stmt = sql.select(deliveriesTable, _tableOpts(dialect))
703
728
  .columns(DELIVERY_VIEW_COLS)
704
729
  .where("delivery_id", deliveryId)
705
730
  .limit(1)
@@ -715,7 +740,7 @@ function dispatcher(opts) {
715
740
  validateOpts.requireNonEmptyString(deliveryId, "retry: deliveryId",
716
741
  WebhookDispatcherError, "webhook-dispatcher/bad-opts");
717
742
  var dialect = _sqlDialect(externalDb);
718
- var stmt = sql.update(deliveriesTable, { dialect: dialect })
743
+ var stmt = sql.update(deliveriesTable, _tableOpts(dialect))
719
744
  .set({ status: "pending", attempts: 0, next_attempt_at: _nowDate(), claimed_at: null, last_error: null })
720
745
  .where("delivery_id", deliveryId)
721
746
  .toExternalSql(dialect);
@@ -724,12 +749,18 @@ function dispatcher(opts) {
724
749
  }
725
750
 
726
751
  function _emitAudit(action, outcome, metadata) {
752
+ // Drop-silent hot-path sinks: metadata is always an object at both call
753
+ // sites (so the `|| {}` guards never fire) and safeEmit / safeEvent never
754
+ // throw for the dispatcher's fixed string actions, so the catch arms are
755
+ // belt-and-suspenders the public API can't reach.
756
+ /* c8 ignore start */
727
757
  try {
728
758
  audit().safeEmit({ action: action, outcome: outcome, metadata: metadata || {} });
729
759
  } catch (_e) { /* audit is a drop-silent hot-path sink — never crash the delivery */ }
730
760
  try {
731
761
  observability().safeEvent(action, 1, metadata || {});
732
762
  } catch (_e) { /* drop-silent */ }
763
+ /* c8 ignore stop */
733
764
  }
734
765
 
735
766
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.17.24",
3
+ "version": "0.18.0",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:c6974353-05d2-497c-8a3d-77ee80a116f1",
5
+ "serialNumber": "urn:uuid:cd7168a2-46e6-4f4b-a75c-259ff3ea2998",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-25T23:40:58.349Z",
8
+ "timestamp": "2026-07-27T07:19:29.355Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.17.24",
22
+ "bom-ref": "@blamejs/core@0.18.0",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.17.24",
25
+ "version": "0.18.0",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.17.24",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.0",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.17.24",
57
+ "ref": "@blamejs/core@0.18.0",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]