@xtandard/webhooks 0.1.0 → 0.1.1

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 (43) hide show
  1. package/dist/cli.cjs +4 -4
  2. package/dist/cli.mjs +4 -4
  3. package/dist/{core-CMpnmI5Q.mjs → core-C9mPi4iw.mjs} +20 -3
  4. package/dist/core-C9mPi4iw.mjs.map +1 -0
  5. package/dist/{core-ZGhH6Vs2.cjs → core-CF8U0hgi.cjs} +20 -3
  6. package/dist/core-CF8U0hgi.cjs.map +1 -0
  7. package/dist/core.cjs +1 -1
  8. package/dist/core.mjs +1 -1
  9. package/dist/{create-fetch-handler-CmooujQo.cjs → create-fetch-handler-BILaZ2Z7.cjs} +6 -5
  10. package/dist/{create-fetch-handler-CmooujQo.cjs.map → create-fetch-handler-BILaZ2Z7.cjs.map} +1 -1
  11. package/dist/{create-fetch-handler-jy3hy5nZ.d.mts → create-fetch-handler-BN9vXbgW.d.mts} +7 -3
  12. package/dist/{create-fetch-handler-BIdk9P30.mjs → create-fetch-handler-D0F3cjoq.mjs} +6 -5
  13. package/dist/{create-fetch-handler-BIdk9P30.mjs.map → create-fetch-handler-D0F3cjoq.mjs.map} +1 -1
  14. package/dist/{create-fetch-handler-Dlkhustu.d.cts → create-fetch-handler-D9ZRfrY6.d.cts} +7 -3
  15. package/dist/{dispatcher-B0xTEHt1.cjs → dispatcher-DOOCJJxM.cjs} +3 -3
  16. package/dist/{dispatcher-B0xTEHt1.cjs.map → dispatcher-DOOCJJxM.cjs.map} +1 -1
  17. package/dist/{dispatcher-Coubwrka.mjs → dispatcher-DuBrQL46.mjs} +3 -3
  18. package/dist/{dispatcher-Coubwrka.mjs.map → dispatcher-DuBrQL46.mjs.map} +1 -1
  19. package/dist/entry-bun.cjs +1 -1
  20. package/dist/entry-bun.d.cts +1 -1
  21. package/dist/entry-bun.d.mts +1 -1
  22. package/dist/entry-bun.mjs +1 -1
  23. package/dist/entry-elysia.cjs +1 -1
  24. package/dist/entry-elysia.d.cts +1 -1
  25. package/dist/entry-elysia.d.mts +1 -1
  26. package/dist/entry-elysia.mjs +1 -1
  27. package/dist/entry-express.cjs +1 -1
  28. package/dist/entry-express.d.cts +1 -1
  29. package/dist/entry-express.d.mts +1 -1
  30. package/dist/entry-express.mjs +1 -1
  31. package/dist/entry-hono.cjs +1 -1
  32. package/dist/entry-hono.d.cts +1 -1
  33. package/dist/entry-hono.d.mts +1 -1
  34. package/dist/entry-hono.mjs +1 -1
  35. package/dist/index.cjs +3 -3
  36. package/dist/index.d.cts +2 -2
  37. package/dist/index.d.mts +2 -2
  38. package/dist/index.mjs +3 -3
  39. package/dist/testing.cjs +2 -2
  40. package/dist/testing.mjs +2 -2
  41. package/package.json +1 -1
  42. package/dist/core-CMpnmI5Q.mjs.map +0 -1
  43. package/dist/core-ZGhH6Vs2.cjs.map +0 -1
@@ -61,8 +61,12 @@ declare const DEFAULT_PORTAL_ACTIONS: readonly WebhooksAction[];
61
61
  //#region src/server/create-fetch-handler.d.ts
62
62
  /** Options for the panel handler (shared by every framework adapter). */
