@xtandard/webhooks 0.1.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/LICENSE +21 -0
- package/README.md +315 -0
- package/bin/xtandard-webhooks.mjs +3 -0
- package/dist/basic-BIW3Rvuz.cjs +199 -0
- package/dist/basic-BIW3Rvuz.cjs.map +1 -0
- package/dist/basic-DKk0Xfuu.mjs +176 -0
- package/dist/basic-DKk0Xfuu.mjs.map +1 -0
- package/dist/chunk-D7D4PA-g.mjs +13 -0
- package/dist/cli.cjs +655 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +42 -0
- package/dist/cli.d.mts +42 -0
- package/dist/cli.mjs +653 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/contract-8h-Azxa5.d.cts +71 -0
- package/dist/contract-9XpcwcCn.mjs +22 -0
- package/dist/contract-9XpcwcCn.mjs.map +1 -0
- package/dist/contract-B2d5dNU3.cjs +33 -0
- package/dist/contract-B2d5dNU3.cjs.map +1 -0
- package/dist/contract-BEhDcd_5.mjs +28 -0
- package/dist/contract-BEhDcd_5.mjs.map +1 -0
- package/dist/contract-Bf1qguwt.cjs +57 -0
- package/dist/contract-Bf1qguwt.cjs.map +1 -0
- package/dist/contract-Bnb3fgRJ.d.cts +177 -0
- package/dist/contract-C2r2Xzwp.d.mts +46 -0
- package/dist/contract-CiPskNvS.d.cts +46 -0
- package/dist/contract-DhQ4JjGG.d.mts +71 -0
- package/dist/contract-T1kcZNdG.d.mts +177 -0
- package/dist/contract-lETlIuXo.d.cts +30 -0
- package/dist/contract-lETlIuXo.d.mts +30 -0
- package/dist/core-CMpnmI5Q.mjs +1605 -0
- package/dist/core-CMpnmI5Q.mjs.map +1 -0
- package/dist/core-DT4ppWh8.d.mts +502 -0
- package/dist/core-KJawHjFF.d.cts +502 -0
- package/dist/core-ZGhH6Vs2.cjs +1790 -0
- package/dist/core-ZGhH6Vs2.cjs.map +1 -0
- package/dist/core.cjs +8 -0
- package/dist/core.d.cts +2 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.mjs +2 -0
- package/dist/create-fetch-handler-BIdk9P30.mjs +1724 -0
- package/dist/create-fetch-handler-BIdk9P30.mjs.map +1 -0
- package/dist/create-fetch-handler-CmooujQo.cjs +1771 -0
- package/dist/create-fetch-handler-CmooujQo.cjs.map +1 -0
- package/dist/create-fetch-handler-Dlkhustu.d.cts +162 -0
- package/dist/create-fetch-handler-jy3hy5nZ.d.mts +162 -0
- package/dist/dispatcher-B0xTEHt1.cjs +212 -0
- package/dist/dispatcher-B0xTEHt1.cjs.map +1 -0
- package/dist/dispatcher-Coubwrka.mjs +196 -0
- package/dist/dispatcher-Coubwrka.mjs.map +1 -0
- package/dist/entry-auth-basic.cjs +5 -0
- package/dist/entry-auth-basic.d.cts +83 -0
- package/dist/entry-auth-basic.d.mts +83 -0
- package/dist/entry-auth-basic.mjs +2 -0
- package/dist/entry-auth-delegated.cjs +28 -0
- package/dist/entry-auth-delegated.cjs.map +1 -0
- package/dist/entry-auth-delegated.d.cts +36 -0
- package/dist/entry-auth-delegated.d.mts +36 -0
- package/dist/entry-auth-delegated.mjs +27 -0
- package/dist/entry-auth-delegated.mjs.map +1 -0
- package/dist/entry-auth-none.cjs +4 -0
- package/dist/entry-auth-none.d.cts +25 -0
- package/dist/entry-auth-none.d.mts +25 -0
- package/dist/entry-auth-none.mjs +2 -0
- package/dist/entry-authorization-delegated.cjs +27 -0
- package/dist/entry-authorization-delegated.cjs.map +1 -0
- package/dist/entry-authorization-delegated.d.cts +31 -0
- package/dist/entry-authorization-delegated.d.mts +31 -0
- package/dist/entry-authorization-delegated.mjs +26 -0
- package/dist/entry-authorization-delegated.mjs.map +1 -0
- package/dist/entry-authorization-none.cjs +3 -0
- package/dist/entry-authorization-none.d.cts +18 -0
- package/dist/entry-authorization-none.d.mts +18 -0
- package/dist/entry-authorization-none.mjs +2 -0
- package/dist/entry-authorization-roles.cjs +6 -0
- package/dist/entry-authorization-roles.d.cts +65 -0
- package/dist/entry-authorization-roles.d.mts +65 -0
- package/dist/entry-authorization-roles.mjs +2 -0
- package/dist/entry-bun.cjs +24 -0
- package/dist/entry-bun.cjs.map +1 -0
- package/dist/entry-bun.d.cts +8 -0
- package/dist/entry-bun.d.mts +8 -0
- package/dist/entry-bun.mjs +23 -0
- package/dist/entry-bun.mjs.map +1 -0
- package/dist/entry-drizzle-mysql.cjs +20 -0
- package/dist/entry-drizzle-mysql.cjs.map +1 -0
- package/dist/entry-drizzle-mysql.d.cts +27 -0
- package/dist/entry-drizzle-mysql.d.mts +27 -0
- package/dist/entry-drizzle-mysql.mjs +19 -0
- package/dist/entry-drizzle-mysql.mjs.map +1 -0
- package/dist/entry-drizzle-pg.cjs +21 -0
- package/dist/entry-drizzle-pg.cjs.map +1 -0
- package/dist/entry-drizzle-pg.d.cts +26 -0
- package/dist/entry-drizzle-pg.d.mts +26 -0
- package/dist/entry-drizzle-pg.mjs +20 -0
- package/dist/entry-drizzle-pg.mjs.map +1 -0
- package/dist/entry-drizzle-sqlite.cjs +21 -0
- package/dist/entry-drizzle-sqlite.cjs.map +1 -0
- package/dist/entry-drizzle-sqlite.d.cts +23 -0
- package/dist/entry-drizzle-sqlite.d.mts +23 -0
- package/dist/entry-drizzle-sqlite.mjs +20 -0
- package/dist/entry-drizzle-sqlite.mjs.map +1 -0
- package/dist/entry-elysia.cjs +125 -0
- package/dist/entry-elysia.cjs.map +1 -0
- package/dist/entry-elysia.d.cts +1017 -0
- package/dist/entry-elysia.d.mts +1017 -0
- package/dist/entry-elysia.mjs +123 -0
- package/dist/entry-elysia.mjs.map +1 -0
- package/dist/entry-express.cjs +57 -0
- package/dist/entry-express.cjs.map +1 -0
- package/dist/entry-express.d.cts +15 -0
- package/dist/entry-express.d.mts +15 -0
- package/dist/entry-express.mjs +56 -0
- package/dist/entry-express.mjs.map +1 -0
- package/dist/entry-hono.cjs +35 -0
- package/dist/entry-hono.cjs.map +1 -0
- package/dist/entry-hono.d.cts +16 -0
- package/dist/entry-hono.d.mts +16 -0
- package/dist/entry-hono.mjs +34 -0
- package/dist/entry-hono.mjs.map +1 -0
- package/dist/entry-hooks-log.cjs +22 -0
- package/dist/entry-hooks-log.cjs.map +1 -0
- package/dist/entry-hooks-log.d.cts +23 -0
- package/dist/entry-hooks-log.d.mts +23 -0
- package/dist/entry-hooks-log.mjs +21 -0
- package/dist/entry-hooks-log.mjs.map +1 -0
- package/dist/entry-storage-cloudflare-kv.cjs +47 -0
- package/dist/entry-storage-cloudflare-kv.cjs.map +1 -0
- package/dist/entry-storage-cloudflare-kv.d.cts +42 -0
- package/dist/entry-storage-cloudflare-kv.d.mts +42 -0
- package/dist/entry-storage-cloudflare-kv.mjs +46 -0
- package/dist/entry-storage-cloudflare-kv.mjs.map +1 -0
- package/dist/entry-storage-drizzle.cjs +78 -0
- package/dist/entry-storage-drizzle.cjs.map +1 -0
- package/dist/entry-storage-drizzle.d.cts +30 -0
- package/dist/entry-storage-drizzle.d.mts +30 -0
- package/dist/entry-storage-drizzle.mjs +77 -0
- package/dist/entry-storage-drizzle.mjs.map +1 -0
- package/dist/entry-storage-file.cjs +4 -0
- package/dist/entry-storage-file.d.cts +30 -0
- package/dist/entry-storage-file.d.mts +30 -0
- package/dist/entry-storage-file.mjs +2 -0
- package/dist/entry-storage-libsql.cjs +3 -0
- package/dist/entry-storage-libsql.d.cts +48 -0
- package/dist/entry-storage-libsql.d.mts +48 -0
- package/dist/entry-storage-libsql.mjs +2 -0
- package/dist/entry-storage-memory.cjs +3 -0
- package/dist/entry-storage-memory.d.cts +2 -0
- package/dist/entry-storage-memory.d.mts +2 -0
- package/dist/entry-storage-memory.mjs +2 -0
- package/dist/entry-storage-mongodb.cjs +3 -0
- package/dist/entry-storage-mongodb.d.cts +55 -0
- package/dist/entry-storage-mongodb.d.mts +55 -0
- package/dist/entry-storage-mongodb.mjs +2 -0
- package/dist/entry-storage-postgres.cjs +3 -0
- package/dist/entry-storage-postgres.d.cts +62 -0
- package/dist/entry-storage-postgres.d.mts +62 -0
- package/dist/entry-storage-postgres.mjs +2 -0
- package/dist/entry-storage-redis.cjs +4 -0
- package/dist/entry-storage-redis.d.cts +77 -0
- package/dist/entry-storage-redis.d.mts +77 -0
- package/dist/entry-storage-redis.mjs +2 -0
- package/dist/entry-storage-sqlite.cjs +3 -0
- package/dist/entry-storage-sqlite.d.cts +36 -0
- package/dist/entry-storage-sqlite.d.mts +36 -0
- package/dist/entry-storage-sqlite.mjs +2 -0
- package/dist/entry-storage-unstorage.cjs +42 -0
- package/dist/entry-storage-unstorage.cjs.map +1 -0
- package/dist/entry-storage-unstorage.d.cts +29 -0
- package/dist/entry-storage-unstorage.d.mts +29 -0
- package/dist/entry-storage-unstorage.mjs +41 -0
- package/dist/entry-storage-unstorage.mjs.map +1 -0
- package/dist/file-COBYZA4Q.cjs +148 -0
- package/dist/file-COBYZA4Q.cjs.map +1 -0
- package/dist/file-fi02eFHk.mjs +131 -0
- package/dist/file-fi02eFHk.mjs.map +1 -0
- package/dist/index.cjs +123 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +368 -0
- package/dist/index.d.mts +366 -0
- package/dist/index.mjs +61 -0
- package/dist/index.mjs.map +1 -0
- package/dist/keys-Byyj4quQ.mjs +111 -0
- package/dist/keys-Byyj4quQ.mjs.map +1 -0
- package/dist/keys-FiKpaVHX.cjs +302 -0
- package/dist/keys-FiKpaVHX.cjs.map +1 -0
- package/dist/libsql-bpVi0bXN.mjs +113 -0
- package/dist/libsql-bpVi0bXN.mjs.map +1 -0
- package/dist/libsql-pPJEo1e4.cjs +124 -0
- package/dist/libsql-pPJEo1e4.cjs.map +1 -0
- package/dist/memory-8Ef-PL5a.cjs +137 -0
- package/dist/memory-8Ef-PL5a.cjs.map +1 -0
- package/dist/memory-BMsSSwqn.mjs +127 -0
- package/dist/memory-BMsSSwqn.mjs.map +1 -0
- package/dist/memory-FnMJWCmB.d.cts +28 -0
- package/dist/memory-qIvANEs_.d.mts +28 -0
- package/dist/mongodb-Cy8yo0uk.cjs +108 -0
- package/dist/mongodb-Cy8yo0uk.cjs.map +1 -0
- package/dist/mongodb-Ddaq9mml.mjs +97 -0
- package/dist/mongodb-Ddaq9mml.mjs.map +1 -0
- package/dist/none-BnZtaGNJ.mjs +23 -0
- package/dist/none-BnZtaGNJ.mjs.map +1 -0
- package/dist/none-CAsxCOWN.cjs +49 -0
- package/dist/none-CAsxCOWN.cjs.map +1 -0
- package/dist/none-CZVrfnmF.cjs +33 -0
- package/dist/none-CZVrfnmF.cjs.map +1 -0
- package/dist/none-GhVIoh_s.mjs +33 -0
- package/dist/none-GhVIoh_s.mjs.map +1 -0
- package/dist/postgres-C8WbchFa.cjs +134 -0
- package/dist/postgres-C8WbchFa.cjs.map +1 -0
- package/dist/postgres-c3pAhmhr.mjs +123 -0
- package/dist/postgres-c3pAhmhr.mjs.map +1 -0
- package/dist/react.css +1 -0
- package/dist/react.js +31465 -0
- package/dist/receiver.cjs +43 -0
- package/dist/receiver.cjs.map +1 -0
- package/dist/receiver.d.cts +36 -0
- package/dist/receiver.d.mts +36 -0
- package/dist/receiver.mjs +40 -0
- package/dist/receiver.mjs.map +1 -0
- package/dist/redis-CFJkuSgB.cjs +270 -0
- package/dist/redis-CFJkuSgB.cjs.map +1 -0
- package/dist/redis-CvLi0KF7.mjs +254 -0
- package/dist/redis-CvLi0KF7.mjs.map +1 -0
- package/dist/roles-D0G9XqBq.cjs +128 -0
- package/dist/roles-D0G9XqBq.cjs.map +1 -0
- package/dist/roles-vp361lTk.mjs +99 -0
- package/dist/roles-vp361lTk.mjs.map +1 -0
- package/dist/schema-mo__wv4P.d.cts +233 -0
- package/dist/schema-mo__wv4P.d.mts +233 -0
- package/dist/schema.cjs +13 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +2 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +11 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/signing.cjs +162 -0
- package/dist/signing.cjs.map +1 -0
- package/dist/signing.d.cts +73 -0
- package/dist/signing.d.mts +73 -0
- package/dist/signing.mjs +156 -0
- package/dist/signing.mjs.map +1 -0
- package/dist/sqlite-Cmqnrjes.mjs +67 -0
- package/dist/sqlite-Cmqnrjes.mjs.map +1 -0
- package/dist/sqlite-Dcufk0x3.cjs +78 -0
- package/dist/sqlite-Dcufk0x3.cjs.map +1 -0
- package/dist/table-Ce3Tzwqs.d.cts +11 -0
- package/dist/table-Ce3Tzwqs.d.mts +11 -0
- package/dist/testing.cjs +134 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +80 -0
- package/dist/testing.d.mts +80 -0
- package/dist/testing.mjs +131 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-react/react.d.ts +98 -0
- package/dist/types-react/schema.d.ts +229 -0
- package/dist/types-react/ui/App.d.ts +22 -0
- package/dist/types-react/ui/api.d.ts +97 -0
- package/dist/types-react/ui/components/JsonCodeEditor.d.ts +12 -0
- package/dist/types-react/ui/components/ThemeToggle.d.ts +2 -0
- package/dist/types-react/ui/components/Toast.d.ts +16 -0
- package/dist/types-react/ui/components/primitives.d.ts +50 -0
- package/dist/types-react/ui/components/ui-bits.d.ts +22 -0
- package/dist/types-react/ui/components/webhook-bits.d.ts +51 -0
- package/dist/types-react/ui/lib/format.d.ts +39 -0
- package/dist/types-react/ui/lib/nav-guard.d.ts +20 -0
- package/dist/types-react/ui/lib/utils.d.ts +3 -0
- package/dist/types-react/ui/theme.d.ts +12 -0
- package/dist/types-react/ui/types.d.ts +80 -0
- package/dist/types-react/ui/views/AuditView.d.ts +6 -0
- package/dist/types-react/ui/views/DeliveriesView.d.ts +12 -0
- package/dist/types-react/ui/views/EndpointsView.d.ts +11 -0
- package/dist/types-react/ui/views/EventTypesView.d.ts +11 -0
- package/dist/types-react/ui/views/MessagesView.d.ts +10 -0
- package/dist/types-react/ui/views/OverviewView.d.ts +12 -0
- package/dist/ui/assets/index-B0eoQX2U.css +1 -0
- package/dist/ui/assets/index-S5t_CLOe.js +209 -0
- package/dist/ui/index.html +14 -0
- package/package.json +487 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const require_keys = require("./keys-FiKpaVHX.cjs");
|
|
2
|
+
const require_core = require("./core-ZGhH6Vs2.cjs");
|
|
3
|
+
//#region src/dispatcher.ts
|
|
4
|
+
/**
|
|
5
|
+
* The delivery engine. Polls the due index, claims deliveries (lease-based,
|
|
6
|
+
* multi-instance safe when storage supports CAS or the `deliveryQueue`
|
|
7
|
+
* capability), performs the signed HTTP attempts, and drives each delivery's
|
|
8
|
+
* state machine through the retry schedule into success or dead-letter.
|
|
9
|
+
*
|
|
10
|
+
* Runs in-process: the panel starts one by default, a split worker runs one
|
|
11
|
+
* via the CLI (`xtandard-webhooks dispatch`), and tests drive {@link Dispatcher.tick}
|
|
12
|
+
* manually. All timers are `unref()`ed so a dispatcher never keeps a process
|
|
13
|
+
* alive on its own. Semantics are **at-least-once**: a crashed process loses
|
|
14
|
+
* nothing (leases expire, the next tick reclaims); receivers dedupe on
|
|
15
|
+
* `webhook-id`.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
var dispatcher_exports = /* @__PURE__ */ require_keys.__exportAll({
|
|
20
|
+
DEFAULT_RETRY_SCHEDULE: () => DEFAULT_RETRY_SCHEDULE,
|
|
21
|
+
createDispatcher: () => createDispatcher
|
|
22
|
+
});
|
|
23
|
+
/** The default retry schedule (Svix-compatible). */
|
|
24
|
+
const DEFAULT_RETRY_SCHEDULE = [
|
|
25
|
+
"0s",
|
|
26
|
+
"5s",
|
|
27
|
+
"5m",
|
|
28
|
+
"30m",
|
|
29
|
+
"2h",
|
|
30
|
+
"5h",
|
|
31
|
+
"10h"
|
|
32
|
+
];
|
|
33
|
+
/** Fractional jitter applied to every retry delay (±10%). */
|
|
34
|
+
const JITTER = .1;
|
|
35
|
+
/**
|
|
36
|
+
* Create a dispatcher over a core. Not started — call
|
|
37
|
+
* {@link Dispatcher.start}, or drive {@link Dispatcher.tick} manually.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { createWebhooksCore, createDispatcher } from "@xtandard/webhooks";
|
|
42
|
+
* import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";
|
|
43
|
+
*
|
|
44
|
+
* const core = createWebhooksCore({ storage: createMemoryStorage() });
|
|
45
|
+
* const dispatcher = createDispatcher(core);
|
|
46
|
+
* dispatcher.start();
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function createDispatcher(core, options = {}) {
|
|
50
|
+
const merged = {
|
|
51
|
+
...core.options.dispatcher,
|
|
52
|
+
...options
|
|
53
|
+
};
|
|
54
|
+
const pollIntervalMs = merged.pollIntervalMs ?? 1e3;
|
|
55
|
+
const batchSize = merged.batchSize ?? 20;
|
|
56
|
+
const concurrency = merged.concurrency ?? 8;
|
|
57
|
+
const timeoutMs = merged.timeoutMs ?? 2e4;
|
|
58
|
+
const configuredLeaseMs = merged.leaseMs ?? 6e4;
|
|
59
|
+
const leaseMs = Math.max(configuredLeaseMs, timeoutMs + 1e4);
|
|
60
|
+
const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;
|
|
61
|
+
const autoDisable = merged.autoDisable ?? { failingForDays: 5 };
|
|
62
|
+
const responseBodyLimit = merged.responseBodyLimit ?? 4096;
|
|
63
|
+
const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.0`;
|
|
64
|
+
const doFetch = merged.fetch;
|
|
65
|
+
const now = core.options.now;
|
|
66
|
+
let timer = null;
|
|
67
|
+
let inFlight = null;
|
|
68
|
+
/** Delay before the next attempt, per schedule position, with ±10% jitter. */
|
|
69
|
+
function nextDelayMs(attemptsMade) {
|
|
70
|
+
if (attemptsMade >= schedule.length) return null;
|
|
71
|
+
const nominal = require_core.durationToMs(schedule[attemptsMade]);
|
|
72
|
+
const jitter = 1 + (Math.random() * 2 - 1) * JITTER;
|
|
73
|
+
return Math.round(nominal * jitter);
|
|
74
|
+
}
|
|
75
|
+
async function processClaim(delivery) {
|
|
76
|
+
const app = delivery.applicationKey;
|
|
77
|
+
const trigger = delivery.pendingTrigger ?? "schedule";
|
|
78
|
+
const failTerminal = async (error, eventType) => {
|
|
79
|
+
const outcome = {
|
|
80
|
+
ok: false,
|
|
81
|
+
error,
|
|
82
|
+
durationMs: 0,
|
|
83
|
+
at: new Date(now()).toISOString()
|
|
84
|
+
};
|
|
85
|
+
await core.recordAttempt({
|
|
86
|
+
delivery,
|
|
87
|
+
outcome,
|
|
88
|
+
trigger,
|
|
89
|
+
nextAttemptAt: null,
|
|
90
|
+
eventType
|
|
91
|
+
});
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
const message = await core.getMessage(app, delivery.messageId);
|
|
95
|
+
if (!message) return failTerminal("Message no longer exists", "unknown");
|
|
96
|
+
const endpoint = await core.getEndpoint(app, delivery.endpointId);
|
|
97
|
+
if (!endpoint) return failTerminal("Endpoint no longer exists", message.eventType);
|
|
98
|
+
if (endpoint.disabled) {
|
|
99
|
+
const queue = core.options.queueStorage;
|
|
100
|
+
const recheckAt = now() + leaseMs;
|
|
101
|
+
const released = {
|
|
102
|
+
...delivery,
|
|
103
|
+
status: "pending",
|
|
104
|
+
nextAttemptAt: new Date(recheckAt).toISOString(),
|
|
105
|
+
leaseUntil: null,
|
|
106
|
+
updatedAt: new Date(now()).toISOString()
|
|
107
|
+
};
|
|
108
|
+
if (delivery.leaseUntil) await queue.removeItem(require_keys.dueKey(app, Date.parse(delivery.leaseUntil), delivery.id));
|
|
109
|
+
await queue.setItem(require_keys.deliveryKey(app, delivery.id), released);
|
|
110
|
+
await queue.setItem(require_keys.dueKey(app, recheckAt, delivery.id), {
|
|
111
|
+
app,
|
|
112
|
+
deliveryId: delivery.id
|
|
113
|
+
});
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const outcome = await require_core.attemptDelivery({
|
|
117
|
+
endpoint,
|
|
118
|
+
messageId: delivery.messageId,
|
|
119
|
+
body: message.envelope,
|
|
120
|
+
timeoutMs,
|
|
121
|
+
responseBodyLimit,
|
|
122
|
+
userAgent,
|
|
123
|
+
...doFetch ? { fetch: doFetch } : {},
|
|
124
|
+
nowMs: now()
|
|
125
|
+
});
|
|
126
|
+
const attemptsMade = delivery.attemptCount + 1;
|
|
127
|
+
const delay = outcome.ok ? null : nextDelayMs(attemptsMade);
|
|
128
|
+
await core.recordAttempt({
|
|
129
|
+
delivery,
|
|
130
|
+
outcome,
|
|
131
|
+
trigger,
|
|
132
|
+
nextAttemptAt: delay === null ? null : new Date(now() + delay).toISOString(),
|
|
133
|
+
eventType: message.eventType
|
|
134
|
+
});
|
|
135
|
+
await core.noteEndpointOutcome(app, endpoint.id, outcome.ok, autoDisable);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
async function runTick() {
|
|
139
|
+
const claimed = await core.claimDueDeliveries({
|
|
140
|
+
limit: batchSize,
|
|
141
|
+
leaseMs
|
|
142
|
+
});
|
|
143
|
+
if (claimed.length === 0) return 0;
|
|
144
|
+
let attempts = 0;
|
|
145
|
+
let cursor = 0;
|
|
146
|
+
const workers = Array.from({ length: Math.min(concurrency, claimed.length) }, async () => {
|
|
147
|
+
while (cursor < claimed.length) {
|
|
148
|
+
const delivery = claimed[cursor++];
|
|
149
|
+
try {
|
|
150
|
+
if (await processClaim(delivery)) attempts++;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(`[@xtandard/webhooks] delivery ${delivery.id} processing failed:`, error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
await Promise.all(workers);
|
|
157
|
+
return attempts;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
start() {
|
|
161
|
+
if (timer) return;
|
|
162
|
+
timer = setInterval(() => {
|
|
163
|
+
if (inFlight) return;
|
|
164
|
+
inFlight = runTick().catch((error) => {
|
|
165
|
+
console.warn("[@xtandard/webhooks] dispatcher tick failed:", error);
|
|
166
|
+
return 0;
|
|
167
|
+
}).finally(() => {
|
|
168
|
+
inFlight = null;
|
|
169
|
+
});
|
|
170
|
+
}, pollIntervalMs);
|
|
171
|
+
timer.unref?.();
|
|
172
|
+
},
|
|
173
|
+
async stop() {
|
|
174
|
+
if (timer) {
|
|
175
|
+
clearInterval(timer);
|
|
176
|
+
timer = null;
|
|
177
|
+
}
|
|
178
|
+
if (inFlight) await inFlight;
|
|
179
|
+
},
|
|
180
|
+
async tick() {
|
|
181
|
+
while (inFlight) await inFlight;
|
|
182
|
+
inFlight = runTick().finally(() => {
|
|
183
|
+
inFlight = null;
|
|
184
|
+
});
|
|
185
|
+
return inFlight;
|
|
186
|
+
},
|
|
187
|
+
get running() {
|
|
188
|
+
return timer !== null;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
Object.defineProperty(exports, "DEFAULT_RETRY_SCHEDULE", {
|
|
194
|
+
enumerable: true,
|
|
195
|
+
get: function() {
|
|
196
|
+
return DEFAULT_RETRY_SCHEDULE;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
Object.defineProperty(exports, "createDispatcher", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
get: function() {
|
|
202
|
+
return createDispatcher;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
Object.defineProperty(exports, "dispatcher_exports", {
|
|
206
|
+
enumerable: true,
|
|
207
|
+
get: function() {
|
|
208
|
+
return dispatcher_exports;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=dispatcher-B0xTEHt1.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher-B0xTEHt1.cjs","names":["durationToMs","dueKey","deliveryKey","attemptDelivery"],"sources":["../src/dispatcher.ts"],"sourcesContent":["/**\n * The delivery engine. Polls the due index, claims deliveries (lease-based,\n * multi-instance safe when storage supports CAS or the `deliveryQueue`\n * capability), performs the signed HTTP attempts, and drives each delivery's\n * state machine through the retry schedule into success or dead-letter.\n *\n * Runs in-process: the panel starts one by default, a split worker runs one\n * via the CLI (`xtandard-webhooks dispatch`), and tests drive {@link Dispatcher.tick}\n * manually. All timers are `unref()`ed so a dispatcher never keeps a process\n * alive on its own. Semantics are **at-least-once**: a crashed process loses\n * nothing (leases expire, the next tick reclaims); receivers dedupe on\n * `webhook-id`.\n *\n * @module\n */\n\nimport { attemptDelivery, type AttemptOutcome } from \"./deliver.ts\";\nimport type { WebhooksCore } from \"./core.ts\";\nimport { durationToMs } from \"./duration.ts\";\nimport { deliveryKey, dueKey, type DueEntry } from \"./keys.ts\";\nimport type { Delivery, WebhookDuration } from \"./schema.ts\";\nimport { VERSION } from \"./version.ts\";\n\n/** Options for {@link createDispatcher} (also accepted on panels and the core). */\nexport interface DispatcherOptions {\n /** How often to poll for due deliveries. Default `1000`. */\n pollIntervalMs?: number;\n /** Max deliveries claimed per tick. Default `20`. */\n batchSize?: number;\n /** Max in-flight HTTP attempts. Default `8`. */\n concurrency?: number;\n /** Per-attempt timeout (AbortController). Default `20_000`. */\n timeoutMs?: number;\n /** Claim lease duration; an expired lease makes a claim reclaimable. Default `60_000`. */\n leaseMs?: number;\n /**\n * Delay before attempt N+1 after attempt N fails (index 0 = the initial\n * attempt's delay). Exhausting the schedule dead-letters the delivery.\n * Default `[\"0s\", \"5s\", \"5m\", \"30m\", \"2h\", \"5h\", \"10h\"]` (Svix-compatible).\n */\n retrySchedule?: WebhookDuration[];\n /**\n * Auto-disable endpoints whose every attempt has failed for this many\n * consecutive days. `false` disables the policy. Default `{ failingForDays: 5 }`.\n */\n autoDisable?: { failingForDays?: number } | false;\n /** Cap on stored response-body characters per attempt. Default `4096`. */\n responseBodyLimit?: number;\n /** Injectable fetch (tests, instrumentation). Default: global fetch. */\n fetch?: typeof fetch;\n /** `user-agent` header. Default `\"xtandard-webhooks/<version>\"`. */\n userAgent?: string;\n}\n\n/** The delivery engine handle. */\nexport interface Dispatcher {\n /** Begin polling. Idempotent. */\n start(): void;\n /** Stop polling and wait for in-flight attempts to finish. */\n stop(): Promise<void>;\n /**\n * Run one manual pass: claim due deliveries, attempt them, record outcomes.\n * Returns the number of attempts made — the unit-test surface (tests never\n * assert on timers).\n */\n tick(): Promise<number>;\n readonly running: boolean;\n}\n\n/** The default retry schedule (Svix-compatible). */\nexport const DEFAULT_RETRY_SCHEDULE: WebhookDuration[] = [\n \"0s\",\n \"5s\",\n \"5m\",\n \"30m\",\n \"2h\",\n \"5h\",\n \"10h\",\n];\n\n/** Fractional jitter applied to every retry delay (±10%). */\nconst JITTER = 0.1;\n\n/**\n * Create a dispatcher over a core. Not started — call\n * {@link Dispatcher.start}, or drive {@link Dispatcher.tick} manually.\n *\n * @example\n * ```ts\n * import { createWebhooksCore, createDispatcher } from \"@xtandard/webhooks\";\n * import { createMemoryStorage } from \"@xtandard/webhooks/storage/memory\";\n *\n * const core = createWebhooksCore({ storage: createMemoryStorage() });\n * const dispatcher = createDispatcher(core);\n * dispatcher.start();\n * ```\n */\nexport function createDispatcher(core: WebhooksCore, options: DispatcherOptions = {}): Dispatcher {\n const merged = { ...core.options.dispatcher, ...options };\n const pollIntervalMs = merged.pollIntervalMs ?? 1000;\n const batchSize = merged.batchSize ?? 20;\n const concurrency = merged.concurrency ?? 8;\n const timeoutMs = merged.timeoutMs ?? 20_000;\n // The lease must outlast a single attempt, or a slow-but-alive attempt runs\n // past its lease and a second dispatcher reclaims and re-sends it. Enforce\n // lease > timeout with headroom for recording the outcome.\n const configuredLeaseMs = merged.leaseMs ?? 60_000;\n const leaseMs = Math.max(configuredLeaseMs, timeoutMs + 10_000);\n const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;\n const autoDisable = merged.autoDisable ?? { failingForDays: 5 };\n const responseBodyLimit = merged.responseBodyLimit ?? 4096;\n const userAgent = merged.userAgent ?? `xtandard-webhooks/${VERSION}`;\n const doFetch = merged.fetch;\n const now = core.options.now;\n\n let timer: ReturnType<typeof setInterval> | null = null;\n let inFlight: Promise<number> | null = null;\n\n /** Delay before the next attempt, per schedule position, with ±10% jitter. */\n function nextDelayMs(attemptsMade: number): number | null {\n if (attemptsMade >= schedule.length) return null; // exhausted\n const nominal = durationToMs(schedule[attemptsMade] as WebhookDuration);\n const jitter = 1 + (Math.random() * 2 - 1) * JITTER;\n return Math.round(nominal * jitter);\n }\n\n async function processClaim(delivery: Delivery): Promise<boolean> {\n const app = delivery.applicationKey;\n const trigger = delivery.pendingTrigger ?? \"schedule\";\n\n const failTerminal = async (error: string, eventType: string): Promise<boolean> => {\n const outcome: AttemptOutcome = {\n ok: false,\n error,\n durationMs: 0,\n at: new Date(now()).toISOString(),\n };\n await core.recordAttempt({ delivery, outcome, trigger, nextAttemptAt: null, eventType });\n return true;\n };\n\n const message = await core.getMessage(app, delivery.messageId);\n if (!message) return failTerminal(\"Message no longer exists\", \"unknown\");\n\n const endpoint = await core.getEndpoint(app, delivery.endpointId);\n if (!endpoint) return failTerminal(\"Endpoint no longer exists\", message.eventType);\n\n if (endpoint.disabled) {\n // Held, not failed: release the claim and re-check after the lease\n // window. Re-enabling the endpoint resumes delivery automatically.\n const queue = core.options.queueStorage;\n const recheckAt = now() + leaseMs;\n const released: Delivery = {\n ...delivery,\n status: \"pending\",\n nextAttemptAt: new Date(recheckAt).toISOString(),\n leaseUntil: null,\n updatedAt: new Date(now()).toISOString(),\n };\n // Remove the lease-position due entry the claim created, then park the\n // delivery at the recheck time.\n if (delivery.leaseUntil) {\n await queue.removeItem(dueKey(app, Date.parse(delivery.leaseUntil), delivery.id));\n }\n await queue.setItem(deliveryKey(app, delivery.id), released);\n await queue.setItem<DueEntry>(dueKey(app, recheckAt, delivery.id), {\n app,\n deliveryId: delivery.id,\n });\n return false; // no attempt made\n }\n\n const outcome = await attemptDelivery({\n endpoint,\n messageId: delivery.messageId,\n body: message.envelope,\n timeoutMs,\n responseBodyLimit,\n userAgent,\n ...(doFetch ? { fetch: doFetch } : {}),\n nowMs: now(),\n });\n\n const attemptsMade = delivery.attemptCount + 1;\n const delay = outcome.ok ? null : nextDelayMs(attemptsMade);\n await core.recordAttempt({\n delivery,\n outcome,\n trigger,\n nextAttemptAt: delay === null ? null : new Date(now() + delay).toISOString(),\n eventType: message.eventType,\n });\n await core.noteEndpointOutcome(app, endpoint.id, outcome.ok, autoDisable);\n return true;\n }\n\n async function runTick(): Promise<number> {\n const claimed = await core.claimDueDeliveries({ limit: batchSize, leaseMs });\n if (claimed.length === 0) return 0;\n\n let attempts = 0;\n let cursor = 0;\n const workers = Array.from({ length: Math.min(concurrency, claimed.length) }, async () => {\n while (cursor < claimed.length) {\n const delivery = claimed[cursor++] as Delivery;\n try {\n if (await processClaim(delivery)) attempts++;\n } catch (error) {\n // A storage failure mid-claim: leave the delivery leased; the lease\n // expiry re-exposes it. Never let one claim kill the tick.\n // eslint-disable-next-line no-console\n console.warn(`[@xtandard/webhooks] delivery ${delivery.id} processing failed:`, error);\n }\n }\n });\n await Promise.all(workers);\n return attempts;\n }\n\n const dispatcher: Dispatcher = {\n start() {\n if (timer) return;\n timer = setInterval(() => {\n if (inFlight) return; // never overlap ticks\n inFlight = runTick()\n .catch((error) => {\n // eslint-disable-next-line no-console\n console.warn(\"[@xtandard/webhooks] dispatcher tick failed:\", error);\n return 0;\n })\n .finally(() => {\n inFlight = null;\n });\n }, pollIntervalMs);\n // Never keep the host process alive just to poll.\n (timer as unknown as { unref?: () => void }).unref?.();\n },\n\n async stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n if (inFlight) await inFlight;\n },\n\n async tick() {\n // Manual ticks also serialize against the poller.\n while (inFlight) await inFlight;\n inFlight = runTick().finally(() => {\n inFlight = null;\n });\n return inFlight;\n },\n\n get running() {\n return timer !== null;\n },\n };\n\n return dispatcher;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
+
import { A as attemptDelivery, w as durationToMs } from "./core-CMpnmI5Q.mjs";
|
|
3
|
+
import { m as dueKey, p as deliveryKey } from "./keys-Byyj4quQ.mjs";
|
|
4
|
+
//#region src/dispatcher.ts
|
|
5
|
+
/**
|
|
6
|
+
* The delivery engine. Polls the due index, claims deliveries (lease-based,
|
|
7
|
+
* multi-instance safe when storage supports CAS or the `deliveryQueue`
|
|
8
|
+
* capability), performs the signed HTTP attempts, and drives each delivery's
|
|
9
|
+
* state machine through the retry schedule into success or dead-letter.
|
|
10
|
+
*
|
|
11
|
+
* Runs in-process: the panel starts one by default, a split worker runs one
|
|
12
|
+
* via the CLI (`xtandard-webhooks dispatch`), and tests drive {@link Dispatcher.tick}
|
|
13
|
+
* manually. All timers are `unref()`ed so a dispatcher never keeps a process
|
|
14
|
+
* alive on its own. Semantics are **at-least-once**: a crashed process loses
|
|
15
|
+
* nothing (leases expire, the next tick reclaims); receivers dedupe on
|
|
16
|
+
* `webhook-id`.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
var dispatcher_exports = /* @__PURE__ */ __exportAll({
|
|
21
|
+
DEFAULT_RETRY_SCHEDULE: () => DEFAULT_RETRY_SCHEDULE,
|
|
22
|
+
createDispatcher: () => createDispatcher
|
|
23
|
+
});
|
|
24
|
+
/** The default retry schedule (Svix-compatible). */
|
|
25
|
+
const DEFAULT_RETRY_SCHEDULE = [
|
|
26
|
+
"0s",
|
|
27
|
+
"5s",
|
|
28
|
+
"5m",
|
|
29
|
+
"30m",
|
|
30
|
+
"2h",
|
|
31
|
+
"5h",
|
|
32
|
+
"10h"
|
|
33
|
+
];
|
|
34
|
+
/** Fractional jitter applied to every retry delay (±10%). */
|
|
35
|
+
const JITTER = .1;
|
|
36
|
+
/**
|
|
37
|
+
* Create a dispatcher over a core. Not started — call
|
|
38
|
+
* {@link Dispatcher.start}, or drive {@link Dispatcher.tick} manually.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { createWebhooksCore, createDispatcher } from "@xtandard/webhooks";
|
|
43
|
+
* import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";
|
|
44
|
+
*
|
|
45
|
+
* const core = createWebhooksCore({ storage: createMemoryStorage() });
|
|
46
|
+
* const dispatcher = createDispatcher(core);
|
|
47
|
+
* dispatcher.start();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function createDispatcher(core, options = {}) {
|
|
51
|
+
const merged = {
|
|
52
|
+
...core.options.dispatcher,
|
|
53
|
+
...options
|
|
54
|
+
};
|
|
55
|
+
const pollIntervalMs = merged.pollIntervalMs ?? 1e3;
|
|
56
|
+
const batchSize = merged.batchSize ?? 20;
|
|
57
|
+
const concurrency = merged.concurrency ?? 8;
|
|
58
|
+
const timeoutMs = merged.timeoutMs ?? 2e4;
|
|
59
|
+
const configuredLeaseMs = merged.leaseMs ?? 6e4;
|
|
60
|
+
const leaseMs = Math.max(configuredLeaseMs, timeoutMs + 1e4);
|
|
61
|
+
const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;
|
|
62
|
+
const autoDisable = merged.autoDisable ?? { failingForDays: 5 };
|
|
63
|
+
const responseBodyLimit = merged.responseBodyLimit ?? 4096;
|
|
64
|
+
const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.0`;
|
|
65
|
+
const doFetch = merged.fetch;
|
|
66
|
+
const now = core.options.now;
|
|
67
|
+
let timer = null;
|
|
68
|
+
let inFlight = null;
|
|
69
|
+
/** Delay before the next attempt, per schedule position, with ±10% jitter. */
|
|
70
|
+
function nextDelayMs(attemptsMade) {
|
|
71
|
+
if (attemptsMade >= schedule.length) return null;
|
|
72
|
+
const nominal = durationToMs(schedule[attemptsMade]);
|
|
73
|
+
const jitter = 1 + (Math.random() * 2 - 1) * JITTER;
|
|
74
|
+
return Math.round(nominal * jitter);
|
|
75
|
+
}
|
|
76
|
+
async function processClaim(delivery) {
|
|
77
|
+
const app = delivery.applicationKey;
|
|
78
|
+
const trigger = delivery.pendingTrigger ?? "schedule";
|
|
79
|
+
const failTerminal = async (error, eventType) => {
|
|
80
|
+
const outcome = {
|
|
81
|
+
ok: false,
|
|
82
|
+
error,
|
|
83
|
+
durationMs: 0,
|
|
84
|
+
at: new Date(now()).toISOString()
|
|
85
|
+
};
|
|
86
|
+
await core.recordAttempt({
|
|
87
|
+
delivery,
|
|
88
|
+
outcome,
|
|
89
|
+
trigger,
|
|
90
|
+
nextAttemptAt: null,
|
|
91
|
+
eventType
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
const message = await core.getMessage(app, delivery.messageId);
|
|
96
|
+
if (!message) return failTerminal("Message no longer exists", "unknown");
|
|
97
|
+
const endpoint = await core.getEndpoint(app, delivery.endpointId);
|
|
98
|
+
if (!endpoint) return failTerminal("Endpoint no longer exists", message.eventType);
|
|
99
|
+
if (endpoint.disabled) {
|
|
100
|
+
const queue = core.options.queueStorage;
|
|
101
|
+
const recheckAt = now() + leaseMs;
|
|
102
|
+
const released = {
|
|
103
|
+
...delivery,
|
|
104
|
+
status: "pending",
|
|
105
|
+
nextAttemptAt: new Date(recheckAt).toISOString(),
|
|
106
|
+
leaseUntil: null,
|
|
107
|
+
updatedAt: new Date(now()).toISOString()
|
|
108
|
+
};
|
|
109
|
+
if (delivery.leaseUntil) await queue.removeItem(dueKey(app, Date.parse(delivery.leaseUntil), delivery.id));
|
|
110
|
+
await queue.setItem(deliveryKey(app, delivery.id), released);
|
|
111
|
+
await queue.setItem(dueKey(app, recheckAt, delivery.id), {
|
|
112
|
+
app,
|
|
113
|
+
deliveryId: delivery.id
|
|
114
|
+
});
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const outcome = await attemptDelivery({
|
|
118
|
+
endpoint,
|
|
119
|
+
messageId: delivery.messageId,
|
|
120
|
+
body: message.envelope,
|
|
121
|
+
timeoutMs,
|
|
122
|
+
responseBodyLimit,
|
|
123
|
+
userAgent,
|
|
124
|
+
...doFetch ? { fetch: doFetch } : {},
|
|
125
|
+
nowMs: now()
|
|
126
|
+
});
|
|
127
|
+
const attemptsMade = delivery.attemptCount + 1;
|
|
128
|
+
const delay = outcome.ok ? null : nextDelayMs(attemptsMade);
|
|
129
|
+
await core.recordAttempt({
|
|
130
|
+
delivery,
|
|
131
|
+
outcome,
|
|
132
|
+
trigger,
|
|
133
|
+
nextAttemptAt: delay === null ? null : new Date(now() + delay).toISOString(),
|
|
134
|
+
eventType: message.eventType
|
|
135
|
+
});
|
|
136
|
+
await core.noteEndpointOutcome(app, endpoint.id, outcome.ok, autoDisable);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
async function runTick() {
|
|
140
|
+
const claimed = await core.claimDueDeliveries({
|
|
141
|
+
limit: batchSize,
|
|
142
|
+
leaseMs
|
|
143
|
+
});
|
|
144
|
+
if (claimed.length === 0) return 0;
|
|
145
|
+
let attempts = 0;
|
|
146
|
+
let cursor = 0;
|
|
147
|
+
const workers = Array.from({ length: Math.min(concurrency, claimed.length) }, async () => {
|
|
148
|
+
while (cursor < claimed.length) {
|
|
149
|
+
const delivery = claimed[cursor++];
|
|
150
|
+
try {
|
|
151
|
+
if (await processClaim(delivery)) attempts++;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(`[@xtandard/webhooks] delivery ${delivery.id} processing failed:`, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
await Promise.all(workers);
|
|
158
|
+
return attempts;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
start() {
|
|
162
|
+
if (timer) return;
|
|
163
|
+
timer = setInterval(() => {
|
|
164
|
+
if (inFlight) return;
|
|
165
|
+
inFlight = runTick().catch((error) => {
|
|
166
|
+
console.warn("[@xtandard/webhooks] dispatcher tick failed:", error);
|
|
167
|
+
return 0;
|
|
168
|
+
}).finally(() => {
|
|
169
|
+
inFlight = null;
|
|
170
|
+
});
|
|
171
|
+
}, pollIntervalMs);
|
|
172
|
+
timer.unref?.();
|
|
173
|
+
},
|
|
174
|
+
async stop() {
|
|
175
|
+
if (timer) {
|
|
176
|
+
clearInterval(timer);
|
|
177
|
+
timer = null;
|
|
178
|
+
}
|
|
179
|
+
if (inFlight) await inFlight;
|
|
180
|
+
},
|
|
181
|
+
async tick() {
|
|
182
|
+
while (inFlight) await inFlight;
|
|
183
|
+
inFlight = runTick().finally(() => {
|
|
184
|
+
inFlight = null;
|
|
185
|
+
});
|
|
186
|
+
return inFlight;
|
|
187
|
+
},
|
|
188
|
+
get running() {
|
|
189
|
+
return timer !== null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
export { createDispatcher as n, dispatcher_exports as r, DEFAULT_RETRY_SCHEDULE as t };
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=dispatcher-Coubwrka.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatcher-Coubwrka.mjs","names":[],"sources":["../src/dispatcher.ts"],"sourcesContent":["/**\n * The delivery engine. Polls the due index, claims deliveries (lease-based,\n * multi-instance safe when storage supports CAS or the `deliveryQueue`\n * capability), performs the signed HTTP attempts, and drives each delivery's\n * state machine through the retry schedule into success or dead-letter.\n *\n * Runs in-process: the panel starts one by default, a split worker runs one\n * via the CLI (`xtandard-webhooks dispatch`), and tests drive {@link Dispatcher.tick}\n * manually. All timers are `unref()`ed so a dispatcher never keeps a process\n * alive on its own. Semantics are **at-least-once**: a crashed process loses\n * nothing (leases expire, the next tick reclaims); receivers dedupe on\n * `webhook-id`.\n *\n * @module\n */\n\nimport { attemptDelivery, type AttemptOutcome } from \"./deliver.ts\";\nimport type { WebhooksCore } from \"./core.ts\";\nimport { durationToMs } from \"./duration.ts\";\nimport { deliveryKey, dueKey, type DueEntry } from \"./keys.ts\";\nimport type { Delivery, WebhookDuration } from \"./schema.ts\";\nimport { VERSION } from \"./version.ts\";\n\n/** Options for {@link createDispatcher} (also accepted on panels and the core). */\nexport interface DispatcherOptions {\n /** How often to poll for due deliveries. Default `1000`. */\n pollIntervalMs?: number;\n /** Max deliveries claimed per tick. Default `20`. */\n batchSize?: number;\n /** Max in-flight HTTP attempts. Default `8`. */\n concurrency?: number;\n /** Per-attempt timeout (AbortController). Default `20_000`. */\n timeoutMs?: number;\n /** Claim lease duration; an expired lease makes a claim reclaimable. Default `60_000`. */\n leaseMs?: number;\n /**\n * Delay before attempt N+1 after attempt N fails (index 0 = the initial\n * attempt's delay). Exhausting the schedule dead-letters the delivery.\n * Default `[\"0s\", \"5s\", \"5m\", \"30m\", \"2h\", \"5h\", \"10h\"]` (Svix-compatible).\n */\n retrySchedule?: WebhookDuration[];\n /**\n * Auto-disable endpoints whose every attempt has failed for this many\n * consecutive days. `false` disables the policy. Default `{ failingForDays: 5 }`.\n */\n autoDisable?: { failingForDays?: number } | false;\n /** Cap on stored response-body characters per attempt. Default `4096`. */\n responseBodyLimit?: number;\n /** Injectable fetch (tests, instrumentation). Default: global fetch. */\n fetch?: typeof fetch;\n /** `user-agent` header. Default `\"xtandard-webhooks/<version>\"`. */\n userAgent?: string;\n}\n\n/** The delivery engine handle. */\nexport interface Dispatcher {\n /** Begin polling. Idempotent. */\n start(): void;\n /** Stop polling and wait for in-flight attempts to finish. */\n stop(): Promise<void>;\n /**\n * Run one manual pass: claim due deliveries, attempt them, record outcomes.\n * Returns the number of attempts made — the unit-test surface (tests never\n * assert on timers).\n */\n tick(): Promise<number>;\n readonly running: boolean;\n}\n\n/** The default retry schedule (Svix-compatible). */\nexport const DEFAULT_RETRY_SCHEDULE: WebhookDuration[] = [\n \"0s\",\n \"5s\",\n \"5m\",\n \"30m\",\n \"2h\",\n \"5h\",\n \"10h\",\n];\n\n/** Fractional jitter applied to every retry delay (±10%). */\nconst JITTER = 0.1;\n\n/**\n * Create a dispatcher over a core. Not started — call\n * {@link Dispatcher.start}, or drive {@link Dispatcher.tick} manually.\n *\n * @example\n * ```ts\n * import { createWebhooksCore, createDispatcher } from \"@xtandard/webhooks\";\n * import { createMemoryStorage } from \"@xtandard/webhooks/storage/memory\";\n *\n * const core = createWebhooksCore({ storage: createMemoryStorage() });\n * const dispatcher = createDispatcher(core);\n * dispatcher.start();\n * ```\n */\nexport function createDispatcher(core: WebhooksCore, options: DispatcherOptions = {}): Dispatcher {\n const merged = { ...core.options.dispatcher, ...options };\n const pollIntervalMs = merged.pollIntervalMs ?? 1000;\n const batchSize = merged.batchSize ?? 20;\n const concurrency = merged.concurrency ?? 8;\n const timeoutMs = merged.timeoutMs ?? 20_000;\n // The lease must outlast a single attempt, or a slow-but-alive attempt runs\n // past its lease and a second dispatcher reclaims and re-sends it. Enforce\n // lease > timeout with headroom for recording the outcome.\n const configuredLeaseMs = merged.leaseMs ?? 60_000;\n const leaseMs = Math.max(configuredLeaseMs, timeoutMs + 10_000);\n const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;\n const autoDisable = merged.autoDisable ?? { failingForDays: 5 };\n const responseBodyLimit = merged.responseBodyLimit ?? 4096;\n const userAgent = merged.userAgent ?? `xtandard-webhooks/${VERSION}`;\n const doFetch = merged.fetch;\n const now = core.options.now;\n\n let timer: ReturnType<typeof setInterval> | null = null;\n let inFlight: Promise<number> | null = null;\n\n /** Delay before the next attempt, per schedule position, with ±10% jitter. */\n function nextDelayMs(attemptsMade: number): number | null {\n if (attemptsMade >= schedule.length) return null; // exhausted\n const nominal = durationToMs(schedule[attemptsMade] as WebhookDuration);\n const jitter = 1 + (Math.random() * 2 - 1) * JITTER;\n return Math.round(nominal * jitter);\n }\n\n async function processClaim(delivery: Delivery): Promise<boolean> {\n const app = delivery.applicationKey;\n const trigger = delivery.pendingTrigger ?? \"schedule\";\n\n const failTerminal = async (error: string, eventType: string): Promise<boolean> => {\n const outcome: AttemptOutcome = {\n ok: false,\n error,\n durationMs: 0,\n at: new Date(now()).toISOString(),\n };\n await core.recordAttempt({ delivery, outcome, trigger, nextAttemptAt: null, eventType });\n return true;\n };\n\n const message = await core.getMessage(app, delivery.messageId);\n if (!message) return failTerminal(\"Message no longer exists\", \"unknown\");\n\n const endpoint = await core.getEndpoint(app, delivery.endpointId);\n if (!endpoint) return failTerminal(\"Endpoint no longer exists\", message.eventType);\n\n if (endpoint.disabled) {\n // Held, not failed: release the claim and re-check after the lease\n // window. Re-enabling the endpoint resumes delivery automatically.\n const queue = core.options.queueStorage;\n const recheckAt = now() + leaseMs;\n const released: Delivery = {\n ...delivery,\n status: \"pending\",\n nextAttemptAt: new Date(recheckAt).toISOString(),\n leaseUntil: null,\n updatedAt: new Date(now()).toISOString(),\n };\n // Remove the lease-position due entry the claim created, then park the\n // delivery at the recheck time.\n if (delivery.leaseUntil) {\n await queue.removeItem(dueKey(app, Date.parse(delivery.leaseUntil), delivery.id));\n }\n await queue.setItem(deliveryKey(app, delivery.id), released);\n await queue.setItem<DueEntry>(dueKey(app, recheckAt, delivery.id), {\n app,\n deliveryId: delivery.id,\n });\n return false; // no attempt made\n }\n\n const outcome = await attemptDelivery({\n endpoint,\n messageId: delivery.messageId,\n body: message.envelope,\n timeoutMs,\n responseBodyLimit,\n userAgent,\n ...(doFetch ? { fetch: doFetch } : {}),\n nowMs: now(),\n });\n\n const attemptsMade = delivery.attemptCount + 1;\n const delay = outcome.ok ? null : nextDelayMs(attemptsMade);\n await core.recordAttempt({\n delivery,\n outcome,\n trigger,\n nextAttemptAt: delay === null ? null : new Date(now() + delay).toISOString(),\n eventType: message.eventType,\n });\n await core.noteEndpointOutcome(app, endpoint.id, outcome.ok, autoDisable);\n return true;\n }\n\n async function runTick(): Promise<number> {\n const claimed = await core.claimDueDeliveries({ limit: batchSize, leaseMs });\n if (claimed.length === 0) return 0;\n\n let attempts = 0;\n let cursor = 0;\n const workers = Array.from({ length: Math.min(concurrency, claimed.length) }, async () => {\n while (cursor < claimed.length) {\n const delivery = claimed[cursor++] as Delivery;\n try {\n if (await processClaim(delivery)) attempts++;\n } catch (error) {\n // A storage failure mid-claim: leave the delivery leased; the lease\n // expiry re-exposes it. Never let one claim kill the tick.\n // eslint-disable-next-line no-console\n console.warn(`[@xtandard/webhooks] delivery ${delivery.id} processing failed:`, error);\n }\n }\n });\n await Promise.all(workers);\n return attempts;\n }\n\n const dispatcher: Dispatcher = {\n start() {\n if (timer) return;\n timer = setInterval(() => {\n if (inFlight) return; // never overlap ticks\n inFlight = runTick()\n .catch((error) => {\n // eslint-disable-next-line no-console\n console.warn(\"[@xtandard/webhooks] dispatcher tick failed:\", error);\n return 0;\n })\n .finally(() => {\n inFlight = null;\n });\n }, pollIntervalMs);\n // Never keep the host process alive just to poll.\n (timer as unknown as { unref?: () => void }).unref?.();\n },\n\n async stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n if (inFlight) await inFlight;\n },\n\n async tick() {\n // Manual ticks also serialize against the poller.\n while (inFlight) await inFlight;\n inFlight = runTick().finally(() => {\n inFlight = null;\n });\n return inFlight;\n },\n\n get running() {\n return timer !== null;\n },\n };\n\n return dispatcher;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_basic = require("./basic-BIW3Rvuz.cjs");
|
|
3
|
+
exports.basicAuth = require_basic.basicAuth;
|
|
4
|
+
exports.hashPassword = require_basic.hashPassword;
|
|
5
|
+
exports.verifyPassword = require_basic.verifyPassword;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { t as AuthProvider } from "./contract-lETlIuXo.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/basic.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Hash a password with scrypt and a fresh random salt.
|
|
6
|
+
*
|
|
7
|
+
* The returned string is self-describing and safe to store in config or a
|
|
8
|
+
* database: `scrypt$<saltHex>$<hashHex>`. Pass it back to
|
|
9
|
+
* {@link verifyPassword} (or supply it as a user's `passwordHash`) to check a
|
|
10
|
+
* candidate password.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const stored = await hashPassword("correct horse battery staple");
|
|
15
|
+
* // → "scrypt$<32 hex chars>$<128 hex chars>"
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare function hashPassword(password: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Verify a candidate `password` against a digest produced by
|
|
21
|
+
* {@link hashPassword}.
|
|
22
|
+
*
|
|
23
|
+
* Re-derives the scrypt hash using the stored salt and compares it to the stored
|
|
24
|
+
* hash with `crypto.timingSafeEqual` (constant time). Returns `false` — rather
|
|
25
|
+
* than throwing — for malformed or unrecognized digests.
|
|
26
|
+
*/
|
|
27
|
+
declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
28
|
+
/** A configured user for {@link basicAuth}. */
|
|
29
|
+
interface BasicAuthUser {
|
|
30
|
+
/** The login name, matched against the Basic credentials. */
|
|
31
|
+
username: string;
|
|
32
|
+
/**
|
|
33
|
+
* A digest produced by {@link hashPassword} (`scrypt$<salt>$<hash>`).
|
|
34
|
+
* Preferred for production.
|
|
35
|
+
*/
|
|
36
|
+
passwordHash?: string;
|
|
37
|
+
/**
|
|
38
|
+
* A plaintext password. **Development only** — prefer `passwordHash`. Compared
|
|
39
|
+
* in constant time but stored as cleartext in your config.
|
|
40
|
+
*/
|
|
41
|
+
password?: string;
|
|
42
|
+
/** Roles attached to the resulting {@link Principal}. */
|
|
43
|
+
roles?: string[];
|
|
44
|
+
/** Email attached to the resulting {@link Principal}. */
|
|
45
|
+
email?: string;
|
|
46
|
+
/** Principal id. Defaults to {@link BasicAuthUser.username} when omitted. */
|
|
47
|
+
id?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Options for {@link basicAuth}. */
|
|
50
|
+
interface BasicAuthOptions {
|
|
51
|
+
/** The known users. */
|
|
52
|
+
users: BasicAuthUser[];
|
|
53
|
+
/**
|
|
54
|
+
* Realm advertised in the `WWW-Authenticate` header on challenge.
|
|
55
|
+
* @default "xtandard-webhooks"
|
|
56
|
+
*/
|
|
57
|
+
realm?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Custom verifier. When supplied it takes precedence over `passwordHash` and
|
|
60
|
+
* `password` for matched users — delegate to your own credential store and
|
|
61
|
+
* return `true` to accept.
|
|
62
|
+
*/
|
|
63
|
+
passwordVerifier?: (username: string, password: string) => Promise<boolean> | boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create an HTTP Basic {@link AuthProvider}.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { basicAuth, hashPassword } from "@xtandard/webhooks/auth/basic";
|
|
71
|
+
*
|
|
72
|
+
* const auth = basicAuth({
|
|
73
|
+
* realm: "Webhooks Admin",
|
|
74
|
+
* users: [
|
|
75
|
+
* { username: "admin", passwordHash: await hashPassword("s3cret"), roles: ["admin"] },
|
|
76
|
+
* ],
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare function basicAuth(options: BasicAuthOptions): AuthProvider;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { BasicAuthOptions, BasicAuthUser, basicAuth, hashPassword, verifyPassword };
|
|
83
|
+
//# sourceMappingURL=entry-auth-basic.d.cts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { t as AuthProvider } from "./contract-lETlIuXo.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/basic.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Hash a password with scrypt and a fresh random salt.
|
|
6
|
+
*
|
|
7
|
+
* The returned string is self-describing and safe to store in config or a
|
|
8
|
+
* database: `scrypt$<saltHex>$<hashHex>`. Pass it back to
|
|
9
|
+
* {@link verifyPassword} (or supply it as a user's `passwordHash`) to check a
|
|
10
|
+
* candidate password.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const stored = await hashPassword("correct horse battery staple");
|
|
15
|
+
* // → "scrypt$<32 hex chars>$<128 hex chars>"
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare function hashPassword(password: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Verify a candidate `password` against a digest produced by
|
|
21
|
+
* {@link hashPassword}.
|
|
22
|
+
*
|
|
23
|
+
* Re-derives the scrypt hash using the stored salt and compares it to the stored
|
|
24
|
+
* hash with `crypto.timingSafeEqual` (constant time). Returns `false` — rather
|
|
25
|
+
* than throwing — for malformed or unrecognized digests.
|
|
26
|
+
*/
|
|
27
|
+
declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
28
|
+
/** A configured user for {@link basicAuth}. */
|
|
29
|
+
interface BasicAuthUser {
|
|
30
|
+
/** The login name, matched against the Basic credentials. */
|
|
31
|
+
username: string;
|
|
32
|
+
/**
|
|
33
|
+
* A digest produced by {@link hashPassword} (`scrypt$<salt>$<hash>`).
|
|
34
|
+
* Preferred for production.
|
|
35
|
+
*/
|
|
36
|
+
passwordHash?: string;
|
|
37
|
+
/**
|
|
38
|
+
* A plaintext password. **Development only** — prefer `passwordHash`. Compared
|
|
39
|
+
* in constant time but stored as cleartext in your config.
|
|
40
|
+
*/
|
|
41
|
+
password?: string;
|
|
42
|
+
/** Roles attached to the resulting {@link Principal}. */
|
|
43
|
+
roles?: string[];
|
|
44
|
+
/** Email attached to the resulting {@link Principal}. */
|
|
45
|
+
email?: string;
|
|
46
|
+
/** Principal id. Defaults to {@link BasicAuthUser.username} when omitted. */
|
|
47
|
+
id?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Options for {@link basicAuth}. */
|
|
50
|
+
interface BasicAuthOptions {
|
|
51
|
+
/** The known users. */
|
|
52
|
+
users: BasicAuthUser[];
|
|
53
|
+
/**
|
|
54
|
+
* Realm advertised in the `WWW-Authenticate` header on challenge.
|
|
55
|
+
* @default "xtandard-webhooks"
|
|
56
|
+
*/
|
|
57
|
+
realm?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Custom verifier. When supplied it takes precedence over `passwordHash` and
|
|
60
|
+
* `password` for matched users — delegate to your own credential store and
|
|
61
|
+
* return `true` to accept.
|
|
62
|
+
*/
|
|
63
|
+
passwordVerifier?: (username: string, password: string) => Promise<boolean> | boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create an HTTP Basic {@link AuthProvider}.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { basicAuth, hashPassword } from "@xtandard/webhooks/auth/basic";
|
|
71
|
+
*
|
|
72
|
+
* const auth = basicAuth({
|
|
73
|
+
* realm: "Webhooks Admin",
|
|
74
|
+
* users: [
|
|
75
|
+
* { username: "admin", passwordHash: await hashPassword("s3cret"), roles: ["admin"] },
|
|
76
|
+
* ],
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare function basicAuth(options: BasicAuthOptions): AuthProvider;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { BasicAuthOptions, BasicAuthUser, basicAuth, hashPassword, verifyPassword };
|
|
83
|
+
//# sourceMappingURL=entry-auth-basic.d.mts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/auth/delegated.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create an {@link AuthProvider} from plain functions.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { delegatedAuth } from "@xtandard/webhooks/auth/delegated";
|
|
9
|
+
*
|
|
10
|
+
* const auth = delegatedAuth({
|
|
11
|
+
* authenticate: async (request) => {
|
|
12
|
+
* const token = request.headers.get("authorization")?.replace("Bearer ", "");
|
|
13
|
+
* return token ? await verifyToken(token) : null;
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function delegatedAuth(options) {
|
|
19
|
+
const provider = { async authenticate(request) {
|
|
20
|
+
return await options.authenticate(request);
|
|
21
|
+
} };
|
|
22
|
+
if (options.challenge) provider.challenge = options.challenge;
|
|
23
|
+
return provider;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.delegatedAuth = delegatedAuth;
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=entry-auth-delegated.cjs.map
|