@happyvertical/smrt-webhooks 0.0.0-stage → 0.55.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +23 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +141 -2
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/admin.d.ts +25 -0
  8. package/dist/admin.d.ts.map +1 -0
  9. package/dist/admin.js +2 -0
  10. package/dist/change-feed.d.ts +25 -0
  11. package/dist/change-feed.d.ts.map +1 -0
  12. package/dist/chunks/admin-413jve1I.js +318 -0
  13. package/dist/chunks/admin-413jve1I.js.map +1 -0
  14. package/dist/chunks/dispatcher-C5AqtgtK.js +248 -0
  15. package/dist/chunks/dispatcher-C5AqtgtK.js.map +1 -0
  16. package/dist/dispatcher.d.ts +23 -0
  17. package/dist/dispatcher.d.ts.map +1 -0
  18. package/dist/events.d.ts +6 -0
  19. package/dist/events.d.ts.map +1 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +148 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/manifest.json +1032 -0
  25. package/dist/models.d.ts +43 -0
  26. package/dist/models.d.ts.map +1 -0
  27. package/dist/recipes.d.ts +23 -0
  28. package/dist/recipes.d.ts.map +1 -0
  29. package/dist/security.d.ts +15 -0
  30. package/dist/security.d.ts.map +1 -0
  31. package/dist/smrt-knowledge.json +418 -0
  32. package/dist/store.d.ts +26 -0
  33. package/dist/store.d.ts.map +1 -0
  34. package/dist/svelte/WebhookAdmin.svelte +113 -0
  35. package/dist/svelte/WebhookAdmin.svelte.d.ts +22 -0
  36. package/dist/svelte/WebhookAdmin.svelte.d.ts.map +1 -0
  37. package/dist/svelte/index.d.ts +2 -0
  38. package/dist/svelte/index.d.ts.map +1 -0
  39. package/dist/svelte/index.js +1 -0
  40. package/dist/types.d.ts +27 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +0 -0
  43. package/package.json +75 -3