63
63
  interface WebhooksPanelOptions {
64
- /** Control-plane store: applications, event types, endpoints, messages, audit. */
65
- storage: WebhooksStorage;
64
+ /**
65
+ * Control-plane store: applications, event types, endpoints, messages, audit.
66
+ * Required unless a prebuilt `core` is supplied (which carries its own
67
+ * storage, and its `queueStorage`, as the source of truth).
68
+ */
69
+ storage?: WebhooksStorage;
66
70
  /** Store for deliveries, attempts, and the due index. Defaults to `storage`. */
67
71
  queueStorage?: WebhooksStorage;
68
72
  /** Mount prefix, e.g. `"/webhooks"`. Default `""` (root). */
@@ -159,4 +163,4 @@ interface CreateFetchHandlerResult {
159
163
  declare function createFetchHandler(options: WebhooksPanelOptions): CreateFetchHandlerResult;
160
164
  //#endregion
161
165
  export { WebhooksPortalOptions as a, DEFAULT_PORTAL_ACTIONS as i, WebhooksPanelOptions as n, WebhooksCorsOptions as o, createFetchHandler as r, CreateFetchHandlerResult as t };
162
- //# sourceMappingURL=create-fetch-handler-Dlkhustu.d.cts.map
166
+ //# sourceMappingURL=create-fetch-handler-D9ZRfrY6.d.cts.map
@@ -1,5 +1,5 @@
1
1
  const require_keys = require("./keys-FiKpaVHX.cjs");
2
- const require_core = require("./core-ZGhH6Vs2.cjs");
2
+ const require_core = require("./core-CF8U0hgi.cjs");
3
3
  //#region src/dispatcher.ts
4
4
  /**
5
5
  * The delivery engine. Polls the due index, claims deliveries (lease-based,
@@ -60,7 +60,7 @@ function createDispatcher(core, options = {}) {
60
60
  const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;
61
61
  const autoDisable = merged.autoDisable ?? { failingForDays: 5 };
62
62
  const responseBodyLimit = merged.responseBodyLimit ?? 4096;
63
- const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.0`;
63
+ const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.1`;
64
64
  const doFetch = merged.fetch;
65
65
  const now = core.options.now;
66
66
  let timer = null;
@@ -209,4 +209,4 @@ Object.defineProperty(exports, "dispatcher_exports", {
209
209
  }
210
210
  });
211
211
 
212
- //# sourceMappingURL=dispatcher-B0xTEHt1.cjs.map
212
+ //# sourceMappingURL=dispatcher-DOOCJJxM.cjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"dispatcher-DOOCJJxM.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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"}
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
2
- import { A as attemptDelivery, w as durationToMs } from "./core-CMpnmI5Q.mjs";
2
+ import { A as attemptDelivery, w as durationToMs } from "./core-C9mPi4iw.mjs";
3
3
  import { m as dueKey, p as deliveryKey } from "./keys-Byyj4quQ.mjs";
4
4
  //#region src/dispatcher.ts
5
5
  /**
@@ -61,7 +61,7 @@ function createDispatcher(core, options = {}) {
61
61
  const schedule = merged.retrySchedule ?? DEFAULT_RETRY_SCHEDULE;
62
62
  const autoDisable = merged.autoDisable ?? { failingForDays: 5 };
63
63
  const responseBodyLimit = merged.responseBodyLimit ?? 4096;
64
- const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.0`;
64
+ const userAgent = merged.userAgent ?? `xtandard-webhooks/0.1.1`;
65
65
  const doFetch = merged.fetch;
66
66
  const now = core.options.now;
67
67
  let timer = null;
@@ -193,4 +193,4 @@ function createDispatcher(core, options = {}) {
193
193
  //#endregion
194
194
  export { createDispatcher as n, dispatcher_exports as r, DEFAULT_RETRY_SCHEDULE as t };
195
195
 
196
- //# sourceMappingURL=dispatcher-Coubwrka.mjs.map
196
+ //# sourceMappingURL=dispatcher-DuBrQL46.mjs.map
@@ -1 +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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"}
1
+ {"version":3,"file":"dispatcher-DuBrQL46.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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"}
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_fetch_handler = require("./create-fetch-handler-CmooujQo.cjs");
2
+ const require_create_fetch_handler = require("./create-fetch-handler-BILaZ2Z7.cjs");
3
3
  //#region src/adapters/bun.ts
4
4
  /**
5
5
  * Bun adapter. The handler is already web-standard, so this is a passthrough you
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, t as CreateFetchHandlerResult } from "./create-fetch-handler-Dlkhustu.cjs";
1
+ import { n as WebhooksPanelOptions, t as CreateFetchHandlerResult } from "./create-fetch-handler-D9ZRfrY6.cjs";
2
2
 
3
3
  //#region src/adapters/bun.d.ts
4
4
  /** Create a Bun-ready panel handler (`fetch` + `core` + `dispatcher` + `openapi`). */
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, t as CreateFetchHandlerResult } from "./create-fetch-handler-jy3hy5nZ.mjs";
1
+ import { n as WebhooksPanelOptions, t as CreateFetchHandlerResult } from "./create-fetch-handler-BN9vXbgW.mjs";
2
2
 
3
3
  //#region src/adapters/bun.d.ts
4
4
  /** Create a Bun-ready panel handler (`fetch` + `core` + `dispatcher` + `openapi`). */
@@ -1,4 +1,4 @@
1
- import { t as createFetchHandler } from "./create-fetch-handler-BIdk9P30.mjs";
1
+ import { t as createFetchHandler } from "./create-fetch-handler-D0F3cjoq.mjs";
2
2
  //#region src/adapters/bun.ts
3
3
  /**
4
4
  * Bun adapter. The handler is already web-standard, so this is a passthrough you
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_fetch_handler = require("./create-fetch-handler-CmooujQo.cjs");
2
+ const require_create_fetch_handler = require("./create-fetch-handler-BILaZ2Z7.cjs");
3
3
  let elysia = require("elysia");
4
4
  //#region src/adapters/elysia.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-Dlkhustu.cjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-D9ZRfrY6.cjs";
2
2
  import { Elysia } from "elysia";
3
3
 
4
4
  //#region src/adapters/elysia.d.ts
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-jy3hy5nZ.mjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-BN9vXbgW.mjs";
2
2
  import { Elysia } from "elysia";
3
3
 
4
4
  //#region src/adapters/elysia.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createFetchHandler } from "./create-fetch-handler-BIdk9P30.mjs";
1
+ import { t as createFetchHandler } from "./create-fetch-handler-D0F3cjoq.mjs";
2
2
  import { Elysia, t } from "elysia";
3
3
  //#region src/adapters/elysia.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_fetch_handler = require("./create-fetch-handler-CmooujQo.cjs");
2
+ const require_create_fetch_handler = require("./create-fetch-handler-BILaZ2Z7.cjs");
3
3
  //#region src/adapters/express.ts
4
4
  function headersFrom(req) {
5
5
  const headers = new Headers();
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-Dlkhustu.cjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-D9ZRfrY6.cjs";
2
2
  import { NextFunction, Request, Response } from "express";
3
3
 
4
4
  //#region src/adapters/express.d.ts
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-jy3hy5nZ.mjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-BN9vXbgW.mjs";
2
2
  import { NextFunction, Request, Response } from "express";
3
3
 
4
4
  //#region src/adapters/express.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createFetchHandler } from "./create-fetch-handler-BIdk9P30.mjs";
1
+ import { t as createFetchHandler } from "./create-fetch-handler-D0F3cjoq.mjs";
2
2
  //#region src/adapters/express.ts
3
3
  function headersFrom(req) {
4
4
  const headers = new Headers();
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_fetch_handler = require("./create-fetch-handler-CmooujQo.cjs");
2
+ const require_create_fetch_handler = require("./create-fetch-handler-BILaZ2Z7.cjs");
3
3
  let hono = require("hono");
4
4
  //#region src/adapters/hono.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-Dlkhustu.cjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-D9ZRfrY6.cjs";
2
2
  import { Hono } from "hono";
3
3
 
4
4
  //#region src/adapters/hono.d.ts
@@ -1,4 +1,4 @@
1
- import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-jy3hy5nZ.mjs";
1
+ import { n as WebhooksPanelOptions, r as createFetchHandler } from "./create-fetch-handler-BN9vXbgW.mjs";
2
2
  import { Hono } from "hono";
3
3
 
4
4
  //#region src/adapters/hono.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createFetchHandler } from "./create-fetch-handler-BIdk9P30.mjs";
1
+ import { t as createFetchHandler } from "./create-fetch-handler-D0F3cjoq.mjs";
2
2
  import { Hono } from "hono";
3
3
  //#region src/adapters/hono.ts
4
4
  /**
package/dist/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_keys = require("./keys-FiKpaVHX.cjs");
3
3
  const require_schema = require("./schema.cjs");
4
+ const require_core = require("./core-CF8U0hgi.cjs");
4
5
  const require_signing = require("./signing.cjs");
5
- const require_core = require("./core-ZGhH6Vs2.cjs");
6
6
  const require_contract = require("./contract-Bf1qguwt.cjs");
7
- const require_dispatcher = require("./dispatcher-B0xTEHt1.cjs");
7
+ const require_dispatcher = require("./dispatcher-DOOCJJxM.cjs");
8
8
  const require_contract$1 = require("./contract-B2d5dNU3.cjs");
9
- const require_create_fetch_handler = require("./create-fetch-handler-CmooujQo.cjs");
9
+ const require_create_fetch_handler = require("./create-fetch-handler-BILaZ2Z7.cjs");
10
10
  //#region src/storage/watch.ts
11
11
  /**
12
12
  * Wrap a storage so it implements {@link WatchableWebhooksStorage} using
package/dist/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { a as WatchableWebhooksStorage, c as isCompareAndSwap, d as requirePeer,
4
4
  import { a as HookDeniedError, c as WebhooksHooksInput, d as runAfter, f as runBefore, i as BeforeEventType, l as defaultHookErrorReporter, n as AfterEventType, o as HookErrorReporter, r as BeforeEvent, s as WebhooksHooks, t as AfterEvent, u as normalizeHooks } from "./contract-Bnb3fgRJ.cjs";
5
5
  import { n as Principal, t as AuthProvider } from "./contract-lETlIuXo.cjs";
6
6
  import { a as WebhooksResource, i as WebhooksAction, n as AuthorizeInput, o as isMutatingAction, r as MUTATING_ACTIONS, t as AuthorizationProvider } from "./contract-CiPskNvS.cjs";
7
- import { a as WebhooksPortalOptions, i as DEFAULT_PORTAL_ACTIONS, n as WebhooksPanelOptions, o as WebhooksCorsOptions, r as createFetchHandler, t as CreateFetchHandlerResult } from "./create-fetch-handler-Dlkhustu.cjs";
7
+ import { a as WebhooksPortalOptions, i as DEFAULT_PORTAL_ACTIONS, n as WebhooksPanelOptions, o as WebhooksCorsOptions, r as createFetchHandler, t as CreateFetchHandlerResult } from "./create-fetch-handler-D9ZRfrY6.cjs";
8
8
  import { SECRET_PREFIX, VerifyInput, WebhookVerificationError, generateSecret, sign, signatureHeader, verify } from "./signing.cjs";
9
9
 
10
10
  //#region \0rolldown/runtime.js
@@ -17,7 +17,7 @@ import { SECRET_PREFIX, VerifyInput, WebhookVerificationError, generateSecret, s
17
17
  * @module
18
18
  */
19
19
  /** The published package version. */
20
- declare const VERSION = "0.1.0";
20
+ declare const VERSION = "0.1.1";
21
21
  declare namespace keys_d_exports {
22
22
  export { DueEntry, RESERVED_APPLICATION_KEYS, ROOT, applicationMetaKey, applicationPrefix, applicationsKey, attemptKey, attemptsPrefix, auditLogKey, byEndpointKey, byEndpointPrefix, byMessageKey, byMessagePrefix, deliveriesPrefix, deliveryKey, dueKey, duePrefix, endpointKey, endpointsKey, eventTypeKey, eventTypesKey, globalAuditLogKey, idempotencyKey, lastSegment, messageKey, messagesPrefix, parseDueKey };
23
23
  }
package/dist/index.d.mts CHANGED
@@ -4,7 +4,7 @@ import { a as WatchableWebhooksStorage, c as isCompareAndSwap, d as requirePeer,
4
4
  import { a as HookDeniedError, c as WebhooksHooksInput, d as runAfter, f as runBefore, i as BeforeEventType, l as defaultHookErrorReporter, n as AfterEventType, o as HookErrorReporter, r as BeforeEvent, s as WebhooksHooks, t as AfterEvent, u as normalizeHooks } from "./contract-T1kcZNdG.mjs";
5
5
  import { n as Principal, t as AuthProvider } from "./contract-lETlIuXo.mjs";
6
6
  import { a as WebhooksResource, i as WebhooksAction, n as AuthorizeInput, o as isMutatingAction, r as MUTATING_ACTIONS, t as AuthorizationProvider } from "./contract-C2r2Xzwp.mjs";
7
- import { a as WebhooksPortalOptions, i as DEFAULT_PORTAL_ACTIONS, n as WebhooksPanelOptions, o as WebhooksCorsOptions, r as createFetchHandler, t as CreateFetchHandlerResult } from "./create-fetch-handler-jy3hy5nZ.mjs";
7
+ import { a as WebhooksPortalOptions, i as DEFAULT_PORTAL_ACTIONS, n as WebhooksPanelOptions, o as WebhooksCorsOptions, r as createFetchHandler, t as CreateFetchHandlerResult } from "./create-fetch-handler-BN9vXbgW.mjs";
8
8
  import { SECRET_PREFIX, VerifyInput, WebhookVerificationError, generateSecret, sign, signatureHeader, verify } from "./signing.mjs";
9
9
 
10
10
  //#region src/version.d.ts
@@ -15,7 +15,7 @@ import { SECRET_PREFIX, VerifyInput, WebhookVerificationError, generateSecret, s
15
15
  * @module
16
16
  */
17
17
  /** The published package version. */
18
- declare const VERSION = "0.1.0";
18
+ declare const VERSION = "0.1.1";
19
19
  declare namespace keys_d_exports {
20
20
  export { DueEntry, RESERVED_APPLICATION_KEYS, ROOT, applicationMetaKey, applicationPrefix, applicationsKey, attemptKey, attemptsPrefix, auditLogKey, byEndpointKey, byEndpointPrefix, byMessageKey, byMessagePrefix, deliveriesPrefix, deliveryKey, dueKey, duePrefix, endpointKey, endpointsKey, eventTypeKey, eventTypesKey, globalAuditLogKey, idempotencyKey, lastSegment, messageKey, messagesPrefix, parseDueKey };
21
21
  }
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { SCHEMA_VERSION, isTerminalDeliveryStatus } from "./schema.mjs";
2
+ import { A as attemptDelivery, C as newId, D as DEFAULT_ATTEMPT_TIMEOUT_MS, E as emitDelivery, O as DEFAULT_RESPONSE_BODY_LIMIT, T as parseDurationList, _ as normalizeHooks, a as ReadonlyError, b as VERSION, c as RESERVED_HEADERS, d as validateApplication, f as validateEndpoint, g as defaultHookErrorReporter, h as HookDeniedError, i as PayloadTooLargeError, j as buildSignedRequest, k as activeSecrets, l as ValidationError, m as validateEventType, n as IdempotencyConflictError, o as createWebhooksCore, p as validateEndpointUrl, r as NotFoundError, s as KEY_REGEX, t as ConflictError, u as assertValid, v as runAfter, w as durationToMs, x as idPattern, y as runBefore } from "./core-C9mPi4iw.mjs";
2
3
  import { SECRET_PREFIX, WebhookVerificationError, generateSecret, sign, signatureHeader, verify } from "./signing.mjs";
3
- import { A as attemptDelivery, C as newId, D as DEFAULT_ATTEMPT_TIMEOUT_MS, E as emitDelivery, O as DEFAULT_RESPONSE_BODY_LIMIT, T as parseDurationList, _ as normalizeHooks, a as ReadonlyError, b as VERSION, c as RESERVED_HEADERS, d as validateApplication, f as validateEndpoint, g as defaultHookErrorReporter, h as HookDeniedError, i as PayloadTooLargeError, j as buildSignedRequest, k as activeSecrets, l as ValidationError, m as validateEventType, n as IdempotencyConflictError, o as createWebhooksCore, p as validateEndpointUrl, r as NotFoundError, s as KEY_REGEX, t as ConflictError, u as assertValid, v as runAfter, w as durationToMs, x as idPattern, y as runBefore } from "./core-CMpnmI5Q.mjs";
4
4
  import { x as keys_exports } from "./keys-Byyj4quQ.mjs";
5
5
  import { a as requirePeer, i as isWatchable, n as isCompareAndSwap, r as isTransactional, t as hasDeliveryQueue } from "./contract-BEhDcd_5.mjs";
6
- import { n as createDispatcher, t as DEFAULT_RETRY_SCHEDULE } from "./dispatcher-Coubwrka.mjs";
6
+ import { n as createDispatcher, t as DEFAULT_RETRY_SCHEDULE } from "./dispatcher-DuBrQL46.mjs";
7
7
  import { n as isMutatingAction, t as MUTATING_ACTIONS } from "./contract-9XpcwcCn.mjs";
8
- import { a as PORTAL_TOKEN_PREFIX, c as verifyPortalToken, i as buildOpenApiDocument, o as PortalTokenError, r as DEFAULT_PORTAL_ACTIONS, s as createPortalToken, t as createFetchHandler } from "./create-fetch-handler-BIdk9P30.mjs";
8
+ import { a as PORTAL_TOKEN_PREFIX, c as verifyPortalToken, i as buildOpenApiDocument, o as PortalTokenError, r as DEFAULT_PORTAL_ACTIONS, s as createPortalToken, t as createFetchHandler } from "./create-fetch-handler-D0F3cjoq.mjs";
9
9
  //#region src/storage/watch.ts
10
10
  /**
11
11
  * Wrap a storage so it implements {@link WatchableWebhooksStorage} using
package/dist/testing.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_core = require("./core-CF8U0hgi.cjs");
2
3
  const require_signing = require("./signing.cjs");
3
- const require_core = require("./core-ZGhH6Vs2.cjs");
4
- const require_dispatcher = require("./dispatcher-B0xTEHt1.cjs");
4
+ const require_dispatcher = require("./dispatcher-DOOCJJxM.cjs");
5
5
  const require_memory = require("./memory-8Ef-PL5a.cjs");
6
6
  let node_http = require("node:http");
7
7
  //#region src/testing.ts
package/dist/testing.mjs CHANGED
@@ -1,6 +1,6 @@
1
+ import { o as createWebhooksCore } from "./core-C9mPi4iw.mjs";
1
2
  import { verify } from "./signing.mjs";
2
- import { o as createWebhooksCore } from "./core-CMpnmI5Q.mjs";
3
- import { n as createDispatcher } from "./dispatcher-Coubwrka.mjs";
3
+ import { n as createDispatcher } from "./dispatcher-DuBrQL46.mjs";
4
4
  import { t as createMemoryStorage } from "./memory-BMsSSwqn.mjs";
5
5
  import { createServer } from "node:http";
6
6
  //#region src/testing.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtandard/webhooks",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Self-hosted, embeddable, Standard Webhooks-compliant outbound webhook control plane with pluggable storage, an in-process delivery engine, and an embeddable consumer portal.",
5
5
  "keywords": [
6
6
  "bun",