@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.
Files changed (279) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -0
  3. package/bin/xtandard-webhooks.mjs +3 -0
  4. package/dist/basic-BIW3Rvuz.cjs +199 -0
  5. package/dist/basic-BIW3Rvuz.cjs.map +1 -0
  6. package/dist/basic-DKk0Xfuu.mjs +176 -0
  7. package/dist/basic-DKk0Xfuu.mjs.map +1 -0
  8. package/dist/chunk-D7D4PA-g.mjs +13 -0
  9. package/dist/cli.cjs +655 -0
  10. package/dist/cli.cjs.map +1 -0
  11. package/dist/cli.d.cts +42 -0
  12. package/dist/cli.d.mts +42 -0
  13. package/dist/cli.mjs +653 -0
  14. package/dist/cli.mjs.map +1 -0
  15. package/dist/contract-8h-Azxa5.d.cts +71 -0
  16. package/dist/contract-9XpcwcCn.mjs +22 -0
  17. package/dist/contract-9XpcwcCn.mjs.map +1 -0
  18. package/dist/contract-B2d5dNU3.cjs +33 -0
  19. package/dist/contract-B2d5dNU3.cjs.map +1 -0
  20. package/dist/contract-BEhDcd_5.mjs +28 -0
  21. package/dist/contract-BEhDcd_5.mjs.map +1 -0
  22. package/dist/contract-Bf1qguwt.cjs +57 -0
  23. package/dist/contract-Bf1qguwt.cjs.map +1 -0
  24. package/dist/contract-Bnb3fgRJ.d.cts +177 -0
  25. package/dist/contract-C2r2Xzwp.d.mts +46 -0
  26. package/dist/contract-CiPskNvS.d.cts +46 -0
  27. package/dist/contract-DhQ4JjGG.d.mts +71 -0
  28. package/dist/contract-T1kcZNdG.d.mts +177 -0
  29. package/dist/contract-lETlIuXo.d.cts +30 -0
  30. package/dist/contract-lETlIuXo.d.mts +30 -0
  31. package/dist/core-CMpnmI5Q.mjs +1605 -0
  32. package/dist/core-CMpnmI5Q.mjs.map +1 -0
  33. package/dist/core-DT4ppWh8.d.mts +502 -0
  34. package/dist/core-KJawHjFF.d.cts +502 -0
  35. package/dist/core-ZGhH6Vs2.cjs +1790 -0
  36. package/dist/core-ZGhH6Vs2.cjs.map +1 -0
  37. package/dist/core.cjs +8 -0
  38. package/dist/core.d.cts +2 -0
  39. package/dist/core.d.mts +2 -0
  40. package/dist/core.mjs +2 -0
  41. package/dist/create-fetch-handler-BIdk9P30.mjs +1724 -0
  42. package/dist/create-fetch-handler-BIdk9P30.mjs.map +1 -0
  43. package/dist/create-fetch-handler-CmooujQo.cjs +1771 -0
  44. package/dist/create-fetch-handler-CmooujQo.cjs.map +1 -0
  45. package/dist/create-fetch-handler-Dlkhustu.d.cts +162 -0
  46. package/dist/create-fetch-handler-jy3hy5nZ.d.mts +162 -0
  47. package/dist/dispatcher-B0xTEHt1.cjs +212 -0
  48. package/dist/dispatcher-B0xTEHt1.cjs.map +1 -0
  49. package/dist/dispatcher-Coubwrka.mjs +196 -0
  50. package/dist/dispatcher-Coubwrka.mjs.map +1 -0
  51. package/dist/entry-auth-basic.cjs +5 -0
  52. package/dist/entry-auth-basic.d.cts +83 -0
  53. package/dist/entry-auth-basic.d.mts +83 -0
  54. package/dist/entry-auth-basic.mjs +2 -0
  55. package/dist/entry-auth-delegated.cjs +28 -0
  56. package/dist/entry-auth-delegated.cjs.map +1 -0
  57. package/dist/entry-auth-delegated.d.cts +36 -0
  58. package/dist/entry-auth-delegated.d.mts +36 -0
  59. package/dist/entry-auth-delegated.mjs +27 -0
  60. package/dist/entry-auth-delegated.mjs.map +1 -0
  61. package/dist/entry-auth-none.cjs +4 -0
  62. package/dist/entry-auth-none.d.cts +25 -0
  63. package/dist/entry-auth-none.d.mts +25 -0
  64. package/dist/entry-auth-none.mjs +2 -0
  65. package/dist/entry-authorization-delegated.cjs +27 -0
  66. package/dist/entry-authorization-delegated.cjs.map +1 -0
  67. package/dist/entry-authorization-delegated.d.cts +31 -0
  68. package/dist/entry-authorization-delegated.d.mts +31 -0
  69. package/dist/entry-authorization-delegated.mjs +26 -0
  70. package/dist/entry-authorization-delegated.mjs.map +1 -0
  71. package/dist/entry-authorization-none.cjs +3 -0
  72. package/dist/entry-authorization-none.d.cts +18 -0
  73. package/dist/entry-authorization-none.d.mts +18 -0
  74. package/dist/entry-authorization-none.mjs +2 -0
  75. package/dist/entry-authorization-roles.cjs +6 -0
  76. package/dist/entry-authorization-roles.d.cts +65 -0
  77. package/dist/entry-authorization-roles.d.mts +65 -0
  78. package/dist/entry-authorization-roles.mjs +2 -0
  79. package/dist/entry-bun.cjs +24 -0
  80. package/dist/entry-bun.cjs.map +1 -0
  81. package/dist/entry-bun.d.cts +8 -0
  82. package/dist/entry-bun.d.mts +8 -0
  83. package/dist/entry-bun.mjs +23 -0
  84. package/dist/entry-bun.mjs.map +1 -0
  85. package/dist/entry-drizzle-mysql.cjs +20 -0
  86. package/dist/entry-drizzle-mysql.cjs.map +1 -0
  87. package/dist/entry-drizzle-mysql.d.cts +27 -0
  88. package/dist/entry-drizzle-mysql.d.mts +27 -0
  89. package/dist/entry-drizzle-mysql.mjs +19 -0
  90. package/dist/entry-drizzle-mysql.mjs.map +1 -0
  91. package/dist/entry-drizzle-pg.cjs +21 -0
  92. package/dist/entry-drizzle-pg.cjs.map +1 -0
  93. package/dist/entry-drizzle-pg.d.cts +26 -0
  94. package/dist/entry-drizzle-pg.d.mts +26 -0
  95. package/dist/entry-drizzle-pg.mjs +20 -0
  96. package/dist/entry-drizzle-pg.mjs.map +1 -0
  97. package/dist/entry-drizzle-sqlite.cjs +21 -0
  98. package/dist/entry-drizzle-sqlite.cjs.map +1 -0
  99. package/dist/entry-drizzle-sqlite.d.cts +23 -0
  100. package/dist/entry-drizzle-sqlite.d.mts +23 -0
  101. package/dist/entry-drizzle-sqlite.mjs +20 -0
  102. package/dist/entry-drizzle-sqlite.mjs.map +1 -0
  103. package/dist/entry-elysia.cjs +125 -0
  104. package/dist/entry-elysia.cjs.map +1 -0
  105. package/dist/entry-elysia.d.cts +1017 -0
  106. package/dist/entry-elysia.d.mts +1017 -0
  107. package/dist/entry-elysia.mjs +123 -0
  108. package/dist/entry-elysia.mjs.map +1 -0
  109. package/dist/entry-express.cjs +57 -0
  110. package/dist/entry-express.cjs.map +1 -0
  111. package/dist/entry-express.d.cts +15 -0
  112. package/dist/entry-express.d.mts +15 -0
  113. package/dist/entry-express.mjs +56 -0
  114. package/dist/entry-express.mjs.map +1 -0
  115. package/dist/entry-hono.cjs +35 -0
  116. package/dist/entry-hono.cjs.map +1 -0
  117. package/dist/entry-hono.d.cts +16 -0
  118. package/dist/entry-hono.d.mts +16 -0
  119. package/dist/entry-hono.mjs +34 -0
  120. package/dist/entry-hono.mjs.map +1 -0
  121. package/dist/entry-hooks-log.cjs +22 -0
  122. package/dist/entry-hooks-log.cjs.map +1 -0
  123. package/dist/entry-hooks-log.d.cts +23 -0
  124. package/dist/entry-hooks-log.d.mts +23 -0
  125. package/dist/entry-hooks-log.mjs +21 -0
  126. package/dist/entry-hooks-log.mjs.map +1 -0
  127. package/dist/entry-storage-cloudflare-kv.cjs +47 -0
  128. package/dist/entry-storage-cloudflare-kv.cjs.map +1 -0
  129. package/dist/entry-storage-cloudflare-kv.d.cts +42 -0
  130. package/dist/entry-storage-cloudflare-kv.d.mts +42 -0
  131. package/dist/entry-storage-cloudflare-kv.mjs +46 -0
  132. package/dist/entry-storage-cloudflare-kv.mjs.map +1 -0
  133. package/dist/entry-storage-drizzle.cjs +78 -0
  134. package/dist/entry-storage-drizzle.cjs.map +1 -0
  135. package/dist/entry-storage-drizzle.d.cts +30 -0
  136. package/dist/entry-storage-drizzle.d.mts +30 -0
  137. package/dist/entry-storage-drizzle.mjs +77 -0
  138. package/dist/entry-storage-drizzle.mjs.map +1 -0
  139. package/dist/entry-storage-file.cjs +4 -0
  140. package/dist/entry-storage-file.d.cts +30 -0
  141. package/dist/entry-storage-file.d.mts +30 -0
  142. package/dist/entry-storage-file.mjs +2 -0
  143. package/dist/entry-storage-libsql.cjs +3 -0
  144. package/dist/entry-storage-libsql.d.cts +48 -0
  145. package/dist/entry-storage-libsql.d.mts +48 -0
  146. package/dist/entry-storage-libsql.mjs +2 -0
  147. package/dist/entry-storage-memory.cjs +3 -0
  148. package/dist/entry-storage-memory.d.cts +2 -0
  149. package/dist/entry-storage-memory.d.mts +2 -0
  150. package/dist/entry-storage-memory.mjs +2 -0
  151. package/dist/entry-storage-mongodb.cjs +3 -0
  152. package/dist/entry-storage-mongodb.d.cts +55 -0
  153. package/dist/entry-storage-mongodb.d.mts +55 -0
  154. package/dist/entry-storage-mongodb.mjs +2 -0
  155. package/dist/entry-storage-postgres.cjs +3 -0
  156. package/dist/entry-storage-postgres.d.cts +62 -0
  157. package/dist/entry-storage-postgres.d.mts +62 -0
  158. package/dist/entry-storage-postgres.mjs +2 -0
  159. package/dist/entry-storage-redis.cjs +4 -0
  160. package/dist/entry-storage-redis.d.cts +77 -0
  161. package/dist/entry-storage-redis.d.mts +77 -0
  162. package/dist/entry-storage-redis.mjs +2 -0
  163. package/dist/entry-storage-sqlite.cjs +3 -0
  164. package/dist/entry-storage-sqlite.d.cts +36 -0
  165. package/dist/entry-storage-sqlite.d.mts +36 -0
  166. package/dist/entry-storage-sqlite.mjs +2 -0
  167. package/dist/entry-storage-unstorage.cjs +42 -0
  168. package/dist/entry-storage-unstorage.cjs.map +1 -0
  169. package/dist/entry-storage-unstorage.d.cts +29 -0
  170. package/dist/entry-storage-unstorage.d.mts +29 -0
  171. package/dist/entry-storage-unstorage.mjs +41 -0
  172. package/dist/entry-storage-unstorage.mjs.map +1 -0
  173. package/dist/file-COBYZA4Q.cjs +148 -0
  174. package/dist/file-COBYZA4Q.cjs.map +1 -0
  175. package/dist/file-fi02eFHk.mjs +131 -0
  176. package/dist/file-fi02eFHk.mjs.map +1 -0
  177. package/dist/index.cjs +123 -0
  178. package/dist/index.cjs.map +1 -0
  179. package/dist/index.d.cts +368 -0
  180. package/dist/index.d.mts +366 -0
  181. package/dist/index.mjs +61 -0
  182. package/dist/index.mjs.map +1 -0
  183. package/dist/keys-Byyj4quQ.mjs +111 -0
  184. package/dist/keys-Byyj4quQ.mjs.map +1 -0
  185. package/dist/keys-FiKpaVHX.cjs +302 -0
  186. package/dist/keys-FiKpaVHX.cjs.map +1 -0
  187. package/dist/libsql-bpVi0bXN.mjs +113 -0
  188. package/dist/libsql-bpVi0bXN.mjs.map +1 -0
  189. package/dist/libsql-pPJEo1e4.cjs +124 -0
  190. package/dist/libsql-pPJEo1e4.cjs.map +1 -0
  191. package/dist/memory-8Ef-PL5a.cjs +137 -0
  192. package/dist/memory-8Ef-PL5a.cjs.map +1 -0
  193. package/dist/memory-BMsSSwqn.mjs +127 -0
  194. package/dist/memory-BMsSSwqn.mjs.map +1 -0
  195. package/dist/memory-FnMJWCmB.d.cts +28 -0
  196. package/dist/memory-qIvANEs_.d.mts +28 -0
  197. package/dist/mongodb-Cy8yo0uk.cjs +108 -0
  198. package/dist/mongodb-Cy8yo0uk.cjs.map +1 -0
  199. package/dist/mongodb-Ddaq9mml.mjs +97 -0
  200. package/dist/mongodb-Ddaq9mml.mjs.map +1 -0
  201. package/dist/none-BnZtaGNJ.mjs +23 -0
  202. package/dist/none-BnZtaGNJ.mjs.map +1 -0
  203. package/dist/none-CAsxCOWN.cjs +49 -0
  204. package/dist/none-CAsxCOWN.cjs.map +1 -0
  205. package/dist/none-CZVrfnmF.cjs +33 -0
  206. package/dist/none-CZVrfnmF.cjs.map +1 -0
  207. package/dist/none-GhVIoh_s.mjs +33 -0
  208. package/dist/none-GhVIoh_s.mjs.map +1 -0
  209. package/dist/postgres-C8WbchFa.cjs +134 -0
  210. package/dist/postgres-C8WbchFa.cjs.map +1 -0
  211. package/dist/postgres-c3pAhmhr.mjs +123 -0
  212. package/dist/postgres-c3pAhmhr.mjs.map +1 -0
  213. package/dist/react.css +1 -0
  214. package/dist/react.js +31465 -0
  215. package/dist/receiver.cjs +43 -0
  216. package/dist/receiver.cjs.map +1 -0
  217. package/dist/receiver.d.cts +36 -0
  218. package/dist/receiver.d.mts +36 -0
  219. package/dist/receiver.mjs +40 -0
  220. package/dist/receiver.mjs.map +1 -0
  221. package/dist/redis-CFJkuSgB.cjs +270 -0
  222. package/dist/redis-CFJkuSgB.cjs.map +1 -0
  223. package/dist/redis-CvLi0KF7.mjs +254 -0
  224. package/dist/redis-CvLi0KF7.mjs.map +1 -0
  225. package/dist/roles-D0G9XqBq.cjs +128 -0
  226. package/dist/roles-D0G9XqBq.cjs.map +1 -0
  227. package/dist/roles-vp361lTk.mjs +99 -0
  228. package/dist/roles-vp361lTk.mjs.map +1 -0
  229. package/dist/schema-mo__wv4P.d.cts +233 -0
  230. package/dist/schema-mo__wv4P.d.mts +233 -0
  231. package/dist/schema.cjs +13 -0
  232. package/dist/schema.cjs.map +1 -0
  233. package/dist/schema.d.cts +2 -0
  234. package/dist/schema.d.mts +2 -0
  235. package/dist/schema.mjs +11 -0
  236. package/dist/schema.mjs.map +1 -0
  237. package/dist/signing.cjs +162 -0
  238. package/dist/signing.cjs.map +1 -0
  239. package/dist/signing.d.cts +73 -0
  240. package/dist/signing.d.mts +73 -0
  241. package/dist/signing.mjs +156 -0
  242. package/dist/signing.mjs.map +1 -0
  243. package/dist/sqlite-Cmqnrjes.mjs +67 -0
  244. package/dist/sqlite-Cmqnrjes.mjs.map +1 -0
  245. package/dist/sqlite-Dcufk0x3.cjs +78 -0
  246. package/dist/sqlite-Dcufk0x3.cjs.map +1 -0
  247. package/dist/table-Ce3Tzwqs.d.cts +11 -0
  248. package/dist/table-Ce3Tzwqs.d.mts +11 -0
  249. package/dist/testing.cjs +134 -0
  250. package/dist/testing.cjs.map +1 -0
  251. package/dist/testing.d.cts +80 -0
  252. package/dist/testing.d.mts +80 -0
  253. package/dist/testing.mjs +131 -0
  254. package/dist/testing.mjs.map +1 -0
  255. package/dist/types-react/react.d.ts +98 -0
  256. package/dist/types-react/schema.d.ts +229 -0
  257. package/dist/types-react/ui/App.d.ts +22 -0
  258. package/dist/types-react/ui/api.d.ts +97 -0
  259. package/dist/types-react/ui/components/JsonCodeEditor.d.ts +12 -0
  260. package/dist/types-react/ui/components/ThemeToggle.d.ts +2 -0
  261. package/dist/types-react/ui/components/Toast.d.ts +16 -0
  262. package/dist/types-react/ui/components/primitives.d.ts +50 -0
  263. package/dist/types-react/ui/components/ui-bits.d.ts +22 -0
  264. package/dist/types-react/ui/components/webhook-bits.d.ts +51 -0
  265. package/dist/types-react/ui/lib/format.d.ts +39 -0
  266. package/dist/types-react/ui/lib/nav-guard.d.ts +20 -0
  267. package/dist/types-react/ui/lib/utils.d.ts +3 -0
  268. package/dist/types-react/ui/theme.d.ts +12 -0
  269. package/dist/types-react/ui/types.d.ts +80 -0
  270. package/dist/types-react/ui/views/AuditView.d.ts +6 -0
  271. package/dist/types-react/ui/views/DeliveriesView.d.ts +12 -0
  272. package/dist/types-react/ui/views/EndpointsView.d.ts +11 -0
  273. package/dist/types-react/ui/views/EventTypesView.d.ts +11 -0
  274. package/dist/types-react/ui/views/MessagesView.d.ts +10 -0
  275. package/dist/types-react/ui/views/OverviewView.d.ts +12 -0
  276. package/dist/ui/assets/index-B0eoQX2U.css +1 -0
  277. package/dist/ui/assets/index-S5t_CLOe.js +209 -0
  278. package/dist/ui/index.html +14 -0
  279. 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":";;;;;;;;;;;;;;;;;;;;;;;;AAsEA,MAAa,yBAA4C;CACvD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,SAAS;;;;;;;;;;;;;;;AAgBf,SAAgB,iBAAiB,MAAoB,UAA6B,CAAC,GAAe;CAChG,MAAM,SAAS;EAAE,GAAG,KAAK,QAAQ;EAAY,GAAG;CAAQ;CACxD,MAAM,iBAAiB,OAAO,kBAAkB;CAChD,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,cAAc,OAAO,eAAe;CAC1C,MAAM,YAAY,OAAO,aAAa;CAItC,MAAM,oBAAoB,OAAO,WAAW;CAC5C,MAAM,UAAU,KAAK,IAAI,mBAAmB,YAAY,GAAM;CAC9D,MAAM,WAAW,OAAO,iBAAiB;CACzC,MAAM,cAAc,OAAO,eAAe,EAAE,gBAAgB,EAAE;CAC9D,MAAM,oBAAoB,OAAO,qBAAqB;CACtD,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,UAAU,OAAO;CACvB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,QAA+C;CACnD,IAAI,WAAmC;;CAGvC,SAAS,YAAY,cAAqC;EACxD,IAAI,gBAAgB,SAAS,QAAQ,OAAO;EAC5C,MAAM,UAAU,aAAa,SAAS,aAAgC;EACtE,MAAM,SAAS,KAAK,KAAK,OAAO,IAAI,IAAI,KAAK;EAC7C,OAAO,KAAK,MAAM,UAAU,MAAM;CACpC;CAEA,eAAe,aAAa,UAAsC;EAChE,MAAM,MAAM,SAAS;EACrB,MAAM,UAAU,SAAS,kBAAkB;EAE3C,MAAM,eAAe,OAAO,OAAe,cAAwC;GACjF,MAAM,UAA0B;IAC9B,IAAI;IACJ;IACA,YAAY;IACZ,IAAI,IAAI,KAAK,IAAI,CAAC,EAAE,YAAY;GAClC;GACA,MAAM,KAAK,cAAc;IAAE;IAAU;IAAS;IAAS,eAAe;IAAM;GAAU,CAAC;GACvF,OAAO;EACT;EAEA,MAAM,UAAU,MAAM,KAAK,WAAW,KAAK,SAAS,SAAS;EAC7D,IAAI,CAAC,SAAS,OAAO,aAAa,4BAA4B,SAAS;EAEvE,MAAM,WAAW,MAAM,KAAK,YAAY,KAAK,SAAS,UAAU;EAChE,IAAI,CAAC,UAAU,OAAO,aAAa,6BAA6B,QAAQ,SAAS;EAEjF,IAAI,SAAS,UAAU;GAGrB,MAAM,QAAQ,KAAK,QAAQ;GAC3B,MAAM,YAAY,IAAI,IAAI;GAC1B,MAAM,WAAqB;IACzB,GAAG;IACH,QAAQ;IACR,eAAe,IAAI,KAAK,SAAS,EAAE,YAAY;IAC/C,YAAY;IACZ,WAAW,IAAI,KAAK,IAAI,CAAC,EAAE,YAAY;GACzC;GAGA,IAAI,SAAS,YACX,MAAM,MAAM,WAAW,OAAO,KAAK,KAAK,MAAM,SAAS,UAAU,GAAG,SAAS,EAAE,CAAC;GAElF,MAAM,MAAM,QAAQ,YAAY,KAAK,SAAS,EAAE,GAAG,QAAQ;GAC3D,MAAM,MAAM,QAAkB,OAAO,KAAK,WAAW,SAAS,EAAE,GAAG;IACjE;IACA,YAAY,SAAS;GACvB,CAAC;GACD,OAAO;EACT;EAEA,MAAM,UAAU,MAAM,gBAAgB;GACpC;GACA,WAAW,SAAS;GACpB,MAAM,QAAQ;GACd;GACA;GACA;GACA,GAAI,UAAU,EAAE,OAAO,QAAQ,IAAI,CAAC;GACpC,OAAO,IAAI;EACb,CAAC;EAED,MAAM,eAAe,SAAS,eAAe;EAC7C,MAAM,QAAQ,QAAQ,KAAK,OAAO,YAAY,YAAY;EAC1D,MAAM,KAAK,cAAc;GACvB;GACA;GACA;GACA,eAAe,UAAU,OAAO,OAAO,IAAI,KAAK,IAAI,IAAI,KAAK,EAAE,YAAY;GAC3E,WAAW,QAAQ;EACrB,CAAC;EACD,MAAM,KAAK,oBAAoB,KAAK,SAAS,IAAI,QAAQ,IAAI,WAAW;EACxE,OAAO;CACT;CAEA,eAAe,UAA2B;EACxC,MAAM,UAAU,MAAM,KAAK,mBAAmB;GAAE,OAAO;GAAW;EAAQ,CAAC;EAC3E,IAAI,QAAQ,WAAW,GAAG,OAAO;EAEjC,IAAI,WAAW;EACf,IAAI,SAAS;EACb,MAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,QAAQ,MAAM,EAAE,GAAG,YAAY;GACxF,OAAO,SAAS,QAAQ,QAAQ;IAC9B,MAAM,WAAW,QAAQ;IACzB,IAAI;KACF,IAAI,MAAM,aAAa,QAAQ,GAAG;IACpC,SAAS,OAAO;KAId,QAAQ,KAAK,iCAAiC,SAAS,GAAG,sBAAsB,KAAK;IACvF;GACF;EACF,CAAC;EACD,MAAM,QAAQ,IAAI,OAAO;EACzB,OAAO;CACT;CA2CA,OAAO;EAxCL,QAAQ;GACN,IAAI,OAAO;GACX,QAAQ,kBAAkB;IACxB,IAAI,UAAU;IACd,WAAW,QAAQ,EAChB,OAAO,UAAU;KAEhB,QAAQ,KAAK,gDAAgD,KAAK;KAClE,OAAO;IACT,CAAC,EACA,cAAc;KACb,WAAW;IACb,CAAC;GACL,GAAG,cAAc;GAEjB,MAA6C,QAAQ;EACvD;EAEA,MAAM,OAAO;GACX,IAAI,OAAO;IACT,cAAc,KAAK;IACnB,QAAQ;GACV;GACA,IAAI,UAAU,MAAM;EACtB;EAEA,MAAM,OAAO;GAEX,OAAO,UAAU,MAAM;GACvB,WAAW,QAAQ,EAAE,cAAc;IACjC,WAAW;GACb,CAAC;GACD,OAAO;EACT;EAEA,IAAI,UAAU;GACZ,OAAO,UAAU;EACnB;CAGc;AAClB"}
@@ -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,2 @@
1
+ import { i as verifyPassword, r as hashPassword, t as basicAuth } from "./basic-DKk0Xfuu.mjs";
2
+ export { basicAuth, hashPassword, verifyPassword };
@@ -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