@@ -0,0 +1,43 @@
1
+ import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core';
2
+ import { type JobExecutionContext } from '@happyvertical/smrt-jobs';
3
+ /** Server-only subscription. Hosts authorize administration before accessing it. */
4
+ export declare class WebhookSubscription extends SmrtObject {
5
+ tenantId: string;
6
+ url: string;
7
+ events: string[];
8
+ models: string[];
9
+ secret: string;
10
+ enabled: boolean;
11
+ save(): Promise<this>;
12
+ }
13
+ export declare class WebhookSubscriptionCollection extends SmrtCollection<WebhookSubscription> {
14
+ static readonly _itemClass: typeof WebhookSubscription;
15
+ }
16
+ /** Durable outbox and safe outcome metadata; payload never appears in public projections. */
17
+ export declare class WebhookDelivery extends SmrtObject {
18
+ tenantId: string;
19
+ subscriptionId: string;
20
+ eventId: string;
21
+ payload: string;
22
+ status: string;
23
+ generation: number;
24
+ attempts: number;
25
+ responseStatus: number | null;
26
+ error: string | null;
27
+ leaseToken: string | null;
28
+ leaseUntil: Date | null;
29
+ }
30
+ export declare class WebhookDeliveryCollection extends SmrtCollection<WebhookDelivery> {
31
+ static readonly _itemClass: typeof WebhookDelivery;
32
+ }
33
+ /** Jobs queue entry point; persisted arguments contain IDs only. */
34
+ export declare class WebhookDeliveryTask extends SmrtObject {
35
+ deliver(args?: Record<string, unknown>, _context?: JobExecutionContext): Promise<void>;
36
+ }
37
+ /** Tenant-bound durable cursor for replayable model change-feed publication. */
38
+ export declare class WebhookCursor extends SmrtObject {
39
+ tenantId: string;
40
+ runtime: string;
41
+ position: number;
42
+ }
43
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,cAAc,EACd,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAIlC,oFAAoF;AACpF,qBAOa,mBAAoB,SAAQ,UAAU;IACrC,QAAQ,EAAE,MAAM,CAAM;IACO,GAAG,EAAE,MAAM,CAAM;IACjC,MAAM,EAAE,MAAM,EAAE,CAAM;IACtB,MAAM,EAAE,MAAM,EAAE,CAAM;IACW,MAAM,EAAE,MAAM,CAAM;IACnC,OAAO,EAAE,OAAO,CAAQ;IAEpD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAcrC;AACD,qBAAa,6BAA8B,SAAQ,cAAc,CAAC,mBAAmB,CAAC;IACpF,MAAM,CAAC,QAAQ,CAAC,UAAU,6BAAuB;CAClD;AAED,6FAA6F;AAC7F,qBAca,eAAgB,SAAQ,UAAU;IACjC,QAAQ,EAAE,MAAM,CAAM;IAKlC,cAAc,EAAE,MAAM,CAAM;IACa,OAAO,EAAE,MAAM,CAAM;IACJ,OAAO,EAAE,MAAM,CACpE;IACwC,MAAM,EAAE,MAAM,CAAa;IAChC,UAAU,EAAE,MAAM,CAAK;IACvB,QAAQ,EAAE,MAAM,CAAK;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAClE;IACkC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IACX,UAAU,EAChE,MAAM,GACN,IAAI,CAAQ;IAC6B,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;CAC7E;AACD,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;CAC9C;AAED,oEAAoE;AACpE,qBAMa,mBAAoB,SAAQ,UAAU;IAE3C,OAAO,CACX,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAClC,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,IAAI,CAAC;CAIjB;AAED,gFAAgF;AAChF,qBAca,aAAc,SAAQ,UAAU;IAC/B,QAAQ,EAAE,MAAM,CAAM;IACO,OAAO,EAAE,MAAM,CAAM;IACtB,QAAQ,EAAE,MAAM,CAAK;CAC9D"}
@@ -0,0 +1,23 @@
1
+ import { SmrtRecipe } from '@happyvertical/smrt-core';
2
+ import { WebhookCursor, WebhookDelivery, WebhookDeliveryTask, WebhookSubscription } from './models.js';
3
+ /** Server-backed administration through authenticated host callbacks. */
4
+ export declare class WebhooksRecipe extends SmrtRecipe {
5
+ static id: string;
6
+ static label: string;
7
+ static summary: string;
8
+ static models: (typeof WebhookSubscription | typeof WebhookDelivery | typeof WebhookDeliveryTask | typeof WebhookCursor)[];
9
+ static group: {
10
+ id: string;
11
+ label: string;
12
+ summary: string;
13
+ };
14
+ static runtime: "server";
15
+ static surfaces: readonly [{
16
+ readonly kind: "settings-panel";
17
+ readonly export: "@happyvertical/smrt-webhooks/svelte#WebhookAdmin";
18
+ readonly label: "Outbound webhooks";
19
+ }];
20
+ static nav: never[];
21
+ static help: string;
22
+ }
23
+ //# sourceMappingURL=recipes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AACrB,yEAAyE;AACzE,qBAAa,cAAe,SAAQ,UAAU;IAC5C,MAAM,CAAC,EAAE,SAA2B;IACpC,MAAM,CAAC,KAAK,SAAuB;IACnC,MAAM,CAAC,OAAO,SAC+D;IAC7E,MAAM,CAAC,MAAM,8GAKX;IACF,MAAM,CAAC,KAAK;;;;MAIV;IACF,MAAM,CAAC,OAAO,EAAG,QAAQ,CAAU;IACnC,MAAM,CAAC,QAAQ;;;;OAMJ;IAEX,MAAM,CAAC,GAAG,UAAM;IAChB,MAAM,CAAC,IAAI,SAA0B;CACtC"}
@@ -0,0 +1,15 @@
1
+ /** Conservative public-unicast allowlist. Transition/mapped IPv6 is excluded. */
2
+ export declare function isPublicAddress(address: string): boolean;
3
+ export declare function assertWebhookUrl(value: string): URL;
4
+ export declare function assertHmacSecret(secret: string): void;
5
+ export interface WebhookRequest {
6
+ url: string;
7
+ body: string;
8
+ headers: Record<string, string>;
9
+ }
10
+ export type WebhookTransport = (request: WebhookRequest) => Promise<number>;
11
+ /** Validate every DNS answer, then pin the socket lookup; TLS still verifies the hostname.
12
+ * No proxy, redirects, pooled sockets, response body buffering, or second DNS lookup.
13
+ */
14
+ export declare const sendPublicWebhook: WebhookTransport;
15
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgCxD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAuBnD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQrD;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AACD,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE5E;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,gBAwD/B,CAAC"}
@@ -0,0 +1,418 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "sensitiveFieldsExcluded": true,
4
+ "generatedAt": "1970-01-01T00:00:00.000Z",
5
+ "packageName": "@happyvertical/smrt-webhooks",
6
+ "packageVersion": "0.55.10",
7
+ "sourceManifestPath": "dist/manifest.json",
8
+ "agentDocPath": "AGENTS.md",
9
+ "sourceHashes": {
10
+ "manifest": "6de4a4cc2ec7b5158d5b5549ce47bf93fb9cbbfa40f3711335132d2cc70f42e5",
11
+ "packageJson": "92c0b6f55dec98164fa19f30da19d6f1c97b2030f53ec77b8e9055755388bbd9",
12
+ "agents": "f67b2a340106cf4b4b44c6b2bf918086286236ac3cf34b03b24b1db6e17b40a5"
13
+ },
14
+ "exports": [
15
+ ".",
16
+ "./admin",
17
+ "./manifest",
18
+ "./manifest.json",
19
+ "./svelte"
20
+ ],
21
+ "dependencies": {
22
+ "@happyvertical/smrt-core": "workspace:*",
23
+ "@happyvertical/smrt-jobs": "workspace:*",
24
+ "@happyvertical/smrt-tenancy": "workspace:*",
25
+ "@happyvertical/sql": "catalog:",
26
+ "@happyvertical/smrt-ui": "workspace:*",
27
+ "@happyvertical/smrt-vitest": "workspace:*",
28
+ "@sveltejs/package": "^2.5.8",
29
+ "@types/node": "24.13.6",
30
+ "svelte": "^5.57.0",
31
+ "svelte-check": "^4.7.6",
32
+ "typescript": "6.0.3",
33
+ "vite": "8.1.4",
34
+ "vitest": "4.1.10",
35
+ "@playwright/test": "^1.63.0",
36
+ "@sveltejs/vite-plugin-svelte": "^7.3.0"
37
+ },
38
+ "smrtDependencies": [
39
+ "@happyvertical/smrt-core",
40
+ "@happyvertical/smrt-jobs",
41
+ "@happyvertical/smrt-tenancy",
42
+ "@happyvertical/smrt-ui",
43
+ "@happyvertical/smrt-vitest"
44
+ ],
45
+ "sdkDependencies": [
46
+ "@happyvertical/sql"
47
+ ],
48
+ "tags": [
49
+ "cross-package"
50
+ ],
51
+ "risks": [
52
+ "sensitive-fields-excluded"
53
+ ],
54
+ "objects": [
55
+ {
56
+ "name": "WebhookSubscription",
57
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookSubscription",
58
+ "collection": "webhooksubscriptions",
59
+ "description": "Server-only subscription. Hosts authorize administration before accessing it.",
60
+ "tableName": "_smrt_webhook_subscriptions",
61
+ "packageName": "@happyvertical/smrt-webhooks",
62
+ "extends": "SmrtObject",
63
+ "fields": [
64
+ {
65
+ "name": "tenantId",
66
+ "type": "text",
67
+ "required": true,
68
+ "columnType": "UUID"
69
+ },
70
+ {
71
+ "name": "url",
72
+ "type": "text",
73
+ "required": true,
74
+ "columnType": "TEXT"
75
+ },
76
+ {
77
+ "name": "events",
78
+ "type": "json",
79
+ "required": false,
80
+ "columnType": "JSON"
81
+ },
82
+ {
83
+ "name": "models",
84
+ "type": "json",
85
+ "required": false,
86
+ "columnType": "JSON"
87
+ },
88
+ {
89
+ "name": "enabled",
90
+ "type": "boolean",
91
+ "required": false,
92
+ "columnType": "BOOLEAN",
93
+ "default": true
94
+ }
95
+ ],
96
+ "relationships": [],
97
+ "methods": [
98
+ "save"
99
+ ],
100
+ "methodSignatures": [
101
+ {
102
+ "name": "save",
103
+ "async": true,
104
+ "returns": "Promise<this>"
105
+ }
106
+ ],
107
+ "tenant": {
108
+ "scoped": true,
109
+ "mode": "required",
110
+ "field": "tenantId"
111
+ },
112
+ "conflictColumns": [
113
+ "tenant_id",
114
+ "slug",
115
+ "context"
116
+ ],
117
+ "surfaces": [],
118
+ "withheldSurfaces": [
119
+ {
120
+ "kind": "api",
121
+ "operation": "save",
122
+ "code": "api-disabled",
123
+ "reason": "api is disabled",
124
+ "objectName": "@happyvertical/smrt-webhooks:WebhookSubscription"
125
+ }
126
+ ],
127
+ "relationshipFeatures": [
128
+ "uuidColumns"
129
+ ],
130
+ "tags": [],
131
+ "risks": []
132
+ },
133
+ {
134
+ "name": "WebhookSubscriptionCollection",
135
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookSubscriptionCollection",
136
+ "collection": "webhooksubscriptions",
137
+ "tableName": "_smrt_webhook_subscriptions",
138
+ "packageName": "@happyvertical/smrt-webhooks",
139
+ "extends": "SmrtCollection",
140
+ "fields": [],
141
+ "relationships": [],
142
+ "methods": [],
143
+ "surfaces": [],
144
+ "relationshipFeatures": [
145
+ "uuidColumns"
146
+ ],
147
+ "tags": [],
148
+ "risks": []
149
+ },
150
+ {
151
+ "name": "WebhookDelivery",
152
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookDelivery",
153
+ "collection": "webhookdeliveries",
154
+ "description": "Durable outbox and safe outcome metadata; payload never appears in public projections.",
155
+ "tableName": "_smrt_webhook_deliveries",
156
+ "packageName": "@happyvertical/smrt-webhooks",
157
+ "extends": "SmrtObject",
158
+ "fields": [
159
+ {
160
+ "name": "tenantId",
161
+ "type": "text",
162
+ "required": true,
163
+ "columnType": "UUID"
164
+ },
165
+ {
166
+ "name": "subscriptionId",
167
+ "type": "foreignKey",
168
+ "required": false,
169
+ "related": "WebhookSubscription",
170
+ "columnType": "UUID"
171
+ },
172
+ {
173
+ "name": "eventId",
174
+ "type": "text",
175
+ "required": true,
176
+ "columnType": "TEXT"
177
+ },
178
+ {
179
+ "name": "status",
180
+ "type": "text",
181
+ "required": false,
182
+ "columnType": "TEXT",
183
+ "default": "pending"
184
+ },
185
+ {
186
+ "name": "generation",
187
+ "type": "integer",
188
+ "required": false,
189
+ "columnType": "INTEGER",
190
+ "default": 0
191
+ },
192
+ {
193
+ "name": "attempts",
194
+ "type": "integer",
195
+ "required": false,
196
+ "columnType": "INTEGER",
197
+ "default": 0
198
+ },
199
+ {
200
+ "name": "responseStatus",
201
+ "type": "integer",
202
+ "required": false,
203
+ "columnType": "INTEGER"
204
+ },
205
+ {
206
+ "name": "error",
207
+ "type": "text",
208
+ "required": false,
209
+ "columnType": "TEXT"
210
+ },
211
+ {
212
+ "name": "leaseUntil",
213
+ "type": "datetime",
214
+ "required": false,
215
+ "columnType": "TIMESTAMP"
216
+ }
217
+ ],
218
+ "relationships": [
219
+ {
220
+ "name": "subscriptionId",
221
+ "type": "foreignKey",
222
+ "required": false,
223
+ "related": "WebhookSubscription",
224
+ "columnType": "UUID"
225
+ }
226
+ ],
227
+ "methods": [],
228
+ "tenant": {
229
+ "scoped": true,
230
+ "mode": "required",
231
+ "field": "tenantId"
232
+ },
233
+ "conflictColumns": [
234
+ "tenant_id",
235
+ "slug",
236
+ "context"
237
+ ],
238
+ "surfaces": [],
239
+ "relationshipFeatures": [
240
+ "foreignKey",
241
+ "uuidColumns"
242
+ ],
243
+ "tags": [],
244
+ "risks": []
245
+ },
246
+ {
247
+ "name": "WebhookDeliveryCollection",
248
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookDeliveryCollection",
249
+ "collection": "webhookdeliveries",
250
+ "tableName": "_smrt_webhook_deliveries",
251
+ "packageName": "@happyvertical/smrt-webhooks",
252
+ "extends": "SmrtCollection",
253
+ "fields": [],
254
+ "relationships": [],
255
+ "methods": [],
256
+ "surfaces": [],
257
+ "relationshipFeatures": [
258
+ "uuidColumns"
259
+ ],
260
+ "tags": [],
261
+ "risks": []
262
+ },
263
+ {
264
+ "name": "WebhookDeliveryTask",
265
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookDeliveryTask",
266
+ "collection": "webhookdeliverytasks",
267
+ "description": "Jobs queue entry point; persisted arguments contain IDs only.",
268
+ "tableName": "_smrt_webhook_delivery_tasks",
269
+ "packageName": "@happyvertical/smrt-webhooks",
270
+ "extends": "SmrtObject",
271
+ "fields": [],
272
+ "relationships": [],
273
+ "methods": [
274
+ "deliver"
275
+ ],
276
+ "methodSignatures": [
277
+ {
278
+ "name": "deliver",
279
+ "async": true,
280
+ "params": [
281
+ "args?: Record<string, unknown>",
282
+ "_context?: JobExecutionContext"
283
+ ],
284
+ "returns": "Promise<void>"
285
+ }
286
+ ],
287
+ "surfaces": [],
288
+ "withheldSurfaces": [
289
+ {
290
+ "kind": "api",
291
+ "operation": "deliver",
292
+ "code": "api-disabled",
293
+ "reason": "api is disabled",
294
+ "objectName": "@happyvertical/smrt-webhooks:WebhookDeliveryTask"
295
+ }
296
+ ],
297
+ "relationshipFeatures": [
298
+ "uuidColumns"
299
+ ],
300
+ "tags": [],
301
+ "risks": []
302
+ },
303
+ {
304
+ "name": "WebhookCursor",
305
+ "qualifiedName": "@happyvertical/smrt-webhooks:WebhookCursor",
306
+ "collection": "webhookcursors",
307
+ "description": "Tenant-bound durable cursor for replayable model change-feed publication.",
308
+ "tableName": "_smrt_webhook_cursors",
309
+ "packageName": "@happyvertical/smrt-webhooks",
310
+ "extends": "SmrtObject",
311
+ "fields": [
312
+ {
313
+ "name": "tenantId",
314
+ "type": "text",
315
+ "required": true,
316
+ "columnType": "UUID"
317
+ },
318
+ {
319
+ "name": "runtime",
320
+ "type": "text",
321
+ "required": true,
322
+ "columnType": "TEXT"
323
+ },
324
+ {
325
+ "name": "position",
326
+ "type": "integer",
327
+ "required": false,
328
+ "columnType": "INTEGER",
329
+ "default": 0
330
+ }
331
+ ],
332
+ "relationships": [],
333
+ "methods": [],
334
+ "tenant": {
335
+ "scoped": true,
336
+ "mode": "required",
337
+ "field": "tenantId"
338
+ },
339
+ "conflictColumns": [
340
+ "tenant_id",
341
+ "slug",
342
+ "context"
343
+ ],
344
+ "surfaces": [],
345
+ "relationshipFeatures": [
346
+ "uuidColumns"
347
+ ],
348
+ "tags": [],
349
+ "risks": []
350
+ }
351
+ ],
352
+ "surfaces": [],
353
+ "prompts": [],
354
+ "relationshipsV2": {
355
+ "foreignKeyFields": 1,
356
+ "crossPackageRefFields": 0,
357
+ "junctionCollections": 0,
358
+ "hierarchicalObjects": 0,
359
+ "polymorphicAssociations": 0,
360
+ "uuidColumns": 13
361
+ },
362
+ "agentDoc": "# @happyvertical/smrt-webhooks\n\nServer-only outbound webhooks. `change-feed.ts` owns durable model publication\nwith tenant/runtime cursors and retained-history recovery; `events.ts` is the\noptional non-atomic hook path. Never enable both for the same models. `store.ts` owns tenant-bound SQL and atomic\noutbox/job enqueue; `dispatcher.ts` owns signing and job execution. Always use\n`requireTenant()` plus explicit tenant predicates, including raw CAS writes.\n\n`security.ts` validates every DNS result, pins HTTPS lookup, disables pooling,\nand bounds DNS/request time. Never substitute a preflight DNS check followed by\nfetch, follow redirects, or persist raw transport errors. Preserve exact stored\npayload bytes for signing. HTTP is at-least-once; receivers deduplicate the\nstable delivery ID. Do not promise exactly-once network effects.\n\nAdmin projections are allowlists. Secrets and payloads are sensitive fields;\ngenerated API/CLI/MCP access is disabled. The host authorizes administrators via\n`WebhookAdminService`; Svelte callbacks must use authenticated, CSRF-protected\nhost routes. No credentials in the browser read model.\n\nValidate: `pnpm test`, `pnpm typecheck`, `pnpm check`, `pnpm test:e2e` within this\npackage after building its dependencies. E2E uses the maintained Playwright\nharness under `e2e/`; no external webhook server or network credential is needed.\nSee README.md for migration, lifecycle-hook boundaries and worker setup.\n",
363
+ "recipes": [
364
+ {
365
+ "id": "integrations.webhooks",
366
+ "className": "WebhooksRecipe",
367
+ "label": "Outbound webhooks",
368
+ "summary": "Deliver signed model events and inspect delivery attempts from a server.",
369
+ "synonyms": [],
370
+ "models": [
371
+ "@happyvertical/smrt-webhooks:WebhookSubscription",
372
+ "@happyvertical/smrt-webhooks:WebhookDelivery",
373
+ "@happyvertical/smrt-webhooks:WebhookDeliveryTask",
374
+ "@happyvertical/smrt-webhooks:WebhookCursor"
375
+ ],
376
+ "nav": [],
377
+ "requires": [],
378
+ "group": {
379
+ "id": "integrations",
380
+ "label": "Integrations",
381
+ "summary": "Connect your application to external services."
382
+ },
383
+ "help": {
384
+ "markdown": "## Overview\n\nSend signed event notifications to an external HTTPS endpoint. Administrators\ncan pause subscriptions and inspect delivery attempts. This integration requires\na server and a running background worker.\n\n## Tasks\n\n- Add a subscription with a public HTTPS endpoint, signing secret, and event/model filters.\n- Disable a subscription to cancel unsent deliveries.\n- Inspect the delivery log, then retry failed deliveries after correcting their endpoint.\n- Ask the endpoint owner to verify signatures and deduplicate delivery IDs.\n",
385
+ "fieldRefs": []
386
+ },
387
+ "surfaces": [
388
+ {
389
+ "kind": "settings-panel",
390
+ "export": "@happyvertical/smrt-webhooks/svelte#WebhookAdmin",
391
+ "label": "Outbound webhooks"
392
+ }
393
+ ],
394
+ "runtime": "server",
395
+ "demo": {
396
+ "mode": "server",
397
+ "reasons": [
398
+ "The recipe declares runtime \"server\".",
399
+ "Needs a server because its package does not build for a browser: Intentionally server-only outbound delivery: node:crypto HMAC and node:https/dns/net pinned transport enforce signing and SSRF protection. Browser hosts use the separate /svelte admin export with authenticated server callbacks.; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3627, #3717)."
400
+ ]
401
+ }
402
+ }
403
+ ],
404
+ "browser": {
405
+ "status": "server-only",
406
+ "issues": [
407
+ "#3615",
408
+ "#3624",
409
+ "#3627",
410
+ "#3717"
411
+ ],
412
+ "reason": "Intentionally server-only outbound delivery: node:crypto HMAC and node:https/dns/net pinned transport enforce signing and SSRF protection. Browser hosts use the separate /svelte admin export with authenticated server callbacks.",
413
+ "via": [
414
+ "@happyvertical/smrt-jobs",
415
+ "@happyvertical/smrt-tenancy"
416
+ ]
417
+ }
418
+ }
@@ -0,0 +1,26 @@
1
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
+ import { type WebhookSubscription } from './models.js';
3
+ import type { WebhookEventEnvelope } from './types.js';
4
+ /** Explicit tenant predicates also protect deployments that have not enabled interceptors. */
5
+ export declare class WebhookStore {
6
+ readonly db: DatabaseInterface;
7
+ readonly runtime: string;
8
+ constructor(db: DatabaseInterface, runtime: string);
9
+ subscriptions(): Promise<WebhookSubscription[]>;
10
+ subscription(id: string): Promise<WebhookSubscription | null>;
11
+ delivery(id: string): Promise<import("./models.js").WebhookDelivery | null>;
12
+ deliveries(): Promise<import("./models.js").WebhookDelivery[]>;
13
+ /** The unique outbox identity and jobs enqueue commit or roll back together. */
14
+ accept(subscriptionId: string, event: WebhookEventEnvelope): Promise<string>;
15
+ private enqueue;
16
+ /** A bounded lease prevents overlapping jobs; expired leases recover after process death. */
17
+ claim(id: string, generation?: number): Promise<string | null>;
18
+ finish(id: string, token: string, status: string, responseStatus: number | null, error: string | null): Promise<void>;
19
+ /** Reconcile exhausted/cancelled jobs, including process death before an attempt starts.
20
+ * Tenant-scoped and lease-aware; safe to call on each admin refresh or operator sweep.
21
+ */
22
+ reconcile(): Promise<void>;
23
+ /** Explicit operator recovery for exhausted jobs. Repeated/concurrent replay is a no-op. */
24
+ replay(id: string): Promise<boolean>;
25
+ }
26
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,8FAA8F;AAC9F,qBAAa,YAAY;IAErB,QAAQ,CAAC,EAAE,EAAE,iBAAiB;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM;gBADf,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,MAAM;IAIpB,aAAa;IAiBb,YAAY,CAAC,EAAE,EAAE,MAAM;IAOvB,QAAQ,CAAC,EAAE,EAAE,MAAM;IAOnB,UAAU;IAShB,gFAAgF;IAC1E,MAAM,CACV,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC;YAoCJ,OAAO;IAsBrB,6FAA6F;IACvF,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiBzD,MAAM,CACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,KAAK,EAAE,MAAM,GAAG,IAAI;IAgBtB;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BhC,4FAA4F;IACtF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqB3C"}
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import { Alert } from '@happyvertical/smrt-ui/feedback';
3
+ import { Input } from '@happyvertical/smrt-ui/forms';
4
+ import { Button, Card } from '@happyvertical/smrt-ui/ui';
5
+ import type {
6
+ WebhookDeliveryRecord,
7
+ WebhookSubscriptionRecord,
8
+ } from '../types.js';
9
+
10
+ interface Props {
11
+ /** Safe subscription summaries for the authenticated administrator. */
12
+ subscriptions: WebhookSubscriptionRecord[];
13
+ /** Recent delivery outcomes without payloads, credentials, or lease tokens. */
14
+ deliveries: WebhookDeliveryRecord[];
15
+ /** Create a subscription through the host's authorized server endpoint. */
16
+ onCreate: (input: {
17
+ url: string;
18
+ events: string[];
19
+ models: string[];
20
+ secret: string;
21
+ }) => Promise<void>;
22
+ /** Enable or pause the selected subscription through the authorized host. */
23
+ onToggle: (id: string, enabled: boolean) => Promise<void>;
24
+ /** Retry a failed delivery after the host verifies administrator access. */
25
+ onReplay: (id: string) => Promise<void>;
26
+ }
27
+ let { subscriptions, deliveries, onCreate, onToggle, onReplay }: Props =
28
+ $props();
29
+ const uid = $props.id();
30
+ let url = $state('');
31
+ let events = $state('create,update,delete');
32
+ let models = $state('');
33
+ let secret = $state('');
34
+ let error = $state('');
35
+ let busy = $state(false);
36
+ const split = (value: string) =>
37
+ value
38
+ .split(',')
39
+ .map((v) => v.trim())
40
+ .filter(Boolean);
41
+ async function action(callback: () => Promise<void>) {
42
+ if (busy) return;
43
+ busy = true;
44
+ error = '';
45
+ try {
46
+ await callback();
47
+ } catch {
48
+ error = 'The webhook operation could not be completed. Please try again.';
49
+ } finally {
50
+ busy = false;
51
+ }
52
+ }
53
+ async function submit() {
54
+ await action(async () => {
55
+ await onCreate({
56
+ url,
57
+ events: split(events),
58
+ models: split(models),
59
+ secret,
60
+ });
61
+ url = '';
62
+ secret = '';
63
+ });
64
+ }
65
+ </script>
66
+ <Card>
67
+ <section aria-labelledby={`${uid}-title`} aria-busy={busy}>
68
+ <h2 id={`${uid}-title`}>Outbound webhooks</h2>
69
+ <form onsubmit={(event) => { event.preventDefault(); void submit(); }}>
70
+ <label for={`${uid}-url`}>Endpoint</label>
71
+ <Input id={`${uid}-url`} bind:value={url} type="url" required placeholder="https://partner.example/webhooks" disabled={busy} />
72
+ <label for={`${uid}-events`}>Events</label>
73
+ <Input id={`${uid}-events`} bind:value={events} disabled={busy} />
74
+ <label for={`${uid}-models`}>Models</label>
75
+ <Input id={`${uid}-models`} bind:value={models} disabled={busy} />
76
+ <p>Use comma-separated names. Empty filters match all events or models.</p>
77
+ <label for={`${uid}-secret`}>Signing secret</label>
78
+ <Input id={`${uid}-secret`} bind:value={secret} type="password" required minlength={32} autocomplete="new-password" interaction={false} disabled={busy} />
79
+ <Button type="submit" disabled={busy} loading={busy}>Add subscription</Button>
80
+ </form>
81
+ {#if error}<Alert variant="error">{error}</Alert>{/if}
82
+ <h3>Subscriptions</h3>
83
+ <ul aria-label="Webhook subscriptions">
84
+ {#each subscriptions as subscription (subscription.id)}
85
+ <li><code>{subscription.url}</code>
86
+ <Button disabled={busy} aria-label={`${subscription.enabled ? 'Disable' : 'Enable'} ${subscription.url}`} onclick={() => action(() => onToggle(subscription.id, !subscription.enabled))}>
87
+ {subscription.enabled ? 'Disable' : 'Enable'}
88
+ </Button>
89
+ </li>
90
+ {:else}<li>No subscriptions yet.</li>{/each}
91
+ </ul>
92
+ <h3>Delivery log</h3>
93
+ <div class="table-scroll">
94
+ <table>
95
+ <caption>Recent webhook deliveries</caption>
96
+ <thead><tr><th scope="col">Delivery</th><th scope="col">Status</th><th scope="col">Attempts</th><th scope="col">Response</th><th scope="col">Actions</th></tr></thead>
97
+ <tbody>{#each deliveries as delivery (delivery.id)}
98
+ <tr><td>{delivery.id}</td><td>{delivery.status}</td><td>{delivery.attempts}</td><td>{delivery.responseStatus ?? '—'}</td><td>
99
+ {#if delivery.status === 'failed'}<Button disabled={busy} onclick={() => action(() => onReplay(delivery.id))}>Retry delivery</Button>{/if}
100
+ </td></tr>
101
+ {:else}<tr><td colspan="5">No deliveries yet.</td></tr>{/each}</tbody>
102
+ </table>
103
+ </div>
104
+ </section>
105
+ </Card>
106
+ <style>
107
+ form { display: grid; gap: var(--smrt-spacing-2, 0.5rem); }
108
+ li { display: flex; gap: var(--smrt-spacing-2, 0.5rem); align-items: center; flex-wrap: wrap; }
109
+ code { overflow-wrap: anywhere; }
110
+ .table-scroll { overflow-x: auto; }
111
+ table { width: 100%; text-align: left; }
112
+ th, td { padding: var(--smrt-spacing-2, 0.5rem); }
113
+ </style>