@xtandard/webhooks 0.1.1 → 0.1.3

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 (82) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.cjs +4 -4
  3. package/dist/cli.mjs +4 -4
  4. package/dist/{core-CF8U0hgi.cjs → core-DkIXozBY.cjs} +2 -2
  5. package/dist/{core-CF8U0hgi.cjs.map → core-DkIXozBY.cjs.map} +1 -1
  6. package/dist/{core-C9mPi4iw.mjs → core-tmmtadF1.mjs} +2 -2
  7. package/dist/{core-C9mPi4iw.mjs.map → core-tmmtadF1.mjs.map} +1 -1
  8. package/dist/core.cjs +1 -1
  9. package/dist/core.mjs +1 -1
  10. package/dist/{create-fetch-handler-BILaZ2Z7.cjs → create-fetch-handler-B5WBbf8x.cjs} +3 -3
  11. package/dist/{create-fetch-handler-BILaZ2Z7.cjs.map → create-fetch-handler-B5WBbf8x.cjs.map} +1 -1
  12. package/dist/{create-fetch-handler-D0F3cjoq.mjs → create-fetch-handler-DZohI08M.mjs} +3 -3
  13. package/dist/{create-fetch-handler-D0F3cjoq.mjs.map → create-fetch-handler-DZohI08M.mjs.map} +1 -1
  14. package/dist/{dispatcher-DOOCJJxM.cjs → dispatcher-Bn4O874S.cjs} +3 -3
  15. package/dist/{dispatcher-DOOCJJxM.cjs.map → dispatcher-Bn4O874S.cjs.map} +1 -1
  16. package/dist/{dispatcher-DuBrQL46.mjs → dispatcher-CQcoIsS9.mjs} +3 -3
  17. package/dist/{dispatcher-DuBrQL46.mjs.map → dispatcher-CQcoIsS9.mjs.map} +1 -1
  18. package/dist/elysia-shared-CgTU3yPa.mjs +47 -0
  19. package/dist/elysia-shared-CgTU3yPa.mjs.map +1 -0
  20. package/dist/elysia-shared-Cop1-ie0.cjs +58 -0
  21. package/dist/elysia-shared-Cop1-ie0.cjs.map +1 -0
  22. package/dist/elysia-shared-CvSsqMbR.d.mts +19 -0
  23. package/dist/elysia-shared-DSSrfuoF.d.cts +19 -0
  24. package/dist/entry-bun.cjs +1 -1
  25. package/dist/entry-bun.mjs +1 -1
  26. package/dist/entry-elysia.cjs +5 -25
  27. package/dist/entry-elysia.cjs.map +1 -1
  28. package/dist/entry-elysia.d.cts +5 -15
  29. package/dist/entry-elysia.d.mts +5 -15
  30. package/dist/entry-elysia.mjs +4 -24
  31. package/dist/entry-elysia.mjs.map +1 -1
  32. package/dist/entry-elysia2.cjs +95 -0
  33. package/dist/entry-elysia2.cjs.map +1 -0
  34. package/dist/entry-elysia2.d.cts +1273 -0
  35. package/dist/entry-elysia2.d.mts +1273 -0
  36. package/dist/entry-elysia2.mjs +93 -0
  37. package/dist/entry-elysia2.mjs.map +1 -0
  38. package/dist/entry-express.cjs +1 -1
  39. package/dist/entry-express.mjs +1 -1
  40. package/dist/entry-hono.cjs +1 -1
  41. package/dist/entry-hono.mjs +1 -1
  42. package/dist/index.cjs +3 -3
  43. package/dist/index.d.cts +1 -1
  44. package/dist/index.d.mts +1 -1
  45. package/dist/index.mjs +3 -3
  46. package/dist/react.css +1 -1
  47. package/dist/react.js +2522 -2506
  48. package/dist/testing.cjs +2 -2
  49. package/dist/testing.mjs +2 -2
  50. package/dist/types-elysia2/adapters/elysia-shared.d.ts +34 -0
  51. package/dist/types-elysia2/adapters/elysia2.d.ts +820 -0
  52. package/dist/types-elysia2/auth/contract.d.ts +26 -0
  53. package/dist/types-elysia2/authorization/contract.d.ts +50 -0
  54. package/dist/types-elysia2/canonical.d.ts +27 -0
  55. package/dist/types-elysia2/core.d.ts +344 -0
  56. package/dist/types-elysia2/deliver.d.ts +79 -0
  57. package/dist/types-elysia2/delivery-sink.d.ts +57 -0
  58. package/dist/types-elysia2/dispatcher.d.ts +80 -0
  59. package/dist/types-elysia2/duration.d.ts +26 -0
  60. package/dist/types-elysia2/entry-elysia2.d.ts +8 -0
  61. package/dist/types-elysia2/hooks/contract.d.ts +203 -0
  62. package/dist/types-elysia2/id.d.ts +24 -0
  63. package/dist/types-elysia2/keys.d.ts +98 -0
  64. package/dist/types-elysia2/portal.d.ts +69 -0
  65. package/dist/types-elysia2/schema.d.ts +229 -0
  66. package/dist/types-elysia2/server/base-path.d.ts +15 -0
  67. package/dist/types-elysia2/server/cors.d.ts +36 -0
  68. package/dist/types-elysia2/server/create-fetch-handler.d.ts +121 -0
  69. package/dist/types-elysia2/server/openapi.d.ts +25 -0
  70. package/dist/types-elysia2/server/render-index-html.d.ts +18 -0
  71. package/dist/types-elysia2/server/routes.d.ts +52 -0
  72. package/dist/types-elysia2/server/static-assets.d.ts +13 -0
  73. package/dist/types-elysia2/signing.d.ts +88 -0
  74. package/dist/types-elysia2/storage/contract.d.ts +75 -0
  75. package/dist/types-elysia2/validation.d.ts +82 -0
  76. package/dist/types-elysia2/version.d.ts +8 -0
  77. package/dist/types-react/ui/lib/portal-container.d.ts +24 -0
  78. package/dist/ui/assets/{index-S5t_CLOe.js → index-BgyX_njf.js} +44 -44
  79. package/dist/ui/assets/index-mJCJq_EB.css +1 -0
  80. package/dist/ui/index.html +2 -2
  81. package/package.json +23 -6
  82. package/dist/ui/assets/index-B0eoQX2U.css +0 -1
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Authentication contract. Answers: "Who is this request from?"
3
+ *
4
+ * `@xtandard/webhooks` ships `none`/`basic`/`delegated` implementations, but any
5
+ * object satisfying {@link AuthProvider} works — bring your own.
6
+ *
7
+ * @module
8
+ */
9
+ /** An authenticated identity. */
10
+ export interface Principal {
11
+ id: string;
12
+ email?: string;
13
+ name?: string;
14
+ roles?: string[];
15
+ metadata?: unknown;
16
+ }
17
+ /** Authenticates an incoming web-standard {@link Request}. */
18
+ export interface AuthProvider {
19
+ /** Resolve the principal, or `null` if the request is unauthenticated. */
20
+ authenticate(request: Request): Promise<Principal | null>;
21
+ /**
22
+ * Optional: build a `Response` that prompts for credentials (e.g. a 401 with a
23
+ * `WWW-Authenticate` header). When omitted, the server returns a plain 401.
24
+ */
25
+ challenge?(request: Request): Response | undefined;
26
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Authorization contract. Answers: "Can this principal perform this action on
3
+ * this resource?"
4
+ *
5
+ * Every mutating admin API route consults the {@link AuthorizationProvider}.
6
+ * Ships `none`/`roles`/`delegated` implementations.
7
+ *
8
+ * @module
9
+ */
10
+ import type { Principal } from "../auth/contract.ts";
11
+ /** The full set of authorizable actions. */
12
+ export type WebhooksAction = "application:read" | "application:create" | "application:update" | "application:delete" | "event-type:read" | "event-type:create" | "event-type:update" | "event-type:delete" | "endpoint:read" | "endpoint:create" | "endpoint:update" | "endpoint:delete" | "endpoint:rotate-secret" | "endpoint:read-secret" | "message:read" | "message:publish" | "delivery:read" | "delivery:retry" | "audit:read";
13
+ /** The resource an action targets. */
14
+ export type WebhooksResource = {
15
+ type: "application";
16
+ applicationKey: string;
17
+ } | {
18
+ type: "event-type";
19
+ name: string;
20
+ } | {
21
+ type: "endpoint";
22
+ applicationKey: string;
23
+ endpointId: string;
24
+ } | {
25
+ type: "message";
26
+ applicationKey: string;
27
+ messageId?: string;
28
+ } | {
29
+ type: "delivery";
30
+ applicationKey: string;
31
+ deliveryId?: string;
32
+ } | {
33
+ type: "audit";
34
+ applicationKey?: string;
35
+ };
36
+ /** Input passed to {@link AuthorizationProvider.authorize}. */
37
+ export interface AuthorizeInput {
38
+ principal: Principal | null;
39
+ action: WebhooksAction;
40
+ resource: WebhooksResource;
41
+ request: Request;
42
+ }
43
+ /** Decides whether an action is permitted. */
44
+ export interface AuthorizationProvider {
45
+ authorize(input: AuthorizeInput): Promise<boolean>;
46
+ }
47
+ /** Actions that mutate state — blocked in readonly mode. */
48
+ export declare const MUTATING_ACTIONS: ReadonlySet<WebhooksAction>;
49
+ /** True if the action mutates state. */
50
+ export declare const isMutatingAction: (action: WebhooksAction) => boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Canonical JSON serialization: object keys sorted recursively, so two values
3
+ * that are structurally equal produce byte-identical output regardless of key
4
+ * insertion order.
5
+ *
6
+ * Used for the idempotency comparison in {@link ./core.WebhooksCore.publish}:
7
+ * some control-plane stores canonicalize object key order on round-trip
8
+ * (Postgres `jsonb`, some BSON paths), so a stored payload read back may have a
9
+ * different key order than the freshly-published one. An order-sensitive
10
+ * comparison would then flag an identical re-publish as an
11
+ * `IdempotencyConflictError`. Comparing canonical forms fixes it uniformly,
12
+ * independent of the storage adapter.
13
+ *
14
+ * @module
15
+ */
16
+ import type { JsonValue } from "./schema.ts";
17
+ /**
18
+ * Serialize a JSON value with object keys sorted lexicographically at every
19
+ * depth. Arrays keep their order (order is semantic for arrays); `undefined`
20
+ * inside objects is dropped exactly as `JSON.stringify` would.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * canonicalStringify({ b: 1, a: 2 }) === canonicalStringify({ a: 2, b: 1 }); // true
25
+ * ```
26
+ */
27
+ export declare function canonicalStringify(value: JsonValue): string;
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Admin + publish core — the operations layer the API, CLI, and dispatcher sit
3
+ * on top of.
4
+ *
5
+ * Owns the split between the **control plane** (CRUD on applications / event
6
+ * types / endpoints, browsing messages and deliveries, replay — rare,
7
+ * human-driven, hook-guarded, audited) and the **delivery plane** (`publish()`
8
+ * + the dispatcher's claim/record internals — the hot path). `publish()` never
9
+ * performs an HTTP call and never throws because an endpoint is down: it
10
+ * persists one message and fans out one pending delivery per matching enabled
11
+ * endpoint; the dispatcher owns all network I/O.
12
+ *
13
+ * Storage can be split: control data lives in `storage`, while deliveries +
14
+ * the due index live in `queueStorage` (defaults to `storage`) — e.g. control
15
+ * in Postgres, queue in Redis.
16
+ *
17
+ * @module
18
+ */
19
+ import { type AttemptOutcome, type SignedRequest } from "./deliver.ts";
20
+ import { type DeliveryErrorReporter, type DeliveryListener } from "./delivery-sink.ts";
21
+ import type { DispatcherOptions } from "./dispatcher.ts";
22
+ import type { Actor, Application, AuditEntry, Delivery, DeliveryAttempt, Endpoint, EventType, JsonValue, Message, WebhookDuration } from "./schema.ts";
23
+ import { type WebhooksStorage } from "./storage/contract.ts";
24
+ import type { HookErrorReporter, WebhooksHooks, WebhooksHooksInput } from "./hooks/contract.ts";
25
+ /** Thrown by mutating operations when the core is in readonly mode. */
26
+ export declare class ReadonlyError extends Error {
27
+ constructor(operation: string);
28
+ }
29
+ /** Thrown when a referenced application/event type/endpoint/delivery does not exist. */
30
+ export declare class NotFoundError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ /** Thrown when creating an entity whose key already exists. Maps to HTTP 409. */
34
+ export declare class ConflictError extends Error {
35
+ constructor(message: string);
36
+ }
37
+ /** Thrown by {@link WebhooksCore.publish} when the payload exceeds the limit. Maps to 413. */
38
+ export declare class PayloadTooLargeError extends Error {
39
+ constructor(size: number, limit: number);
40
+ }
41
+ /**
42
+ * Thrown by {@link WebhooksCore.publish} when an idempotency key is reused with
43
+ * a **different** payload (same key + same payload returns the original
44
+ * message instead). Maps to HTTP 409.
45
+ */
46
+ export declare class IdempotencyConflictError extends Error {
47
+ constructor(key: string);
48
+ }
49
+ /**
50
+ * One retention rule set. When both fields are set, an item is **kept if either
51
+ * rule keeps it** (union of keeps — the same semantics as backup tools): pruned
52
+ * only when it is outside the `keepLast` most recent AND older than `maxAge`.
53
+ */
54
+ export interface RetentionRule {
55
+ /** Keep at most the N most recent items. */
56
+ keepLast?: number;
57
+ /** Keep items newer than this age (relative to prune time). */
58
+ maxAge?: WebhookDuration;
59
+ }
60
+ /** Retention policy for {@link WebhooksCoreOptions.retention}. */
61
+ export interface RetentionOptions {
62
+ /**
63
+ * Prune old messages, cascading their deliveries and attempts. Messages with
64
+ * a non-terminal delivery are never pruned (the dispatcher still needs them).
65
+ */
66
+ messages?: RetentionRule;
67
+ /** Prune old audit entries. */
68
+ audit?: RetentionRule;
69
+ }
70
+ /** Options for {@link createWebhooksCore}. */
71
+ export interface WebhooksCoreOptions {
72
+ /** Control-plane store: applications, event types, endpoints, messages, audit. */
73
+ storage: WebhooksStorage;
74
+ /**
75
+ * Store for deliveries, attempts, and the due index. Defaults to `storage`.
76
+ * Splitting lets control data live in Postgres while the queue lives in Redis.
77
+ */
78
+ queueStorage?: WebhooksStorage;
79
+ /** When true, all mutating operations throw {@link ReadonlyError}. */
80
+ readonly?: boolean;
81
+ /**
82
+ * Control-plane hooks fired around admin mutations. Pass one hook or an array.
83
+ * `before` hooks run sequentially and may **throw to deny**; `after` hooks run
84
+ * post-commit for side effects and never fail the operation.
85
+ */
86
+ hooks?: WebhooksHooksInput;
87
+ /** Reporter invoked when an `after` hook throws. Defaults to `console.warn`. */
88
+ onHookError?: HookErrorReporter;
89
+ /** Fire-and-forget sink invoked for **every** delivery attempt (metrics tap). */
90
+ onDelivery?: DeliveryListener;
91
+ /** Reporter invoked when the `onDelivery` sink throws/rejects. */
92
+ onDeliveryError?: DeliveryErrorReporter;
93
+ /**
94
+ * Retention policy. Message pruning cascades deliveries/attempts and is
95
+ * non-vetoable; removed payloads are surfaced to `after` hooks
96
+ * (`message.pruned`, `audit.pruned`) so they can be offloaded first. Pruning
97
+ * runs opportunistically after publishes (off the hot path, fire-and-forget)
98
+ * and on demand via {@link WebhooksCore.prune}.
99
+ */
100
+ retention?: RetentionOptions;
101
+ /**
102
+ * Dispatcher configuration, carried here so panels/CLI construct dispatchers
103
+ * consistently. The core itself never starts one — creation is the
104
+ * panel/CLI's job (or yours, via `createDispatcher(core, core.options.dispatcher)`).
105
+ */
106
+ dispatcher?: DispatcherOptions;
107
+ /** Grace window during which a rotated-out secret keeps signing. Default `"24h"`. */
108
+ secretRotationGrace?: WebhookDuration;
109
+ /** Allow `http:` endpoint URLs beyond localhost (dev only; default `false`). */
110
+ allowInsecureUrls?: boolean;
111
+ /** Extra endpoint-URL gate; return `false` to reject (SSRF denylist etc.). */
112
+ urlPolicy?: (url: string) => boolean;
113
+ /** Max serialized payload size accepted by publish. Default `262_144` bytes. */
114
+ payloadLimitBytes?: number;
115
+ /**
116
+ * Whether {@link WebhooksCore.publish} requires the event type to exist in
117
+ * the catalog. Default `true` — a typo'd event type name would otherwise
118
+ * silently deliver to nobody (endpoints subscribe by exact name).
119
+ */
120
+ requireKnownEventTypes?: boolean;
121
+ /** Injectable clock (unix millis) for tests. Default `Date.now`. */
122
+ now?: () => number;
123
+ }
124
+ /** Per-call actor attribution for audited mutations. */
125
+ export interface ActorOptions {
126
+ actor?: Actor | null;
127
+ }
128
+ /** Input to {@link WebhooksCore.publish}. */
129
+ export interface PublishInput {
130
+ eventType: string;
131
+ payload: JsonValue;
132
+ /** ISO-8601 event-occurred time. Defaults to publish time. */
133
+ timestamp?: string;
134
+ /** Dedupe key: same key + same payload within retention returns the existing message. */
135
+ idempotencyKey?: string;
136
+ }
137
+ /** Result of {@link WebhooksCore.publish}. */
138
+ export interface PublishResult {
139
+ message: Message;
140
+ deliveries: Delivery[];
141
+ /** True when an idempotency key matched and the existing message was returned. */
142
+ deduplicated: boolean;
143
+ }
144
+ /** Pagination + filters for {@link WebhooksCore.listMessages}. */
145
+ export interface ListMessagesOptions {
146
+ /** Page size. Default 50, max 200. */
147
+ limit?: number;
148
+ /** Cursor: return items strictly older than the message with this id. */
149
+ before?: string;
150
+ eventType?: string;
151
+ }
152
+ /** Pagination + filters for {@link WebhooksCore.listDeliveries}. */
153
+ export interface ListDeliveriesOptions {
154
+ status?: Delivery["status"];
155
+ endpointId?: string;
156
+ messageId?: string;
157
+ /** Page size. Default 50, max 200. */
158
+ limit?: number;
159
+ /** Cursor: return items strictly older than the delivery with this id. */
160
+ before?: string;
161
+ }
162
+ /** Input to the dispatcher-facing {@link WebhooksCore.claimDueDeliveries}. */
163
+ export interface ClaimInput {
164
+ limit: number;
165
+ leaseMs: number;
166
+ }
167
+ /** Input to the dispatcher-facing {@link WebhooksCore.recordAttempt}. */
168
+ export interface RecordAttemptInput {
169
+ delivery: Delivery;
170
+ outcome: AttemptOutcome;
171
+ trigger: DeliveryAttempt["trigger"];
172
+ /**
173
+ * When the outcome failed: the next attempt time (dispatcher computes the
174
+ * schedule + jitter), or `null` when the schedule is exhausted (dead-letter).
175
+ * Ignored for successful outcomes.
176
+ */
177
+ nextAttemptAt?: string | null;
178
+ /** The message's event type (for the sink event); loaded by the dispatcher anyway. */
179
+ eventType: string;
180
+ }
181
+ /** Result of {@link WebhooksCore.sendExample}. */
182
+ export interface SendExampleResult {
183
+ outcome: AttemptOutcome;
184
+ /** The envelope body that was sent. */
185
+ body: string;
186
+ /** The synthetic message id used as `webhook-id` (not retained). */
187
+ messageId: string;
188
+ }
189
+ /** Result of {@link WebhooksCore.recoverEndpoint}. */
190
+ export interface RecoverResult {
191
+ /** Ids of the failed deliveries that were re-queued. */
192
+ deliveryIds: string[];
193
+ }
194
+ /** The admin + publish core surface. */
195
+ export interface WebhooksCore {
196
+ readonly options: {
197
+ storage: WebhooksStorage;
198
+ queueStorage: WebhooksStorage;
199
+ readonly: boolean;
200
+ /** Normalized hooks (always an array). */
201
+ hooks: WebhooksHooks[];
202
+ retention?: RetentionOptions;
203
+ dispatcher?: DispatcherOptions;
204
+ secretRotationGrace: WebhookDuration;
205
+ allowInsecureUrls: boolean;
206
+ urlPolicy?: (url: string) => boolean;
207
+ payloadLimitBytes: number;
208
+ requireKnownEventTypes: boolean;
209
+ onDelivery?: DeliveryListener;
210
+ onDeliveryError?: DeliveryErrorReporter;
211
+ now: () => number;
212
+ };
213
+ listApplications(): Promise<Application[]>;
214
+ createApplication(input: {
215
+ key: string;
216
+ name?: string;
217
+ metadata?: JsonValue;
218
+ }, options?: ActorOptions): Promise<Application>;
219
+ getApplication(applicationKey: string): Promise<Application | null>;
220
+ updateApplication(applicationKey: string, patch: {
221
+ name?: string;
222
+ metadata?: JsonValue;
223
+ }, options?: ActorOptions): Promise<Application>;
224
+ /** Deletes the application and **everything** under it (endpoints, messages, deliveries). */
225
+ deleteApplication(applicationKey: string, options?: ActorOptions): Promise<void>;
226
+ listEventTypes(): Promise<EventType[]>;
227
+ getEventType(name: string): Promise<EventType | null>;
228
+ upsertEventType(input: EventType, options?: ActorOptions): Promise<EventType>;
229
+ /**
230
+ * Delete an event type. Endpoints referencing it keep their subscription
231
+ * entry and simply stop matching new publishes of that name.
232
+ */
233
+ deleteEventType(name: string, options?: ActorOptions): Promise<void>;
234
+ listEndpoints(applicationKey: string): Promise<Endpoint[]>;
235
+ getEndpoint(applicationKey: string, endpointId: string): Promise<Endpoint | null>;
236
+ /** Creates the endpoint and generates its first signing secret. */
237
+ createEndpoint(applicationKey: string, input: {
238
+ url: string;
239
+ description?: string;
240
+ eventTypes?: string[];
241
+ headers?: Record<string, string>;
242
+ metadata?: JsonValue;
243
+ disabled?: boolean;
244
+ }, options?: ActorOptions): Promise<Endpoint>;
245
+ updateEndpoint(applicationKey: string, endpointId: string, patch: {
246
+ url?: string;
247
+ description?: string;
248
+ eventTypes?: string[];
249
+ headers?: Record<string, string>;
250
+ metadata?: JsonValue;
251
+ }, options?: ActorOptions): Promise<Endpoint>;
252
+ deleteEndpoint(applicationKey: string, endpointId: string, options?: ActorOptions): Promise<void>;
253
+ /**
254
+ * Mint a new current secret; the previous one keeps verifying until
255
+ * `secretRotationGrace` elapses. Expired grace secrets are pruned lazily.
256
+ */
257
+ rotateSecret(applicationKey: string, endpointId: string, options?: ActorOptions): Promise<Endpoint>;
258
+ /** The endpoint's secrets (current first). Gate behind `endpoint:read-secret`. */
259
+ getSecrets(applicationKey: string, endpointId: string): Promise<Endpoint["secrets"]>;
260
+ enableEndpoint(applicationKey: string, endpointId: string, options?: ActorOptions): Promise<Endpoint>;
261
+ disableEndpoint(applicationKey: string, endpointId: string, options?: ActorOptions): Promise<Endpoint>;
262
+ /**
263
+ * Persist a message and fan out one pending delivery per matching enabled
264
+ * endpoint. Never performs HTTP; never throws because an endpoint is down.
265
+ */
266
+ publish(applicationKey: string, input: PublishInput, options?: ActorOptions): Promise<PublishResult>;
267
+ listMessages(applicationKey: string, options?: ListMessagesOptions): Promise<Message[]>;
268
+ getMessage(applicationKey: string, messageId: string): Promise<Message | null>;
269
+ listDeliveries(applicationKey: string, options?: ListDeliveriesOptions): Promise<Delivery[]>;
270
+ getDelivery(applicationKey: string, deliveryId: string): Promise<{
271
+ delivery: Delivery;
272
+ attempts: DeliveryAttempt[];
273
+ } | null>;
274
+ /**
275
+ * The exact signed HTTP request this delivery would send **right now** —
276
+ * method, URL, all headers (including the computed `webhook-signature`), and
277
+ * body. The inspector view: shows what a receiver gets, without sending.
278
+ * Returns `null` if the delivery or its endpoint no longer exists. The
279
+ * `webhook-timestamp`/signature reflect the current time (they change per
280
+ * attempt by design); the id and body are stable.
281
+ */
282
+ previewDeliveryRequest(applicationKey: string, deliveryId: string): Promise<SignedRequest | null>;
283
+ /** Re-queue a dead-lettered delivery (`failed` → `pending`, due immediately). */
284
+ retryDelivery(applicationKey: string, deliveryId: string, options?: ActorOptions): Promise<Delivery>;
285
+ /** Re-queue every failed delivery for an endpoint created at/after `since`. */
286
+ recoverEndpoint(applicationKey: string, endpointId: string, input: {
287
+ since: string;
288
+ }, options?: ActorOptions): Promise<RecoverResult>;
289
+ /**
290
+ * Fire a one-off signed test delivery at an endpoint, synchronously, through
291
+ * the same wire path as real deliveries (`trigger: "test"`). The synthetic
292
+ * message is not retained.
293
+ */
294
+ sendExample(applicationKey: string, endpointId: string, input: {
295
+ eventType: string;
296
+ payload?: JsonValue;
297
+ }, options?: ActorOptions): Promise<SendExampleResult>;
298
+ /** Newest-first. With `applicationKey`: that app's log; without: global + all apps. */
299
+ listAudit(applicationKey?: string): Promise<AuditEntry[]>;
300
+ /** Run retention now (also runs opportunistically after publishes). */
301
+ prune(): Promise<void>;
302
+ /**
303
+ * Claim up to `limit` due deliveries for exclusive processing. Uses the
304
+ * storage's native `claimDue` when available; otherwise scans the due index
305
+ * with compare-and-swap claiming (plain read-modify-write when the storage
306
+ * has no CAS — single-dispatcher assumption, see docs/DELIVERY.md).
307
+ */
308
+ claimDueDeliveries(input: ClaimInput): Promise<Delivery[]>;
309
+ /** Record one attempt's outcome and advance the delivery's state machine. */
310
+ recordAttempt(input: RecordAttemptInput): Promise<Delivery>;
311
+ /**
312
+ * Update the endpoint's failure streak after an attempt (any success clears
313
+ * it) and auto-disable once the streak exceeds `failingForDays`. Returns the
314
+ * endpoint when it was auto-disabled by this call.
315
+ */
316
+ noteEndpointOutcome(applicationKey: string, endpointId: string, ok: boolean, policy?: {
317
+ failingForDays?: number;
318
+ } | false): Promise<Endpoint | null>;
319
+ }
320
+ /**
321
+ * Construct the core over the configured storage.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * import { createWebhooksCore } from "@xtandard/webhooks";
326
+ * import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";
327
+ *
328
+ * const core = createWebhooksCore({ storage: createMemoryStorage() });
329
+ *
330
+ * await core.createApplication({ key: "acme" });
331
+ * await core.upsertEventType({ name: "invoice.paid" });
332
+ * const endpoint = await core.createEndpoint("acme", {
333
+ * url: "https://api.acme-customer.com/webhooks",
334
+ * eventTypes: ["invoice.paid"],
335
+ * });
336
+ *
337
+ * // The hot path — call this from your app code:
338
+ * await core.publish("acme", {
339
+ * eventType: "invoice.paid",
340
+ * payload: { invoiceId: "inv_1", amount: 4200 },
341
+ * });
342
+ * ```
343
+ */
344
+ export declare function createWebhooksCore(options: WebhooksCoreOptions): WebhooksCore;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The single-attempt HTTP delivery primitive: build the Standard Webhooks
3
+ * headers, POST the envelope, capture the outcome. Owns no retry/queue logic —
4
+ * the dispatcher schedules attempts and `core.sendExample` fires one-off test
5
+ * sends through the same code path so test deliveries are wire-identical to
6
+ * real ones.
7
+ *
8
+ * @module
9
+ */
10
+ import type { Endpoint } from "./schema.ts";
11
+ /** Default cap on how many response-body characters are kept per attempt. */
12
+ export declare const DEFAULT_RESPONSE_BODY_LIMIT = 4096;
13
+ /** Default per-attempt timeout. */
14
+ export declare const DEFAULT_ATTEMPT_TIMEOUT_MS = 20000;
15
+ /** Input to {@link attemptDelivery}. */
16
+ export interface AttemptDeliveryInput {
17
+ endpoint: Endpoint;
18
+ /** The `webhook-id` header — the **message** id (stable across retries). */
19
+ messageId: string;
20
+ /** The serialized envelope body, exactly as stored at publish time. */
21
+ body: string;
22
+ /** Per-attempt timeout (AbortController). Default 20s. */
23
+ timeoutMs?: number;
24
+ /** Cap on stored response-body characters. Default 4096. */
25
+ responseBodyLimit?: number;
26
+ /** `user-agent` header value. */
27
+ userAgent?: string;
28
+ /** Injectable fetch (tests, instrumentation). Default: global fetch. */
29
+ fetch?: typeof fetch;
30
+ /** Unix millis of the attempt; defaults to `Date.now()`. */
31
+ nowMs?: number;
32
+ }
33
+ /** The observed outcome of one HTTP delivery attempt. */
34
+ export interface AttemptOutcome {
35
+ ok: boolean;
36
+ httpStatus?: number;
37
+ /** Truncated error message on network error/timeout. */
38
+ error?: string;
39
+ /** Truncated response body. */
40
+ responseBody?: string;
41
+ durationMs: number;
42
+ /** ISO-8601 timestamp of the attempt. */
43
+ at: string;
44
+ }
45
+ /** The exact HTTP request a delivery attempt sends — the inspector's view. */
46
+ export interface SignedRequest {
47
+ method: "POST";
48
+ url: string;
49
+ headers: Record<string, string>;
50
+ body: string;
51
+ }
52
+ /** Input to {@link buildSignedRequest}. */
53
+ export interface BuildSignedRequestInput {
54
+ endpoint: Endpoint;
55
+ /** `webhook-id` — the message id (stable across retries). */
56
+ messageId: string;
57
+ /** The serialized envelope body. */
58
+ body: string;
59
+ /** `user-agent` header value. */
60
+ userAgent?: string;
61
+ /** Unix millis; defaults to `Date.now()`. Determines `webhook-timestamp`. */
62
+ nowMs?: number;
63
+ }
64
+ /**
65
+ * Build the exact signed request an attempt would POST — Standard Webhooks
66
+ * headers, all unexpired secrets signing — **without sending it**. Shared by
67
+ * {@link attemptDelivery} and the panel's request inspector so what you preview
68
+ * is byte-for-byte what a receiver gets.
69
+ */
70
+ export declare function buildSignedRequest(input: BuildSignedRequestInput): Promise<SignedRequest>;
71
+ /** The endpoint's currently-signing secrets: the active one + unexpired rotation grace. */
72
+ export declare function activeSecrets(endpoint: Endpoint, nowMs: number): string[];
73
+ /**
74
+ * Perform one signed POST to `endpoint.url`. Never throws for remote-party
75
+ * failures — network errors, timeouts, and non-2xx responses all come back as
76
+ * a failed {@link AttemptOutcome}. (It does throw on programmer error, e.g. an
77
+ * endpoint with no unexpired secret.)
78
+ */
79
+ export declare function attemptDelivery(input: AttemptDeliveryInput): Promise<AttemptOutcome>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Delivery sink — a **delivery-plane** observation tap, fired once per attempt.
3
+ * Deliberately separate from the admin-plane {@link ./hooks/contract.WebhooksHooks}
4
+ * (`before`/`after` around control-plane mutations):
5
+ *
6
+ * - **Different plane.** Admin hooks live around rare, human-driven mutations
7
+ * (endpoints, event types, replay). Attempts happen inside the dispatcher,
8
+ * potentially thousands per minute.
9
+ * - **Hot path.** The sink is **fire-and-forget**: invoked *after* the attempt
10
+ * is recorded, never awaited, and its errors never propagate into the
11
+ * delivery loop.
12
+ *
13
+ * It exists to feed metrics/observability pipelines — Prometheus counters,
14
+ * per-endpoint success dashboards, usage-based stale-endpoint detection —
15
+ * without a stats engine. The `after` hooks only see **terminal** transitions
16
+ * (`delivery.succeeded` / `delivery.exhausted`); the sink sees every attempt.
17
+ *
18
+ * @module
19
+ */
20
+ import type { AttemptTrigger } from "./schema.ts";
21
+ /** One recorded delivery attempt, delivered to a {@link DeliveryListener}. */
22
+ export interface DeliveryEvent {
23
+ applicationKey: string;
24
+ endpointId: string;
25
+ messageId: string;
26
+ deliveryId: string;
27
+ /** The message's event type name. */
28
+ eventType: string;
29
+ /** 1-based attempt ordinal within the delivery. */
30
+ attemptNumber: number;
31
+ /** True when the receiver answered 2xx. */
32
+ ok: boolean;
33
+ /** True when this attempt drove the delivery to a terminal state. */
34
+ terminal: boolean;
35
+ /** HTTP status code; absent on network error/timeout. */
36
+ httpStatus?: number;
37
+ /** Wall-clock duration of the HTTP round trip. */
38
+ durationMs: number;
39
+ /** What initiated the attempt. */
40
+ trigger: AttemptTrigger;
41
+ /** ISO-8601 timestamp of the attempt. */
42
+ at: string;
43
+ }
44
+ /**
45
+ * A fire-and-forget observer of delivery attempts. Return value (including a
46
+ * Promise) is ignored by the caller; throwing / rejecting never affects the
47
+ * delivery — failures are routed to the configured error reporter.
48
+ */
49
+ export type DeliveryListener = (event: DeliveryEvent) => void | Promise<void>;
50
+ /** Reports an error thrown/rejected by a {@link DeliveryListener}. */
51
+ export type DeliveryErrorReporter = (error: unknown, event: DeliveryEvent) => void;
52
+ /**
53
+ * Invoke `listener` safely off the delivery path: synchronous throws are caught
54
+ * and a returned Promise's rejection is handled, so a broken sink can never
55
+ * fail (or slow, since it is not awaited) a delivery.
56
+ */
57
+ export declare function emitDelivery(listener: DeliveryListener, event: DeliveryEvent, onError?: DeliveryErrorReporter): void;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The delivery engine. Polls the due index, claims deliveries (lease-based,
3
+ * multi-instance safe when storage supports CAS or the `deliveryQueue`
4
+ * capability), performs the signed HTTP attempts, and drives each delivery's
5
+ * state machine through the retry schedule into success or dead-letter.
6
+ *
7
+ * Runs in-process: the panel starts one by default, a split worker runs one
8
+ * via the CLI (`xtandard-webhooks dispatch`), and tests drive {@link Dispatcher.tick}
9
+ * manually. All timers are `unref()`ed so a dispatcher never keeps a process
10
+ * alive on its own. Semantics are **at-least-once**: a crashed process loses
11
+ * nothing (leases expire, the next tick reclaims); receivers dedupe on
12
+ * `webhook-id`.
13
+ *
14
+ * @module
15
+ */
16
+ import type { WebhooksCore } from "./core.ts";
17
+ import type { WebhookDuration } from "./schema.ts";
18
+ /** Options for {@link createDispatcher} (also accepted on panels and the core). */
19
+ export interface DispatcherOptions {
20
+ /** How often to poll for due deliveries. Default `1000`. */
21
+ pollIntervalMs?: number;
22
+ /** Max deliveries claimed per tick. Default `20`. */
23
+ batchSize?: number;
24
+ /** Max in-flight HTTP attempts. Default `8`. */
25
+ concurrency?: number;
26
+ /** Per-attempt timeout (AbortController). Default `20_000`. */
27
+ timeoutMs?: number;
28
+ /** Claim lease duration; an expired lease makes a claim reclaimable. Default `60_000`. */
29
+ leaseMs?: number;
30
+ /**
31
+ * Delay before attempt N+1 after attempt N fails (index 0 = the initial
32
+ * attempt's delay). Exhausting the schedule dead-letters the delivery.
33
+ * Default `["0s", "5s", "5m", "30m", "2h", "5h", "10h"]` (Svix-compatible).
34
+ */
35
+ retrySchedule?: WebhookDuration[];
36
+ /**
37
+ * Auto-disable endpoints whose every attempt has failed for this many
38
+ * consecutive days. `false` disables the policy. Default `{ failingForDays: 5 }`.
39
+ */
40
+ autoDisable?: {
41
+ failingForDays?: number;
42
+ } | false;
43
+ /** Cap on stored response-body characters per attempt. Default `4096`. */
44
+ responseBodyLimit?: number;
45
+ /** Injectable fetch (tests, instrumentation). Default: global fetch. */
46
+ fetch?: typeof fetch;
47
+ /** `user-agent` header. Default `"xtandard-webhooks/<version>"`. */
48
+ userAgent?: string;
49
+ }
50
+ /** The delivery engine handle. */
51
+ export interface Dispatcher {
52
+ /** Begin polling. Idempotent. */
53
+ start(): void;
54
+ /** Stop polling and wait for in-flight attempts to finish. */
55
+ stop(): Promise<void>;
56
+ /**
57
+ * Run one manual pass: claim due deliveries, attempt them, record outcomes.
58
+ * Returns the number of attempts made — the unit-test surface (tests never
59
+ * assert on timers).
60
+ */
61
+ tick(): Promise<number>;
62
+ readonly running: boolean;
63
+ }
64
+ /** The default retry schedule (Svix-compatible). */
65
+ export declare const DEFAULT_RETRY_SCHEDULE: WebhookDuration[];
66
+ /**
67
+ * Create a dispatcher over a core. Not started — call
68
+ * {@link Dispatcher.start}, or drive {@link Dispatcher.tick} manually.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { createWebhooksCore, createDispatcher } from "@xtandard/webhooks";
73
+ * import { createMemoryStorage } from "@xtandard/webhooks/storage/memory";
74
+ *
75
+ * const core = createWebhooksCore({ storage: createMemoryStorage() });
76
+ * const dispatcher = createDispatcher(core);
77
+ * dispatcher.start();
78
+ * ```
79
+ */
80
+ export declare function createDispatcher(core: WebhooksCore, options?: DispatcherOptions): Dispatcher;