@blamejs/core 0.17.23 → 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.
- package/CHANGELOG.md +11 -5
- package/NOTICE +12 -13
- package/README.md +3 -3
- package/lib/audit.js +69 -1
- package/lib/auth/oauth.js +57 -0
- package/lib/cli.js +15 -5
- package/lib/daemon.js +192 -2
- package/lib/guard-tenant-id.js +12 -1
- package/lib/mtls-ca.js +173 -18
- package/lib/mtls-engine-default.js +339 -314
- package/lib/outbox.js +43 -13
- package/lib/redact.js +39 -6
- package/lib/safe-json.js +9 -1
- package/lib/self-update.js +215 -24
- package/lib/vendor/MANIFEST.json +27 -30
- package/lib/vendor/blamejs-pki.cjs +28419 -0
- package/lib/webhook-dispatcher.js +55 -24
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/vendor/pki.cjs +0 -39718
|
@@ -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
|
-
|
|
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
|
-
],
|
|
314
|
+
], _tableOpts(dialect)), dialect);
|
|
290
315
|
var endpointsIdx = sql.toExternalSql(sql.createIndex(endpointsTable + "_eid_idx",
|
|
291
|
-
endpointsTable, ["endpoint_id"],
|
|
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
|
-
],
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:cd7168a2-46e6-4f4b-a75c-259ff3ea2998",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.18.0",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.18.0",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|