@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,36 @@
1
+ /**
2
+ * Framework-independent CORS for the mounted panel. When `webhooksPanel` /
3
+ * `createFetchHandler` is given a `cors` option, the handler answers `OPTIONS`
4
+ * preflights itself and attaches `Access-Control-*` headers to every response —
5
+ * so a **cross-origin embed** (portal on `app.example.com`, panel on
6
+ * `api.example.com`) works even if the host framework's CORS middleware does not
7
+ * wrap the mounted sub-handler.
8
+ *
9
+ * @module
10
+ */
11
+ /** CORS configuration for the panel handler. */
12
+ export interface WebhooksCorsOptions {
13
+ /**
14
+ * Allowed origin(s): a single origin (`"https://app.example.com"`), `"*"`, a
15
+ * list, or a predicate `(origin) => boolean`. With `credentials: true`, `"*"`
16
+ * is echoed back as the caller's exact origin (the browser forbids `*` +
17
+ * credentials).
18
+ */
19
+ origin: string | string[] | ((origin: string) => boolean);
20
+ /** Send `Access-Control-Allow-Credentials: true` (needed for cookie auth). Default `false`. */
21
+ credentials?: boolean;
22
+ /** Allowed methods advertised in preflight. Default `GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS`. */
23
+ methods?: string[];
24
+ /**
25
+ * Allowed request headers advertised in preflight. Default: reflect the
26
+ * request's `Access-Control-Request-Headers`, falling back to
27
+ * `Content-Type, Authorization`.
28
+ */
29
+ headers?: string[];
30
+ /** `Access-Control-Max-Age` (seconds) for the preflight cache. */
31
+ maxAge?: number;
32
+ }
33
+ /** Attach `Access-Control-*` headers to `response` for an allowed cross-origin request. */
34
+ export declare function applyCorsHeaders(request: Request, response: Response, cors: WebhooksCorsOptions): Response;
35
+ /** Build the `204` response for a CORS preflight (`OPTIONS`). */
36
+ export declare function preflightResponse(request: Request, cors: WebhooksCorsOptions): Response;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The web-standard fetch handler at the heart of every framework adapter and the
3
+ * standalone app. Composes auth/authorization (plus portal-token scoping), the
4
+ * JSON admin API, static-asset serving, and SPA fallback into a single
5
+ * `(request: Request) => Promise<Response>` — and, by default, starts the
6
+ * in-process delivery dispatcher so mounting the panel is all it takes to
7
+ * deliver webhooks.
8
+ *
9
+ * @module
10
+ */
11
+ import type { AuthProvider } from "../auth/contract.ts";
12
+ import type { AuthorizationProvider } from "../authorization/contract.ts";
13
+ import { type RetentionOptions, type WebhooksCore } from "../core.ts";
14
+ import type { DeliveryErrorReporter, DeliveryListener } from "../delivery-sink.ts";
15
+ import { type Dispatcher, type DispatcherOptions } from "../dispatcher.ts";
16
+ import type { HookErrorReporter, WebhooksHooksInput } from "../hooks/contract.ts";
17
+ import { type WebhooksStorage } from "../storage/contract.ts";
18
+ import { type WebhooksCorsOptions } from "./cors.ts";
19
+ import { type WebhooksPortalOptions } from "./routes.ts";
20
+ /** Options for the panel handler (shared by every framework adapter). */
21
+ export interface WebhooksPanelOptions {
22
+ /**
23
+ * Control-plane store: applications, event types, endpoints, messages, audit.
24
+ * Required unless a prebuilt `core` is supplied (which carries its own
25
+ * storage, and its `queueStorage`, as the source of truth).
26
+ */
27
+ storage?: WebhooksStorage;
28
+ /** Store for deliveries, attempts, and the due index. Defaults to `storage`. */
29
+ queueStorage?: WebhooksStorage;
30
+ /** Mount prefix, e.g. `"/webhooks"`. Default `""` (root). */
31
+ basePath?: string;
32
+ /** Authentication provider. Default: anonymous (no auth). */
33
+ auth?: AuthProvider;
34
+ /** Authorization provider. Default: allow all. */
35
+ authorization?: AuthorizationProvider;
36
+ /**
37
+ * Portal-token composition: requests bearing a valid `whpt_…` token act as a
38
+ * portal principal scoped to the token's application. Mint tokens
39
+ * server-side with `createPortalToken`. See
40
+ * {@link ./routes.WebhooksPortalOptions}.
41
+ */
42
+ portal?: WebhooksPortalOptions;
43
+ /** Block all mutating operations when true. */
44
+ readonly?: boolean;
45
+ /** UI title shown in the page and bootstrap config. */
46
+ title?: string;
47
+ /** Logo image URL shown in the navbar in place of the title wordmark. */
48
+ logoUrl?: string;
49
+ /** Override the directory the bundled UI is served from (defaults to `./ui` beside this module). */
50
+ uiDir?: string;
51
+ /** Reuse an existing core instead of constructing one. */
52
+ core?: WebhooksCore;
53
+ /**
54
+ * Control-plane hooks fired around admin mutations (see
55
+ * {@link ../hooks/contract.WebhooksHooks}). Ignored when `core` is supplied —
56
+ * configure hooks on that core instead.
57
+ */
58
+ hooks?: WebhooksHooksInput;
59
+ /** Reporter for errors thrown by `after` hooks. Default: `console.warn`. */
60
+ onHookError?: HookErrorReporter;
61
+ /**
62
+ * Message/audit retention policy. Ignored when a prebuilt `core` is supplied —
63
+ * configure it on that core instead. See {@link ../core.RetentionOptions}.
64
+ */
65
+ retention?: RetentionOptions;
66
+ /**
67
+ * Fire-and-forget sink invoked for **every** delivery attempt (metrics tap).
68
+ * Ignored when `core` is supplied. See {@link ../delivery-sink.DeliveryListener}.
69
+ */
70
+ onDelivery?: DeliveryListener;
71
+ /** Reporter for errors thrown by `onDelivery`. Default: `console.warn`. */
72
+ onDeliveryError?: DeliveryErrorReporter;
73
+ /**
74
+ * Enable CORS on the handler itself — answers `OPTIONS` preflights and attaches
75
+ * `Access-Control-*` headers to every response, so a **cross-origin** embed
76
+ * works regardless of the host framework. See {@link ./cors.WebhooksCorsOptions}.
77
+ */
78
+ cors?: WebhooksCorsOptions;
79
+ /**
80
+ * Delivery-engine configuration. By default the panel creates **and starts**
81
+ * a dispatcher so deliveries flow the moment the panel is mounted. Pass
82
+ * `false` to skip it entirely (split-worker deployments where a separate
83
+ * process runs `xtandard-webhooks dispatch` against the same storage).
84
+ */
85
+ dispatcher?: DispatcherOptions | false;
86
+ }
87
+ /** Return shape of {@link createFetchHandler}. */
88
+ export interface CreateFetchHandlerResult {
89
+ /** Web-standard request handler. */
90
+ fetch(request: Request): Promise<Response>;
91
+ /** The underlying core (handy for `publish()`, tests, CLI, and standalone wiring). */
92
+ core: WebhooksCore;
93
+ /** The started dispatcher, or `null` when `dispatcher: false`. */
94
+ dispatcher: Dispatcher | null;
95
+ /**
96
+ * The admin API as an OpenAPI 3.1 document (also served at `{basePath}/api/openapi.json`).
97
+ * Merge it into your host app's docs — e.g. Elysia `@elysiajs/openapi` `references`.
98
+ */
99
+ openapi(): Record<string, unknown>;
100
+ }
101
+ /**
102
+ * Build the panel fetch handler.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { createFetchHandler } from "@xtandard/webhooks";
107
+ * import { createFileStorage } from "@xtandard/webhooks/storage/file";
108
+ *
109
+ * const storage = createFileStorage({ dir: "./data/webhooks" });
110
+ * const { fetch, core } = createFetchHandler({
111
+ * storage,
112
+ * basePath: "/webhooks",
113
+ * title: "Acme Webhooks",
114
+ * });
115
+ *
116
+ * Bun.serve({ port: 3000, fetch });
117
+ * // Elsewhere in the app:
118
+ * await core.publish("acme", { eventType: "invoice.paid", payload: { id: "inv_1" } });
119
+ * ```
120
+ */
121
+ export declare function createFetchHandler(options: WebhooksPanelOptions): CreateFetchHandlerResult;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenAPI 3.1 description of the admin JSON API.
3
+ *
4
+ * Exposed two ways, mirroring the flags panel:
5
+ * - served at `GET {basePath}/api/openapi.json` for standalone tools (Scalar,
6
+ * Swagger UI, Postman, codegen);
7
+ * - returned by `createFetchHandler(...).openapi()` so you can MERGE it into
8
+ * your host app's OpenAPI document (e.g. Elysia `@elysiajs/openapi`
9
+ * `references`, or Hono's OpenAPI).
10
+ *
11
+ * Hand-authored — no generation dependency (flags precedent).
12
+ *
13
+ * @module
14
+ */
15
+ /** Options for {@link buildOpenApiDocument}. */
16
+ export interface OpenApiOptions {
17
+ /** Mount prefix used in the `servers` url (e.g. `"/webhooks"`). */
18
+ basePath?: string;
19
+ /** Document title. */
20
+ title?: string;
21
+ /** Document version (defaults to the package's API version). */
22
+ version?: string;
23
+ }
24
+ /** Build the OpenAPI 3.1 document for the admin API. Pure — safe to call anywhere. */
25
+ export declare function buildOpenApiDocument(options?: OpenApiOptions): Record<string, unknown>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Renders the SPA `index.html`: injects a `<base>` tag so relative asset URLs
3
+ * resolve under any mount path, and a bootstrap `window.__WEBHOOKS_CONFIG__`
4
+ * blob. Falls back to a minimal built-in page when the UI bundle is absent
5
+ * (e.g. before `bun run build:ui`, or in headless tests).
6
+ *
7
+ * @module
8
+ */
9
+ /** Bootstrap config injected into the page and exposed at `/config`. */
10
+ export interface BootstrapConfig {
11
+ title: string;
12
+ basePath: string;
13
+ readonly: boolean;
14
+ /** Optional logo image URL shown in the navbar in place of the title wordmark. */
15
+ logoUrl?: string;
16
+ }
17
+ /** Read, inject into, and return the SPA index HTML (or a fallback). */
18
+ export declare function renderIndexHtml(uiDir: string, config: BootstrapConfig): Promise<string>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * JSON admin API. A tiny method+pattern router over {@link WebhooksCore}, wired
3
+ * to authentication, authorization, and portal-token scoping. Returns `null`
4
+ * for non-API paths so the caller can fall through to static-asset / SPA
5
+ * handling.
6
+ *
7
+ * The consumer **portal** is not a separate route set — a request bearing a
8
+ * valid `Authorization: Bearer whpt_…` token hits this same API with its
9
+ * authorization force-scoped to the token's application and allowed actions
10
+ * (the host's own auth/authorization providers are bypassed for portal
11
+ * principals; portal scoping wins).
12
+ *
13
+ * @module
14
+ */
15
+ import type { AuthProvider } from "../auth/contract.ts";
16
+ import type { AuthorizationProvider, WebhooksAction } from "../authorization/contract.ts";
17
+ import { type WebhooksCore } from "../core.ts";
18
+ /**
19
+ * Portal composition for the panel: when set, requests bearing a valid
20
+ * `whpt_…` token act as a portal principal scoped to the token's application.
21
+ */
22
+ export interface WebhooksPortalOptions {
23
+ /** The HMAC secret portal tokens are minted with (`createPortalToken`). */
24
+ secret: string;
25
+ /**
26
+ * Actions a portal principal may perform (always confined to the token's
27
+ * application). Defaults to {@link DEFAULT_PORTAL_ACTIONS}.
28
+ */
29
+ allow?: WebhooksAction[];
30
+ }
31
+ /**
32
+ * The default portal grant: manage own endpoints (including secrets), inspect
33
+ * own messages/deliveries, retry, and read the event-type catalog.
34
+ */
35
+ export declare const DEFAULT_PORTAL_ACTIONS: readonly WebhooksAction[];
36
+ /** Everything the API router needs. */
37
+ export interface ApiContext {
38
+ core: WebhooksCore;
39
+ auth: AuthProvider;
40
+ authorization: AuthorizationProvider;
41
+ title: string;
42
+ readonly: boolean;
43
+ basePath: string;
44
+ logoUrl?: string;
45
+ /** Portal-token composition (see {@link WebhooksPortalOptions}). */
46
+ portal?: WebhooksPortalOptions;
47
+ }
48
+ /**
49
+ * Handle an API request. `path` is already base-path-stripped. Returns a
50
+ * `Response` for API/config routes, or `null` if the path is not an API route.
51
+ */
52
+ export declare function handleApiRequest(request: Request, path: string, ctx: ApiContext): Promise<Response | null>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Serves the bundled SPA's static assets from the UI output directory. Maps file
3
+ * extensions to content types and guards against path traversal.
4
+ *
5
+ * @module
6
+ */
7
+ /** True if the path has a file extension (so a miss should 404 rather than fall through to the SPA). */
8
+ export declare function looksLikeAsset(path: string): boolean;
9
+ /**
10
+ * Try to serve `path` (base-path-stripped, leading `/`) as a static file from
11
+ * `uiDir`. Returns a `Response`, or `null` if the file does not exist.
12
+ */
13
+ export declare function serveStaticAsset(uiDir: string, path: string): Promise<Response | null>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Standard Webhooks symmetric (v1) signing and verification.
3
+ *
4
+ * Pure and zero-dependency (Web Crypto only) — safe on the request path and in
5
+ * any WinterCG runtime. Shared by the sender (the dispatcher signs outgoing
6
+ * deliveries) and the receiver subpath (`@xtandard/webhooks/receiver`), and
7
+ * compatible with any Standard Webhooks implementation, including Svix.
8
+ *
9
+ * The wire contract (https://www.standardwebhooks.com):
10
+ *
11
+ * - secrets are `whsec_` + base64-encoded key material (24–64 bytes decoded)
12
+ * - the signed content is `${id}.${timestamp}.${body}`
13
+ * - the signature is `v1,` + base64(HMAC-SHA256(key, signedContent))
14
+ * - `webhook-signature` may carry several space-separated signatures (secret
15
+ * rotation); a receiver accepts if **any** matches
16
+ * - `webhook-timestamp` (unix seconds) must be within tolerance, both ways
17
+ *
18
+ * @module
19
+ */
20
+ import type { WebhookEnvelope } from "./schema.ts";
21
+ export type { WebhookEnvelope };
22
+ /** Prefix of every symmetric Standard Webhooks secret. */
23
+ export declare const SECRET_PREFIX = "whsec_";
24
+ /** Thrown by {@link verify} / receiver helpers when a webhook fails verification. */
25
+ export declare class WebhookVerificationError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ /**
29
+ * Generate a new signing secret: `whsec_` + base64 of 24 crypto-random bytes.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { generateSecret } from "@xtandard/webhooks/signing";
34
+ *
35
+ * const secret = generateSecret(); // "whsec_…"
36
+ * ```
37
+ */
38
+ export declare function generateSecret(): string;
39
+ /**
40
+ * Sign one delivery attempt with one secret: `v1,` + base64(HMAC-SHA256 over
41
+ * `${id}.${timestamp}.${body}`). `timestamp` is unix **seconds**.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { sign } from "@xtandard/webhooks/signing";
46
+ *
47
+ * const signature = await sign(secret, "msg_…", 1720000000, body);
48
+ * // "v1,K5oZ…"
49
+ * ```
50
+ */
51
+ export declare function sign(secret: string, id: string, timestamp: number, body: string): Promise<string>;
52
+ /**
53
+ * Build the full `webhook-signature` header value: one signature per secret,
54
+ * space-separated (multiple entries appear during secret rotation).
55
+ */
56
+ export declare function signatureHeader(secrets: string[], id: string, timestamp: number, body: string): Promise<string>;
57
+ /** Input to {@link verify}. */
58
+ export interface VerifyInput {
59
+ /** The raw request body, **exactly** as received (no re-serialization). */
60
+ payload: string;
61
+ /** Request headers; name lookup is case-insensitive. */
62
+ headers: Record<string, string>;
63
+ /** Candidate secret(s) — pass several during rotation; any match passes. */
64
+ secret: string | string[];
65
+ /** Allowed clock skew in seconds, both past and future. Default `300`. */
66
+ toleranceSeconds?: number;
67
+ /** Current unix time in seconds — injectable for tests. */
68
+ now?: number;
69
+ }
70
+ /**
71
+ * Verify an incoming Standard Webhooks request (from **any** compliant sender,
72
+ * not just this package). Checks the timestamp tolerance, then compares every
73
+ * `v1,` signature in the header against every candidate secret in constant
74
+ * time. Returns the parsed envelope on success; throws
75
+ * {@link WebhookVerificationError} otherwise.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { verify } from "@xtandard/webhooks/signing";
80
+ *
81
+ * const envelope = await verify({
82
+ * payload: rawBody,
83
+ * headers: { "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig },
84
+ * secret: "whsec_…",
85
+ * });
86
+ * ```
87
+ */
88
+ export declare function verify(input: VerifyInput): Promise<WebhookEnvelope>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Storage contracts. The base {@link WebhooksStorage} is intentionally tiny —
3
+ * four async methods — so users can bring their own backend. Optional
4
+ * capabilities (watch, transactions, compare-and-swap, native delivery-queue
5
+ * claiming) are separate interfaces that adapters may implement and the core
6
+ * feature-detects.
7
+ *
8
+ * @module
9
+ */
10
+ import type { Delivery } from "../schema.ts";
11
+ /** The minimal key/value contract every storage backend must satisfy. */
12
+ export interface WebhooksStorage {
13
+ /** Read a value, or `null` if absent. */
14
+ getItem<T>(key: string): Promise<T | null>;
15
+ /** Write a value (overwriting any existing). */
16
+ setItem<T>(key: string, value: T): Promise<void>;
17
+ /** Delete a key (no-op if absent). */
18
+ removeItem(key: string): Promise<void>;
19
+ /** List all keys beginning with `prefix`. */
20
+ getKeys(prefix: string): Promise<string[]>;
21
+ }
22
+ /** A storage change event delivered to {@link WatchableWebhooksStorage.watch} callbacks. */
23
+ export interface StorageChangeEvent {
24
+ type: "update" | "remove";
25
+ key: string;
26
+ }
27
+ /** Storage that can push change notifications (e.g. Redis pub/sub, fs.watch). */
28
+ export interface WatchableWebhooksStorage extends WebhooksStorage {
29
+ /**
30
+ * Subscribe to changes under `prefix`. Resolves to an unsubscribe function.
31
+ */
32
+ watch(prefix: string, callback: (event: StorageChangeEvent) => void): Promise<() => void>;
33
+ }
34
+ /** Storage that supports atomic multi-key transactions. */
35
+ export interface TransactionalWebhooksStorage extends WebhooksStorage {
36
+ transaction<T>(callback: (tx: WebhooksStorage) => Promise<T>): Promise<T>;
37
+ }
38
+ /** Storage that supports optimistic concurrency via compare-and-swap. */
39
+ export interface CompareAndSwapWebhooksStorage extends WebhooksStorage {
40
+ compareAndSwap<T>(input: {
41
+ key: string;
42
+ expected: T | null;
43
+ next: T;
44
+ }): Promise<boolean>;
45
+ }
46
+ /**
47
+ * Storage that can natively claim due deliveries (sorted-set / `SKIP LOCKED`
48
+ * class backends). When present, the dispatcher delegates claiming entirely to
49
+ * the adapter instead of scanning the generic due index.
50
+ *
51
+ * The claim contract: atomically transition each returned delivery to
52
+ * `status: "delivering"` with `leaseUntil = now + leaseMs`, reposition its due
53
+ * entry at the lease expiry (so a crashed claimer's work re-surfaces), and
54
+ * never return the same delivery to two concurrent claimers.
55
+ */
56
+ export interface DeliveryQueueStorage extends WebhooksStorage {
57
+ claimDue(input: {
58
+ now: string;
59
+ limit: number;
60
+ leaseMs: number;
61
+ }): Promise<Delivery[]>;
62
+ }
63
+ /** Runtime feature-detection: does this storage implement `watch`? */
64
+ export declare function isWatchable(storage: WebhooksStorage): storage is WatchableWebhooksStorage;
65
+ /** Runtime feature-detection: does this storage implement `transaction`? */
66
+ export declare function isTransactional(storage: WebhooksStorage): storage is TransactionalWebhooksStorage;
67
+ /** Runtime feature-detection: does this storage implement `compareAndSwap`? */
68
+ export declare function isCompareAndSwap(storage: WebhooksStorage): storage is CompareAndSwapWebhooksStorage;
69
+ /** Runtime feature-detection: does this storage implement `claimDue`? */
70
+ export declare function hasDeliveryQueue(storage: WebhooksStorage): storage is DeliveryQueueStorage;
71
+ /**
72
+ * Helper for adapters whose subpath requires an optional peer dependency. Throws
73
+ * a clear, actionable error when the peer is missing.
74
+ */
75
+ export declare function requirePeer(name: string, subpath: string): never;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Runtime validation for control-plane inputs, built on `valibot`.
3
+ *
4
+ * This is the **admin path** only — `publish()` performs its own minimal
5
+ * checks and the wire/receiver path never imports this module, so `valibot`
6
+ * stays off the hot paths. Validation combines structural parsing (valibot)
7
+ * with semantic checks (URL policy, reserved header names, reserved keys).
8
+ *
9
+ * @module
10
+ */
11
+ /** Allowed characters for application keys and event type names. */
12
+ export declare const KEY_REGEX: RegExp;
13
+ /**
14
+ * Validate a caller-supplied string that becomes a **storage key segment**
15
+ * (e.g. an idempotency key). Rejects anything that could escape its namespace
16
+ * or traverse the filesystem on the file adapter: characters outside
17
+ * {@link KEY_REGEX}, and the dot-only segments `.` / `..`. Returns the issue
18
+ * list (empty when safe) so callers can fold it into a {@link ValidationError}.
19
+ */
20
+ export declare function validateKeySegment(value: string, path: string): ValidationIssue[];
21
+ /**
22
+ * Header names owned by the Standard Webhooks wire contract. Endpoints may not
23
+ * override them via static headers.
24
+ */
25
+ export declare const RESERVED_HEADERS: readonly ["webhook-id", "webhook-timestamp", "webhook-signature"];
26
+ /** A single validation problem with a dotted path into the offending data. */
27
+ export interface ValidationIssue {
28
+ path: string;
29
+ message: string;
30
+ }
31
+ /** Result of the `validate*` functions. */
32
+ export interface ValidationResult {
33
+ valid: boolean;
34
+ errors: ValidationIssue[];
35
+ }
36
+ /** Raised by {@link assertValid} when an input fails validation. Maps to HTTP 422. */
37
+ export declare class ValidationError extends Error {
38
+ readonly errors: ValidationIssue[];
39
+ constructor(errors: ValidationIssue[]);
40
+ }
41
+ /** Throw a {@link ValidationError} when `result` is invalid. */
42
+ export declare function assertValid(result: ValidationResult): void;
43
+ /**
44
+ * Validate an {@link Application}: structure + reserved-key check.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { validateApplication } from "@xtandard/webhooks";
49
+ *
50
+ * const result = validateApplication({ key: "acme" });
51
+ * // result.valid === true
52
+ * ```
53
+ */
54
+ export declare function validateApplication(input: unknown, basePath?: string): ValidationResult;
55
+ /** Validate an {@link EventType}: structural only (the name regex carries the semantics). */
56
+ export declare function validateEventType(input: unknown, basePath?: string): ValidationResult;
57
+ /** Options that relax/extend the endpoint URL policy. */
58
+ export interface UrlPolicyOptions {
59
+ /** Allow `http:` for non-localhost hosts (dev only; default `false`). */
60
+ allowInsecureUrls?: boolean;
61
+ /** Extra host-supplied gate; return `false` to reject (e.g. an SSRF denylist). */
62
+ urlPolicy?: (url: string) => boolean;
63
+ }
64
+ /**
65
+ * Validate an endpoint destination URL: parseable, `https:` (or `http:` for
66
+ * localhost, or anywhere when `allowInsecureUrls`), no embedded credentials,
67
+ * and passing the host's optional `urlPolicy` gate.
68
+ */
69
+ export declare function validateEndpointUrl(url: string, options?: UrlPolicyOptions, basePath?: string): ValidationResult;
70
+ /**
71
+ * Validate an {@link Endpoint} input: structure, URL policy, and static-header
72
+ * restrictions (the Standard Webhooks headers are reserved).
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { validateEndpoint } from "@xtandard/webhooks";
77
+ *
78
+ * const result = validateEndpoint({ url: "https://api.example.com/hooks" });
79
+ * // result.valid === true
80
+ * ```
81
+ */
82
+ export declare function validateEndpoint(input: unknown, options?: UrlPolicyOptions, basePath?: string): ValidationResult;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Package version, used for the default dispatcher `user-agent`. Bumped by the
3
+ * release flow alongside `package.json`.
4
+ *
5
+ * @module
6
+ */
7
+ /** The published package version. */
8
+ export declare const VERSION = "0.1.3";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Where Base UI portals (Select/Combobox popups, Dialogs) render.
3
+ *
4
+ * In the **standalone SPA** the stylesheet is global, so portals default to
5
+ * `document.body` and look right. In the **React embed** the stylesheet is
6
+ * scoped under `.xtandard-webhooks` (see `scripts/scope-embed-css.ts`), so
7
+ * anything portaled to `document.body` would fall outside the scope and render
8
+ * unstyled. The embed therefore points portals at its own wrapper element via
9
+ * {@link setPortalContainerRef}, keeping popups inside the scoped subtree.
10
+ *
11
+ * Module-level singleton (same trade-off as `setApiBase`/`setApiToken`): one
12
+ * embed per page. Unset → portals default to `document.body`.
13
+ *
14
+ * @module
15
+ */
16
+ import type { RefObject } from "react";
17
+ /** Point portals at `ref` (the embed wrapper). Pass `undefined` to reset to the body default. */
18
+ export declare function setPortalContainerRef(ref: RefObject<HTMLElement | null> | undefined): void;
19
+ /**
20
+ * The current portal container ref, or `undefined` when none is set (Base UI
21
+ * then defaults to `document.body`). Pass the result to a Base UI
22
+ * `*.Portal container={…}` prop.
23
+ */
24
+ export declare function portalContainerRef(): RefObject<HTMLElement | null> | undefined;