@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.
- package/README.md +1 -0
- package/dist/cli.cjs +4 -4
- package/dist/cli.mjs +4 -4
- package/dist/{core-CF8U0hgi.cjs → core-DkIXozBY.cjs} +2 -2
- package/dist/{core-CF8U0hgi.cjs.map → core-DkIXozBY.cjs.map} +1 -1
- package/dist/{core-C9mPi4iw.mjs → core-tmmtadF1.mjs} +2 -2
- package/dist/{core-C9mPi4iw.mjs.map → core-tmmtadF1.mjs.map} +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.mjs +1 -1
- package/dist/{create-fetch-handler-BILaZ2Z7.cjs → create-fetch-handler-B5WBbf8x.cjs} +3 -3
- package/dist/{create-fetch-handler-BILaZ2Z7.cjs.map → create-fetch-handler-B5WBbf8x.cjs.map} +1 -1
- package/dist/{create-fetch-handler-D0F3cjoq.mjs → create-fetch-handler-DZohI08M.mjs} +3 -3
- package/dist/{create-fetch-handler-D0F3cjoq.mjs.map → create-fetch-handler-DZohI08M.mjs.map} +1 -1
- package/dist/{dispatcher-DOOCJJxM.cjs → dispatcher-Bn4O874S.cjs} +3 -3
- package/dist/{dispatcher-DOOCJJxM.cjs.map → dispatcher-Bn4O874S.cjs.map} +1 -1
- package/dist/{dispatcher-DuBrQL46.mjs → dispatcher-CQcoIsS9.mjs} +3 -3
- package/dist/{dispatcher-DuBrQL46.mjs.map → dispatcher-CQcoIsS9.mjs.map} +1 -1
- package/dist/elysia-shared-CgTU3yPa.mjs +47 -0
- package/dist/elysia-shared-CgTU3yPa.mjs.map +1 -0
- package/dist/elysia-shared-Cop1-ie0.cjs +58 -0
- package/dist/elysia-shared-Cop1-ie0.cjs.map +1 -0
- package/dist/elysia-shared-CvSsqMbR.d.mts +19 -0
- package/dist/elysia-shared-DSSrfuoF.d.cts +19 -0
- package/dist/entry-bun.cjs +1 -1
- package/dist/entry-bun.mjs +1 -1
- package/dist/entry-elysia.cjs +5 -25
- package/dist/entry-elysia.cjs.map +1 -1
- package/dist/entry-elysia.d.cts +5 -15
- package/dist/entry-elysia.d.mts +5 -15
- package/dist/entry-elysia.mjs +4 -24
- package/dist/entry-elysia.mjs.map +1 -1
- package/dist/entry-elysia2.cjs +95 -0
- package/dist/entry-elysia2.cjs.map +1 -0
- package/dist/entry-elysia2.d.cts +1273 -0
- package/dist/entry-elysia2.d.mts +1273 -0
- package/dist/entry-elysia2.mjs +93 -0
- package/dist/entry-elysia2.mjs.map +1 -0
- package/dist/entry-express.cjs +1 -1
- package/dist/entry-express.mjs +1 -1
- package/dist/entry-hono.cjs +1 -1
- package/dist/entry-hono.mjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/react.css +1 -1
- package/dist/react.js +2522 -2506
- package/dist/testing.cjs +2 -2
- package/dist/testing.mjs +2 -2
- package/dist/types-elysia2/adapters/elysia-shared.d.ts +34 -0
- package/dist/types-elysia2/adapters/elysia2.d.ts +820 -0
- package/dist/types-elysia2/auth/contract.d.ts +26 -0
- package/dist/types-elysia2/authorization/contract.d.ts +50 -0
- package/dist/types-elysia2/canonical.d.ts +27 -0
- package/dist/types-elysia2/core.d.ts +344 -0
- package/dist/types-elysia2/deliver.d.ts +79 -0
- package/dist/types-elysia2/delivery-sink.d.ts +57 -0
- package/dist/types-elysia2/dispatcher.d.ts +80 -0
- package/dist/types-elysia2/duration.d.ts +26 -0
- package/dist/types-elysia2/entry-elysia2.d.ts +8 -0
- package/dist/types-elysia2/hooks/contract.d.ts +203 -0
- package/dist/types-elysia2/id.d.ts +24 -0
- package/dist/types-elysia2/keys.d.ts +98 -0
- package/dist/types-elysia2/portal.d.ts +69 -0
- package/dist/types-elysia2/schema.d.ts +229 -0
- package/dist/types-elysia2/server/base-path.d.ts +15 -0
- package/dist/types-elysia2/server/cors.d.ts +36 -0
- package/dist/types-elysia2/server/create-fetch-handler.d.ts +121 -0
- package/dist/types-elysia2/server/openapi.d.ts +25 -0
- package/dist/types-elysia2/server/render-index-html.d.ts +18 -0
- package/dist/types-elysia2/server/routes.d.ts +52 -0
- package/dist/types-elysia2/server/static-assets.d.ts +13 -0
- package/dist/types-elysia2/signing.d.ts +88 -0
- package/dist/types-elysia2/storage/contract.d.ts +75 -0
- package/dist/types-elysia2/validation.d.ts +82 -0
- package/dist/types-elysia2/version.d.ts +8 -0
- package/dist/types-react/ui/lib/portal-container.d.ts +24 -0
- package/dist/ui/assets/{index-S5t_CLOe.js → index-BgyX_njf.js} +44 -44
- package/dist/ui/assets/index-mJCJq_EB.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +23 -6
- package/dist/ui/assets/index-B0eoQX2U.css +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration parsing for {@link ./schema.WebhookDuration} config values. Zero
|
|
3
|
+
* dependencies; shared by the core (rotation grace, retention) and the
|
|
4
|
+
* dispatcher (retry schedule).
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { WebhookDuration } from "./schema.ts";
|
|
9
|
+
/**
|
|
10
|
+
* Convert a {@link WebhookDuration} (`5000`, `"5s"`, `"30m"`, `"2h"`, `"5d"`)
|
|
11
|
+
* to milliseconds. Throws on malformed strings or negative numbers.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { durationToMs } from "@xtandard/webhooks";
|
|
16
|
+
*
|
|
17
|
+
* durationToMs("5m"); // 300000
|
|
18
|
+
* durationToMs(250); // 250
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function durationToMs(duration: WebhookDuration): number;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a comma-separated duration list (the `RETRY_SCHEDULE` env var format,
|
|
24
|
+
* e.g. `"0s,5s,5m,30m,2h,5h,10h"`) into a {@link WebhookDuration} array.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseDurationList(input: string): WebhookDuration[];
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook contracts — control-plane extensibility around admin mutations.
|
|
3
|
+
*
|
|
4
|
+
* A hook is plain JavaScript wired in at {@link ../core.createWebhooksCore}
|
|
5
|
+
* time (never authored through the UI — that would be remote code execution).
|
|
6
|
+
* Two phases, deliberately asymmetric:
|
|
7
|
+
*
|
|
8
|
+
* - **`before`** runs *before* a mutation commits. Throwing **denies** the
|
|
9
|
+
* operation (the thrown error's message is the reason). Multiple `before`
|
|
10
|
+
* hooks run **sequentially in declared order**; the first throw aborts and
|
|
11
|
+
* nothing commits. This is the enforcement primitive that governance,
|
|
12
|
+
* quotas, and publish gates build on.
|
|
13
|
+
* - **`after`** runs *after* a mutation commits. It is for side effects
|
|
14
|
+
* (notifications, cache purges, offloading pruned payloads). An `after`
|
|
15
|
+
* hook **must never fail the operation** — the mutation already committed —
|
|
16
|
+
* so errors are isolated and reported via `onHookError`, not rethrown.
|
|
17
|
+
*
|
|
18
|
+
* Delivery *attempts* are deliberately not hook events (they are data-plane
|
|
19
|
+
* traffic and can be very hot); the per-attempt tap is the separate
|
|
20
|
+
* fire-and-forget {@link ../delivery-sink.DeliveryListener}. Only the terminal
|
|
21
|
+
* transitions (`delivery.succeeded`, `delivery.exhausted`) surface here.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { Actor, Application, AuditEntry, Delivery, DeliveryAttempt, Endpoint, EventType, JsonValue, Message } from "../schema.ts";
|
|
26
|
+
/**
|
|
27
|
+
* Event delivered to {@link WebhooksHooks.before} — the *proposed* mutation,
|
|
28
|
+
* before it commits. Throw from the handler to deny it.
|
|
29
|
+
*/
|
|
30
|
+
export type BeforeEvent = {
|
|
31
|
+
type: "application.create" | "application.update";
|
|
32
|
+
application: Application;
|
|
33
|
+
actor: Actor | null;
|
|
34
|
+
} | {
|
|
35
|
+
type: "application.delete";
|
|
36
|
+
applicationKey: string;
|
|
37
|
+
actor: Actor | null;
|
|
38
|
+
} | {
|
|
39
|
+
type: "event-type.upsert";
|
|
40
|
+
eventType: EventType;
|
|
41
|
+
actor: Actor | null;
|
|
42
|
+
} | {
|
|
43
|
+
type: "event-type.delete";
|
|
44
|
+
name: string;
|
|
45
|
+
actor: Actor | null;
|
|
46
|
+
} | {
|
|
47
|
+
type: "endpoint.create" | "endpoint.update";
|
|
48
|
+
applicationKey: string;
|
|
49
|
+
endpoint: Endpoint;
|
|
50
|
+
actor: Actor | null;
|
|
51
|
+
} | {
|
|
52
|
+
type: "endpoint.delete" | "endpoint.rotate-secret" | "endpoint.disable" | "endpoint.enable";
|
|
53
|
+
applicationKey: string;
|
|
54
|
+
endpointId: string;
|
|
55
|
+
actor: Actor | null;
|
|
56
|
+
} | {
|
|
57
|
+
/** The host's approval/quota gate for the publish hot path. */
|
|
58
|
+
type: "message.publish";
|
|
59
|
+
applicationKey: string;
|
|
60
|
+
eventType: string;
|
|
61
|
+
payload: JsonValue;
|
|
62
|
+
idempotencyKey?: string;
|
|
63
|
+
actor: Actor | null;
|
|
64
|
+
} | {
|
|
65
|
+
type: "delivery.retry";
|
|
66
|
+
applicationKey: string;
|
|
67
|
+
deliveryId: string;
|
|
68
|
+
actor: Actor | null;
|
|
69
|
+
} | {
|
|
70
|
+
type: "endpoint.recover";
|
|
71
|
+
applicationKey: string;
|
|
72
|
+
endpointId: string;
|
|
73
|
+
since: string;
|
|
74
|
+
actor: Actor | null;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Event delivered to {@link WebhooksHooks.after} — the *committed* mutation.
|
|
78
|
+
* Carries the resulting state in full: for destructive events this is the last
|
|
79
|
+
* chance to offload the payload before it is gone from storage.
|
|
80
|
+
*/
|
|
81
|
+
export type AfterEvent = {
|
|
82
|
+
type: "application.created" | "application.updated";
|
|
83
|
+
application: Application;
|
|
84
|
+
at: string;
|
|
85
|
+
} | {
|
|
86
|
+
/** Carries the deleted application — everything under it is already gone. */
|
|
87
|
+
type: "application.deleted";
|
|
88
|
+
applicationKey: string;
|
|
89
|
+
application: Application;
|
|
90
|
+
at: string;
|
|
91
|
+
} | {
|
|
92
|
+
type: "event-type.upserted";
|
|
93
|
+
eventType: EventType;
|
|
94
|
+
at: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: "event-type.deleted";
|
|
97
|
+
name: string;
|
|
98
|
+
eventType: EventType;
|
|
99
|
+
at: string;
|
|
100
|
+
} | {
|
|
101
|
+
type: "endpoint.created" | "endpoint.updated" | "endpoint.deleted" | "endpoint.secret-rotated" | "endpoint.disabled" | "endpoint.enabled"
|
|
102
|
+
/** Disabled by the failure policy, not an operator (see dispatcher `autoDisable`). */
|
|
103
|
+
| "endpoint.auto-disabled";
|
|
104
|
+
applicationKey: string;
|
|
105
|
+
endpoint: Endpoint;
|
|
106
|
+
at: string;
|
|
107
|
+
} | {
|
|
108
|
+
type: "message.published";
|
|
109
|
+
applicationKey: string;
|
|
110
|
+
message: Message;
|
|
111
|
+
deliveryIds: string[];
|
|
112
|
+
at: string;
|
|
113
|
+
} | {
|
|
114
|
+
type: "delivery.succeeded";
|
|
115
|
+
applicationKey: string;
|
|
116
|
+
delivery: Delivery;
|
|
117
|
+
attempt: DeliveryAttempt;
|
|
118
|
+
at: string;
|
|
119
|
+
} | {
|
|
120
|
+
/** Dead-letter offload point: the retry schedule is exhausted. */
|
|
121
|
+
type: "delivery.exhausted";
|
|
122
|
+
applicationKey: string;
|
|
123
|
+
delivery: Delivery;
|
|
124
|
+
attempts: DeliveryAttempt[];
|
|
125
|
+
at: string;
|
|
126
|
+
} | {
|
|
127
|
+
/** Messages removed by retention. Carries the **full messages** — offload now or lose them. */
|
|
128
|
+
type: "message.pruned";
|
|
129
|
+
applicationKey: string;
|
|
130
|
+
messages: Message[];
|
|
131
|
+
at: string;
|
|
132
|
+
} | {
|
|
133
|
+
/** Audit entries removed by retention (oldest-first). */
|
|
134
|
+
type: "audit.pruned";
|
|
135
|
+
applicationKey?: string;
|
|
136
|
+
entries: AuditEntry[];
|
|
137
|
+
at: string;
|
|
138
|
+
};
|
|
139
|
+
/** The discriminant strings of {@link BeforeEvent} / {@link AfterEvent}. */
|
|
140
|
+
export type BeforeEventType = BeforeEvent["type"];
|
|
141
|
+
export type AfterEventType = AfterEvent["type"];
|
|
142
|
+
/**
|
|
143
|
+
* Thrown from a {@link WebhooksHooks.before} handler to **deny** a mutation with
|
|
144
|
+
* a clean HTTP status (default `403`). Any thrown error denies the mutation,
|
|
145
|
+
* but a plain `Error` maps to `500` at the API layer (treated as an unexpected
|
|
146
|
+
* bug); throw this to signal a deliberate policy rejection (`403`, or a custom
|
|
147
|
+
* `status` such as `409`/`422`).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* before(event) {
|
|
152
|
+
* if (event.type === "message.publish" && overQuota(event.applicationKey)) {
|
|
153
|
+
* throw new HookDeniedError("Monthly webhook quota exceeded.", { status: 429 });
|
|
154
|
+
* }
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare class HookDeniedError extends Error {
|
|
159
|
+
/** HTTP status the API layer should respond with. Default `403`. */
|
|
160
|
+
readonly status: number;
|
|
161
|
+
constructor(message: string, options?: {
|
|
162
|
+
status?: number;
|
|
163
|
+
cause?: unknown;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A control-plane hook. Implement `before`, `after`, or both. Pass one — or an
|
|
168
|
+
* array — to {@link ../core.createWebhooksCore} via `hooks`.
|
|
169
|
+
*/
|
|
170
|
+
export interface WebhooksHooks {
|
|
171
|
+
/**
|
|
172
|
+
* Runs before a mutation commits. **Throw to deny** (the error propagates to
|
|
173
|
+
* the caller; its message is the reason). Return/resolve to allow. Must not
|
|
174
|
+
* mutate the event payload.
|
|
175
|
+
*/
|
|
176
|
+
before?(event: BeforeEvent): void | Promise<void>;
|
|
177
|
+
/**
|
|
178
|
+
* Runs after a mutation commits. For side effects only. Errors are isolated
|
|
179
|
+
* and reported via `onHookError` — they never fail the (already committed)
|
|
180
|
+
* operation.
|
|
181
|
+
*/
|
|
182
|
+
after?(event: AfterEvent): void | Promise<void>;
|
|
183
|
+
}
|
|
184
|
+
/** Accepts a single hook, an array, or nothing. */
|
|
185
|
+
export type WebhooksHooksInput = WebhooksHooks | readonly WebhooksHooks[] | undefined;
|
|
186
|
+
/** Reports an error thrown by an `after` hook. */
|
|
187
|
+
export type HookErrorReporter = (error: unknown, event: AfterEvent) => void;
|
|
188
|
+
/** Normalize the `hooks` option into a flat array (empty when unset). */
|
|
189
|
+
export declare function normalizeHooks(input: WebhooksHooksInput): WebhooksHooks[];
|
|
190
|
+
/**
|
|
191
|
+
* Run every `before` hook sequentially, in order. The first hook to throw
|
|
192
|
+
* aborts: the error propagates to the caller (denying the mutation) and no
|
|
193
|
+
* later hook runs. A no-op when there are no `before` hooks.
|
|
194
|
+
*/
|
|
195
|
+
export declare function runBefore(hooks: WebhooksHooks[], event: BeforeEvent): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Run every `after` hook, isolating failures. The mutation has already
|
|
198
|
+
* committed, so a throwing hook must not fail the operation — its error is
|
|
199
|
+
* routed to `onError` and swallowed. Remaining hooks still run.
|
|
200
|
+
*/
|
|
201
|
+
export declare function runAfter(hooks: WebhooksHooks[], event: AfterEvent, onError: HookErrorReporter): Promise<void>;
|
|
202
|
+
/** Default `after`-hook error reporter: warn, but never throw. */
|
|
203
|
+
export declare const defaultHookErrorReporter: HookErrorReporter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity id generation: `{prefix}_{22-char base62}`. Zero dependencies — base62
|
|
3
|
+
* over `crypto.getRandomValues`, no ULID. 22 base62 characters encode ~131 bits;
|
|
4
|
+
* ids are generated from 16 random bytes (128 bits) and left-padded to a fixed
|
|
5
|
+
* length so ids are uniform and lexicographically well-behaved.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** Prefixes for the four generated entity id kinds. */
|
|
10
|
+
export type IdPrefix = "msg" | "ep" | "dlv" | "atp";
|
|
11
|
+
/**
|
|
12
|
+
* Generate a new entity id, e.g. `newId("msg")` → `"msg_0uK9…"` (22-char base62
|
|
13
|
+
* suffix).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { newId } from "@xtandard/webhooks";
|
|
18
|
+
*
|
|
19
|
+
* const id = newId("ep"); // "ep_…"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function newId(prefix: IdPrefix): string;
|
|
23
|
+
/** Regex an id of the given prefix must match. */
|
|
24
|
+
export declare function idPattern(prefix: IdPrefix): RegExp;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage key layout for `@xtandard/webhooks`.
|
|
3
|
+
*
|
|
4
|
+
* Keys are namespaced by application so a single storage backend can host the
|
|
5
|
+
* whole control plane. Pure string helpers — no dependencies.
|
|
6
|
+
*
|
|
7
|
+
* ```txt
|
|
8
|
+
* whk/applications -> string[] index
|
|
9
|
+
* whk/{app}/metadata -> Application
|
|
10
|
+
* whk/event-types -> string[] index (global)
|
|
11
|
+
* whk/event-types/{name} -> EventType
|
|
12
|
+
* whk/{app}/endpoints -> string[] index
|
|
13
|
+
* whk/{app}/endpoints/{id} -> Endpoint
|
|
14
|
+
* whk/{app}/messages/{id} -> Message
|
|
15
|
+
* whk/{app}/idempotency/{key} -> message id
|
|
16
|
+
* whk/{app}/deliveries/{id} -> Delivery
|
|
17
|
+
* whk/{app}/attempts/{deliveryId}/{n} -> DeliveryAttempt (n zero-padded)
|
|
18
|
+
* whk/{app}/due/{dueAtMillis}~{deliveryId} -> DueEntry (13-digit zero-padded millis)
|
|
19
|
+
* whk/{app}/by-message/{messageId}/{deliveryId} -> 1 (reverse index)
|
|
20
|
+
* whk/{app}/by-endpoint/{endpointId}/{deliveryId} -> 1 (reverse index)
|
|
21
|
+
* whk/{app}/audit-log -> AuditEntry[] (single ordered array)
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The due index is the dispatcher's generic work queue: 13-digit zero-padded
|
|
25
|
+
* milliseconds make lexicographic key order chronological, so a sorted
|
|
26
|
+
* `getKeys(duePrefix(app))` scan yields deliveries in due order.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
/** Root namespace segment for all keys. */
|
|
31
|
+
export declare const ROOT = "whk";
|
|
32
|
+
/**
|
|
33
|
+
* Application keys that would collide with global index keys under the shared
|
|
34
|
+
* root. Rejected by validation.
|
|
35
|
+
*/
|
|
36
|
+
export declare const RESERVED_APPLICATION_KEYS: readonly ["applications", "event-types", "audit-log"];
|
|
37
|
+
/** Global audit log (event-type actions, which are not application-scoped). */
|
|
38
|
+
export declare const globalAuditLogKey: () => string;
|
|
39
|
+
/** Index key listing all known application keys. */
|
|
40
|
+
export declare const applicationsKey: () => string;
|
|
41
|
+
/** Metadata for a single application. */
|
|
42
|
+
export declare const applicationMetaKey: (applicationKey: string) => string;
|
|
43
|
+
/** Index key listing all event type names (global catalog). */
|
|
44
|
+
export declare const eventTypesKey: () => string;
|
|
45
|
+
/** A single event type record. */
|
|
46
|
+
export declare const eventTypeKey: (name: string) => string;
|
|
47
|
+
/** Index key listing an application's endpoint ids. */
|
|
48
|
+
export declare const endpointsKey: (applicationKey: string) => string;
|
|
49
|
+
/** A single endpoint record. */
|
|
50
|
+
export declare const endpointKey: (applicationKey: string, endpointId: string) => string;
|
|
51
|
+
/** Prefix under which an application's messages live. */
|
|
52
|
+
export declare const messagesPrefix: (applicationKey: string) => string;
|
|
53
|
+
/** A single message record. */
|
|
54
|
+
export declare const messageKey: (applicationKey: string, messageId: string) => string;
|
|
55
|
+
/** Maps a caller-supplied idempotency key to the message id it produced. */
|
|
56
|
+
export declare const idempotencyKey: (applicationKey: string, key: string) => string;
|
|
57
|
+
/** Prefix under which an application's deliveries live. */
|
|
58
|
+
export declare const deliveriesPrefix: (applicationKey: string) => string;
|
|
59
|
+
/** A single delivery record. */
|
|
60
|
+
export declare const deliveryKey: (applicationKey: string, deliveryId: string) => string;
|
|
61
|
+
/** Prefix under which one delivery's attempts live. */
|
|
62
|
+
export declare const attemptsPrefix: (applicationKey: string, deliveryId: string) => string;
|
|
63
|
+
/**
|
|
64
|
+
* A single attempt record. The attempt number is zero-padded to four digits so
|
|
65
|
+
* lexicographic key order equals attempt order.
|
|
66
|
+
*/
|
|
67
|
+
export declare const attemptKey: (applicationKey: string, deliveryId: string, attemptNumber: number) => string;
|
|
68
|
+
/** Prefix of an application's due index (the dispatcher's work queue). */
|
|
69
|
+
export declare const duePrefix: (applicationKey: string) => string;
|
|
70
|
+
/** Value stored at a due-index key. */
|
|
71
|
+
export interface DueEntry {
|
|
72
|
+
app: string;
|
|
73
|
+
deliveryId: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A due-index entry: 13-digit zero-padded epoch milliseconds, then the delivery
|
|
77
|
+
* id, `~`-separated. Sorting keys lexicographically sorts entries chronologically.
|
|
78
|
+
*/
|
|
79
|
+
export declare const dueKey: (applicationKey: string, dueAtMillis: number, deliveryId: string) => string;
|
|
80
|
+
/** Parse a due-index key back into its due-time and delivery id. */
|
|
81
|
+
export declare const parseDueKey: (key: string) => {
|
|
82
|
+
dueAtMillis: number;
|
|
83
|
+
deliveryId: string;
|
|
84
|
+
} | null;
|
|
85
|
+
/** Prefix of the message → deliveries reverse index. */
|
|
86
|
+
export declare const byMessagePrefix: (applicationKey: string, messageId: string) => string;
|
|
87
|
+
/** One entry in the message → deliveries reverse index. */
|
|
88
|
+
export declare const byMessageKey: (applicationKey: string, messageId: string, deliveryId: string) => string;
|
|
89
|
+
/** Prefix of the endpoint → deliveries reverse index. */
|
|
90
|
+
export declare const byEndpointPrefix: (applicationKey: string, endpointId: string) => string;
|
|
91
|
+
/** One entry in the endpoint → deliveries reverse index. */
|
|
92
|
+
export declare const byEndpointKey: (applicationKey: string, endpointId: string, deliveryId: string) => string;
|
|
93
|
+
/** Append-only audit log for an application, stored as an ordered `AuditEntry[]`. */
|
|
94
|
+
export declare const auditLogKey: (applicationKey: string) => string;
|
|
95
|
+
/** Prefix under which everything belonging to one application lives. */
|
|
96
|
+
export declare const applicationPrefix: (applicationKey: string) => string;
|
|
97
|
+
/** Extract the trailing segment (endpoint id / message id / …) from a key. */
|
|
98
|
+
export declare const lastSegment: (key: string) => string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal tokens — the credential behind the embeddable consumer portal.
|
|
3
|
+
*
|
|
4
|
+
* A portal token is a compact HMAC-signed grant scoping its bearer to exactly
|
|
5
|
+
* **one application** for a limited time. The host mints tokens server-side
|
|
6
|
+
* with {@link createPortalToken} (the portal secret never reaches a browser)
|
|
7
|
+
* and hands them to its frontend; the panel handler verifies them with
|
|
8
|
+
* {@link verifyPortalToken} and force-scopes authorization to the token's
|
|
9
|
+
* application (see the `portal` panel option).
|
|
10
|
+
*
|
|
11
|
+
* Wire format:
|
|
12
|
+
*
|
|
13
|
+
* ```txt
|
|
14
|
+
* whpt_<base64url(JSON { app, exp })>.<base64url(HMAC-SHA256(secret, payloadPart))>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Zero dependencies (Web Crypto only). Unlike Standard Webhooks signing
|
|
18
|
+
* secrets, the portal secret is an **arbitrary string** — it is fed to the
|
|
19
|
+
* HMAC as raw UTF-8 bytes, not base64-decoded.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import type { WebhookDuration } from "./schema.ts";
|
|
24
|
+
/** Prefix of every portal token. */
|
|
25
|
+
export declare const PORTAL_TOKEN_PREFIX = "whpt_";
|
|
26
|
+
/**
|
|
27
|
+
* Thrown by {@link verifyPortalToken} for any invalid token — malformed,
|
|
28
|
+
* bad signature, or expired. Maps to HTTP 401 at the API layer.
|
|
29
|
+
*/
|
|
30
|
+
export declare class PortalTokenError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/** Options for {@link createPortalToken}. */
|
|
34
|
+
export interface PortalTokenOptions {
|
|
35
|
+
/** Token lifetime. Default `"7d"`. */
|
|
36
|
+
expiresIn?: WebhookDuration;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mint a portal token granting access to `applicationKey` until `expiresIn`
|
|
40
|
+
* elapses (default 7 days). Call this **server-side** — anyone holding the
|
|
41
|
+
* portal secret can mint tokens for any application.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { createPortalToken } from "@xtandard/webhooks";
|
|
46
|
+
*
|
|
47
|
+
* // In the host app's session-guarded route:
|
|
48
|
+
* const token = await createPortalToken(process.env.PORTAL_SECRET!, "acme", {
|
|
49
|
+
* expiresIn: "1h",
|
|
50
|
+
* });
|
|
51
|
+
* // Hand `token` to the frontend for <WebhooksPortal token={token} />.
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function createPortalToken(secret: string, applicationKey: string, options?: PortalTokenOptions): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Verify a portal token: format, signature (constant time), then expiry.
|
|
57
|
+
* Returns the granted application key on success; throws
|
|
58
|
+
* {@link PortalTokenError} otherwise.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { verifyPortalToken } from "@xtandard/webhooks";
|
|
63
|
+
*
|
|
64
|
+
* const { applicationKey } = await verifyPortalToken(secret, token);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function verifyPortalToken(secret: string, token: string): Promise<{
|
|
68
|
+
applicationKey: string;
|
|
69
|
+
}>;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for `@xtandard/webhooks`.
|
|
3
|
+
*
|
|
4
|
+
* This module is **types only** — no runtime code beyond tiny guards and the
|
|
5
|
+
* schema-version constant, no dependencies. It is safe to import from the
|
|
6
|
+
* publish hot path and the receiver. Runtime validation (which pulls in
|
|
7
|
+
* `valibot`) lives in {@link ./validation}.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** JSON-serializable value, used for payloads, metadata, and event-type schemas. */
|
|
12
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
13
|
+
[key: string]: JsonValue;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A duration for configuration options: either milliseconds as a number, or a
|
|
17
|
+
* human-friendly string such as `"5s"`, `"30m"`, `"2h"`, `"5d"`. Parsed by
|
|
18
|
+
* {@link ./duration.durationToMs}.
|
|
19
|
+
*/
|
|
20
|
+
export type WebhookDuration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
|
|
21
|
+
/** Identity captured on audit records and passed by callers of audited mutations. */
|
|
22
|
+
export interface Actor {
|
|
23
|
+
id: string;
|
|
24
|
+
email?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Current schema version for stored records. */
|
|
28
|
+
export declare const SCHEMA_VERSION: 1;
|
|
29
|
+
/**
|
|
30
|
+
* An application is the unit of tenancy — typically one of *your* customers.
|
|
31
|
+
* Endpoints, messages, and deliveries are all scoped to an application, and a
|
|
32
|
+
* portal token grants access to exactly one application.
|
|
33
|
+
*/
|
|
34
|
+
export interface Application {
|
|
35
|
+
/** Unique key. Allowed characters: `a-z A-Z 0-9 . _ -`. */
|
|
36
|
+
key: string;
|
|
37
|
+
/** Optional human-friendly name shown in the UI. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Arbitrary host-defined metadata. */
|
|
40
|
+
metadata?: JsonValue;
|
|
41
|
+
/** ISO-8601 creation timestamp, server-stamped. */
|
|
42
|
+
createdAt?: string;
|
|
43
|
+
/** ISO-8601 last-update timestamp, server-stamped. */
|
|
44
|
+
updatedAt?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A named kind of event in the **global** catalog (shared by all applications),
|
|
48
|
+
* e.g. `"invoice.paid"`. Endpoints subscribe to event types by name.
|
|
49
|
+
*/
|
|
50
|
+
export interface EventType {
|
|
51
|
+
/** Unique dot-delimited name, e.g. `"invoice.paid"`. Allowed: `a-z A-Z 0-9 . _ -`. */
|
|
52
|
+
name: string;
|
|
53
|
+
/** Optional description shown in the UI and the public catalog. */
|
|
54
|
+
description?: string;
|
|
55
|
+
/** Optional UI grouping label (e.g. `"Billing"`). Purely presentational. */
|
|
56
|
+
groupName?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Optional JSON Schema describing the payload. Documentation by default;
|
|
59
|
+
* enforced only when the core is configured with a `payloadValidator`.
|
|
60
|
+
*/
|
|
61
|
+
schema?: JsonValue;
|
|
62
|
+
/** Deprecated event types are flagged in the UI but still deliverable. */
|
|
63
|
+
deprecated?: boolean;
|
|
64
|
+
/** ISO-8601 creation timestamp, server-stamped. */
|
|
65
|
+
createdAt?: string;
|
|
66
|
+
/** ISO-8601 last-update timestamp, server-stamped. */
|
|
67
|
+
updatedAt?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A signing secret attached to an endpoint. Secrets are `whsec_` + base64 per
|
|
71
|
+
* the Standard Webhooks spec. Rotation keeps the previous secret verifiable
|
|
72
|
+
* until `expiresAt` (the grace window); expired secrets are pruned lazily.
|
|
73
|
+
*/
|
|
74
|
+
export interface EndpointSecret {
|
|
75
|
+
/** `"whsec_"` + base64-encoded random key material. */
|
|
76
|
+
secret: string;
|
|
77
|
+
/** ISO-8601 creation timestamp. */
|
|
78
|
+
createdAt: string;
|
|
79
|
+
/** Set when superseded by rotation; the secret stops signing after this instant. */
|
|
80
|
+
expiresAt?: string;
|
|
81
|
+
}
|
|
82
|
+
/** Why an endpoint is disabled: by an operator, or by the auto-disable policy. */
|
|
83
|
+
export type EndpointDisabledReason = "manual" | "auto";
|
|
84
|
+
/**
|
|
85
|
+
* A customer-registered URL that receives deliveries for one application.
|
|
86
|
+
*/
|
|
87
|
+
export interface Endpoint {
|
|
88
|
+
/** `"ep_"` + 22-char base62, server-generated. */
|
|
89
|
+
id: string;
|
|
90
|
+
/** Destination URL. `https` is enforced by default (`allowInsecureUrls` for dev). */
|
|
91
|
+
url: string;
|
|
92
|
+
/** Optional description shown in the UI. */
|
|
93
|
+
description?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Event type names this endpoint subscribes to. Empty or absent = receives
|
|
96
|
+
* **all** event types.
|
|
97
|
+
*/
|
|
98
|
+
eventTypes?: string[];
|
|
99
|
+
/** Disabled endpoints receive no deliveries (pending ones are held, not failed). */
|
|
100
|
+
disabled?: boolean;
|
|
101
|
+
/** Who/what disabled it — an operator (`"manual"`) or the failure policy (`"auto"`). */
|
|
102
|
+
disabledReason?: EndpointDisabledReason;
|
|
103
|
+
/** Static extra headers merged into every delivery request. */
|
|
104
|
+
headers?: Record<string, string>;
|
|
105
|
+
/** Signing secrets. `[0]` is current; extras are still-verifiable rotation grace. */
|
|
106
|
+
secrets: EndpointSecret[];
|
|
107
|
+
/** Arbitrary host-defined metadata. */
|
|
108
|
+
metadata?: JsonValue;
|
|
109
|
+
/** ISO-8601 creation timestamp, server-stamped. */
|
|
110
|
+
createdAt?: string;
|
|
111
|
+
/** ISO-8601 last-update timestamp, server-stamped. */
|
|
112
|
+
updatedAt?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Start of the current unbroken failure streak (any successful delivery
|
|
115
|
+
* clears it). Drives the auto-disable policy.
|
|
116
|
+
*/
|
|
117
|
+
firstFailingAt?: string | null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A published event for one application. The message is the receiver-facing
|
|
121
|
+
* unit: its id is the `webhook-id` header (stable across retries — receivers
|
|
122
|
+
* dedupe on it), and its payload is the `data` of the wire envelope.
|
|
123
|
+
*/
|
|
124
|
+
export interface Message {
|
|
125
|
+
/** `"msg_"` + 22-char base62, server-generated. Sent as `webhook-id`. */
|
|
126
|
+
id: string;
|
|
127
|
+
/** Event type name (usually from the global catalog). */
|
|
128
|
+
eventType: string;
|
|
129
|
+
/** The `data` of the wire envelope. */
|
|
130
|
+
payload: JsonValue;
|
|
131
|
+
/** ISO-8601 event-occurred time (defaults to publish time). Sent in the envelope. */
|
|
132
|
+
timestamp: string;
|
|
133
|
+
/** Optional caller-supplied dedupe key; same key within retention returns the same message. */
|
|
134
|
+
idempotencyKey?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The wire envelope serialized **once at publish time** so the signed bytes
|
|
137
|
+
* are identical across every retry of every delivery.
|
|
138
|
+
*/
|
|
139
|
+
envelope: string;
|
|
140
|
+
/** ISO-8601 creation timestamp, server-stamped. */
|
|
141
|
+
createdAt: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Delivery lifecycle. `"failed"` means the retry schedule is exhausted — the
|
|
145
|
+
* dead-letter state. A manual retry moves a failed delivery back to `"pending"`.
|
|
146
|
+
*/
|
|
147
|
+
export type DeliveryStatus = "pending" | "delivering" | "succeeded" | "failed";
|
|
148
|
+
/** Guard: has this delivery reached a terminal state? */
|
|
149
|
+
export declare function isTerminalDeliveryStatus(status: DeliveryStatus): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* One message × one endpoint. Created at publish time (fan-out) and driven to
|
|
152
|
+
* a terminal state by the dispatcher.
|
|
153
|
+
*/
|
|
154
|
+
export interface Delivery {
|
|
155
|
+
/** `"dlv_"` + 22-char base62, server-generated. */
|
|
156
|
+
id: string;
|
|
157
|
+
/** The application this delivery belongs to (deliveries cross the dispatcher denormalized). */
|
|
158
|
+
applicationKey: string;
|
|
159
|
+
messageId: string;
|
|
160
|
+
endpointId: string;
|
|
161
|
+
status: DeliveryStatus;
|
|
162
|
+
/** Number of attempts made so far. */
|
|
163
|
+
attemptCount: number;
|
|
164
|
+
/** When the next attempt is due. `null` once terminal. */
|
|
165
|
+
nextAttemptAt?: string | null;
|
|
166
|
+
/** Dispatcher claim expiry; an expired lease makes the delivery reclaimable. */
|
|
167
|
+
leaseUntil?: string | null;
|
|
168
|
+
/**
|
|
169
|
+
* How the *next* attempt should be recorded: set to `"manual"` by
|
|
170
|
+
* retry/recover so the resulting attempt is attributed correctly, then
|
|
171
|
+
* cleared. Absent = `"schedule"`.
|
|
172
|
+
*/
|
|
173
|
+
pendingTrigger?: AttemptTrigger;
|
|
174
|
+
/** ISO-8601 creation timestamp. */
|
|
175
|
+
createdAt: string;
|
|
176
|
+
/** ISO-8601 last-transition timestamp. */
|
|
177
|
+
updatedAt: string;
|
|
178
|
+
}
|
|
179
|
+
/** What initiated a delivery attempt. */
|
|
180
|
+
export type AttemptTrigger = "schedule" | "manual" | "test";
|
|
181
|
+
/** The recorded outcome of a single HTTP delivery attempt. */
|
|
182
|
+
export interface DeliveryAttempt {
|
|
183
|
+
/** `"atp_"` + 22-char base62, server-generated. */
|
|
184
|
+
id: string;
|
|
185
|
+
deliveryId: string;
|
|
186
|
+
/** 1-based attempt ordinal within the delivery. */
|
|
187
|
+
attemptNumber: number;
|
|
188
|
+
/** ISO-8601 timestamp of the attempt. */
|
|
189
|
+
at: string;
|
|
190
|
+
/** Wall-clock duration of the HTTP round trip. */
|
|
191
|
+
durationMs: number;
|
|
192
|
+
/** True when the receiver answered 2xx. */
|
|
193
|
+
ok: boolean;
|
|
194
|
+
/** HTTP status code; absent on network error or timeout. */
|
|
195
|
+
httpStatus?: number;
|
|
196
|
+
/** Truncated error message on network error/timeout. */
|
|
197
|
+
error?: string;
|
|
198
|
+
/** Truncated response body (default cap 4096 chars). */
|
|
199
|
+
responseBody?: string;
|
|
200
|
+
/** What initiated this attempt. */
|
|
201
|
+
trigger: AttemptTrigger;
|
|
202
|
+
}
|
|
203
|
+
/** Actions recorded in the control-plane audit log. */
|
|
204
|
+
export type AuditAction = "application.create" | "application.update" | "application.delete" | "event-type.create" | "event-type.update" | "event-type.delete" | "endpoint.create" | "endpoint.update" | "endpoint.delete" | "endpoint.rotate-secret" | "endpoint.disable" | "endpoint.enable" | "delivery.retry" | "endpoint.recover";
|
|
205
|
+
/**
|
|
206
|
+
* A single control-plane audit record. Publishes are deliberately *not*
|
|
207
|
+
* audited — they are data-plane traffic, and the message log is their record.
|
|
208
|
+
*/
|
|
209
|
+
export interface AuditEntry {
|
|
210
|
+
action: AuditAction;
|
|
211
|
+
/** ISO-8601 timestamp. */
|
|
212
|
+
at: string;
|
|
213
|
+
by?: Actor | null;
|
|
214
|
+
/** The application the action was scoped to (absent for global event types). */
|
|
215
|
+
applicationKey?: string;
|
|
216
|
+
/** Endpoint id / event type name / delivery id the action targeted. */
|
|
217
|
+
subjectId?: string;
|
|
218
|
+
message?: string;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The wire envelope receivers get — the Standard Webhooks recommended shape.
|
|
222
|
+
* `type` is the event type name, `timestamp` the event-occurred time, and
|
|
223
|
+
* `data` the published payload.
|
|
224
|
+
*/
|
|
225
|
+
export interface WebhookEnvelope {
|
|
226
|
+
type: string;
|
|
227
|
+
timestamp: string;
|
|
228
|
+
data: JsonValue;
|
|
229
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-path helpers. The panel can be mounted under any prefix (`/webhooks`,
|
|
3
|
+
* `/admin/webhooks`, ...). These normalize the configured base path and strip it
|
|
4
|
+
* from incoming request paths so routing is prefix-agnostic.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
/** Normalize a base path to either `""` (root) or `/segment` with no trailing slash. */
|
|
9
|
+
export declare function normalizeBasePath(basePath: string | undefined): string;
|
|
10
|
+
/**
|
|
11
|
+
* Strip the (normalized) base path from a pathname. Returns a path that always
|
|
12
|
+
* starts with `/`. If the pathname does not start with the base path it is
|
|
13
|
+
* returned unchanged (the host framework may already have stripped the mount).
|
|
14
|
+
*/
|
|
15
|
+
export declare function stripBasePath(pathname: string, basePath: string): string;
|