@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,248 @@
1
+ import { a as WebhookDeliveryCollection, c as WebhookSubscriptionCollection, f as sendPublicWebhook, l as assertHmacSecret } from "./admin-413jve1I.js";
2
+ import { requireTenant } from "@happyvertical/smrt-tenancy";
3
+ import { SmrtJobCollection } from "@happyvertical/smrt-jobs";
4
+ import { createHmac, randomUUID } from "node:crypto";
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ return target;
15
+ };
16
+ //#endregion
17
+ //#region src/store.ts
18
+ var WebhookStore = class {
19
+ constructor(db, runtime) {
20
+ this.db = db;
21
+ this.runtime = runtime;
22
+ if (!runtime) throw new Error("Webhook runtime is required.");
23
+ }
24
+ db;
25
+ runtime;
26
+ async subscriptions() {
27
+ const tenantId = requireTenant().tenantId;
28
+ const collection = await WebhookSubscriptionCollection.create({ db: this.db });
29
+ const result = [];
30
+ for (let offset = 0;; offset += 1e3) {
31
+ const page = await collection.list({
32
+ where: { tenantId },
33
+ orderBy: "id ASC",
34
+ limit: 1e3,
35
+ offset
36
+ });
37
+ result.push(...page);
38
+ if (page.length < 1e3) return result;
39
+ }
40
+ }
41
+ async subscription(id) {
42
+ const tenantId = requireTenant().tenantId;
43
+ return (await WebhookSubscriptionCollection.create({ db: this.db })).get({
44
+ id,
45
+ tenantId
46
+ });
47
+ }
48
+ async delivery(id) {
49
+ const tenantId = requireTenant().tenantId;
50
+ return (await WebhookDeliveryCollection.create({ db: this.db })).get({
51
+ id,
52
+ tenantId
53
+ });
54
+ }
55
+ async deliveries() {
56
+ const tenantId = requireTenant().tenantId;
57
+ return (await WebhookDeliveryCollection.create({ db: this.db })).list({
58
+ where: { tenantId },
59
+ orderBy: "created_at DESC",
60
+ limit: 100
61
+ });
62
+ }
63
+ /** The unique outbox identity and jobs enqueue commit or roll back together. */
64
+ async accept(subscriptionId, event) {
65
+ const tenantId = requireTenant().tenantId;
66
+ if (!await this.subscription(subscriptionId)) throw new Error("Webhook subscription not found.");
67
+ if (!this.db.transaction) throw new Error("Webhooks require a transactional database.");
68
+ return this.db.transaction(async (tx) => {
69
+ const id = randomUUID();
70
+ const now = (/* @__PURE__ */ new Date()).toISOString();
71
+ if ((await tx.query(`INSERT INTO _smrt_webhook_deliveries
72
+ (id, slug, context, tenant_id, subscription_id, event_id, payload, status, attempts, generation, created_at, updated_at)
73
+ VALUES (?, ?, '', ?, ?, ?, ?, 'pending', 0, 0, ?, ?)
74
+ ON CONFLICT (tenant_id, subscription_id, event_id) DO NOTHING RETURNING id`, id, id, tenantId, subscriptionId, event.id, JSON.stringify(event), now, now)).rows.length) {
75
+ await this.enqueue(tx, tenantId, id);
76
+ return id;
77
+ }
78
+ return (await tx.query("SELECT CAST(id AS VARCHAR) AS id FROM _smrt_webhook_deliveries WHERE tenant_id = ? AND subscription_id = ? AND event_id = ?", tenantId, subscriptionId, event.id)).rows[0].id;
79
+ });
80
+ }
81
+ async enqueue(db, tenantId, deliveryId, generation = 0) {
82
+ await (await SmrtJobCollection.create({ db })).enqueueJob({
83
+ tenantId,
84
+ queue: "webhooks",
85
+ objectType: "@happyvertical/smrt-webhooks:WebhookDeliveryTask",
86
+ objectId: null,
87
+ method: "deliver",
88
+ args: {
89
+ runtime: this.runtime,
90
+ tenantId,
91
+ deliveryId,
92
+ generation
93
+ },
94
+ maxAttempts: 5,
95
+ timeout: 3e4,
96
+ retryStrategy: {
97
+ type: "exponential",
98
+ config: {
99
+ initialDelay: 1e3,
100
+ multiplier: 2,
101
+ maxDelay: 3e5
102
+ }
103
+ }
104
+ });
105
+ }
106
+ /** A bounded lease prevents overlapping jobs; expired leases recover after process death. */
107
+ async claim(id, generation = 0) {
108
+ const tenantId = requireTenant().tenantId;
109
+ const token = randomUUID();
110
+ const now = /* @__PURE__ */ new Date();
111
+ return (await this.db.query(`UPDATE _smrt_webhook_deliveries SET lease_token = ?, lease_until = ?, attempts = attempts + 1
112
+ WHERE id = ? AND tenant_id = ? AND generation = ? AND status IN ('pending', 'retrying')
113
+ AND (lease_until IS NULL OR lease_until <= ?) RETURNING id`, token, new Date(now.getTime() + 2e4).toISOString(), id, tenantId, generation, now.toISOString())).rows.length ? token : null;
114
+ }
115
+ async finish(id, token, status, responseStatus, error) {
116
+ const tenantId = requireTenant().tenantId;
117
+ if (!(await this.db.query(`UPDATE _smrt_webhook_deliveries SET status = ?, response_status = ?, error = ?, lease_token = NULL, lease_until = NULL, updated_at = ?
118
+ WHERE id = ? AND tenant_id = ? AND lease_token = ? RETURNING id`, status, responseStatus, error, (/* @__PURE__ */ new Date()).toISOString(), id, tenantId, token)).rows.length) throw new Error("Webhook delivery lease lost.");
119
+ }
120
+ /** Reconcile exhausted/cancelled jobs, including process death before an attempt starts.
121
+ * Tenant-scoped and lease-aware; safe to call on each admin refresh or operator sweep.
122
+ */
123
+ async reconcile() {
124
+ const tenantId = requireTenant().tenantId;
125
+ const jobs = await SmrtJobCollection.create({ db: this.db });
126
+ for (let offset = 0;; offset += 1e3) {
127
+ const terminal = await jobs.list({
128
+ where: {
129
+ tenantId,
130
+ queue: "webhooks",
131
+ status: ["failed", "cancelled"]
132
+ },
133
+ orderBy: "id ASC",
134
+ limit: 1e3,
135
+ offset
136
+ });
137
+ for (const job of terminal) {
138
+ if (job.args.runtime !== this.runtime || typeof job.args.deliveryId !== "string") continue;
139
+ await this.db.query(`UPDATE _smrt_webhook_deliveries SET status = 'failed', error = 'Webhook job exhausted.', lease_token = NULL, lease_until = NULL
140
+ WHERE id = ? AND tenant_id = ? AND generation = ? AND status IN ('pending', 'retrying') AND (lease_until IS NULL OR lease_until <= ?)`, job.args.deliveryId, tenantId, job.args.generation ?? 0, (/* @__PURE__ */ new Date()).toISOString());
141
+ }
142
+ if (terminal.length < 1e3) return;
143
+ }
144
+ }
145
+ /** Explicit operator recovery for exhausted jobs. Repeated/concurrent replay is a no-op. */
146
+ async replay(id) {
147
+ const tenantId = requireTenant().tenantId;
148
+ if (!this.db.transaction) throw new Error("Webhooks require a transactional database.");
149
+ return this.db.transaction(async (tx) => {
150
+ const changed = await tx.query(`UPDATE _smrt_webhook_deliveries SET status = 'pending', attempts = 0, generation = generation + 1, error = NULL, lease_token = NULL, lease_until = NULL
151
+ WHERE id = ? AND tenant_id = ? AND status = 'failed' RETURNING generation`, id, tenantId);
152
+ if (!changed.rows.length) return false;
153
+ await this.enqueue(tx, tenantId, id, Number(changed.rows[0].generation));
154
+ return true;
155
+ });
156
+ }
157
+ };
158
+ //#endregion
159
+ //#region src/dispatcher.ts
160
+ var dispatcher_exports = /* @__PURE__ */ __exportAll({
161
+ WebhookDispatcher: () => WebhookDispatcher,
162
+ assertWebhookEvent: () => assertWebhookEvent,
163
+ registerWebhookRuntime: () => registerWebhookRuntime,
164
+ runWebhookDeliveryJob: () => runWebhookDeliveryJob,
165
+ unregisterWebhookRuntime: () => unregisterWebhookRuntime
166
+ });
167
+ var runtimes = /* @__PURE__ */ new Map();
168
+ function registerWebhookRuntime(name, dispatcher) {
169
+ if (!name || runtimes.has(name)) throw new Error("Webhook runtime already registered or invalid.");
170
+ runtimes.set(name, dispatcher);
171
+ }
172
+ function unregisterWebhookRuntime(name) {
173
+ runtimes.delete(name);
174
+ }
175
+ function assertWebhookEvent(event) {
176
+ if (!event || typeof event !== "object" || !event.data || Array.isArray(event.data) || typeof event.data !== "object" || [
177
+ event.id,
178
+ event.event,
179
+ event.model
180
+ ].some((v) => typeof v !== "string" || !v || v.length > 200 || /[\r\n]/.test(v)) || typeof event.occurredAt !== "string" || !Number.isFinite(Date.parse(event.occurredAt)) || Buffer.byteLength(JSON.stringify(event)) > 262144) throw new Error("Invalid webhook event.");
181
+ }
182
+ var WebhookDispatcher = class {
183
+ store;
184
+ transport;
185
+ constructor(options) {
186
+ this.store = new WebhookStore(options.db, options.runtime);
187
+ this.transport = options.transport ?? sendPublicWebhook;
188
+ }
189
+ async dispatch(event) {
190
+ requireTenant();
191
+ assertWebhookEvent(event);
192
+ const ids = [];
193
+ for (const sub of await this.store.subscriptions()) if (sub.enabled && (!sub.events.length || sub.events.includes(event.event)) && (!sub.models.length || sub.models.includes(event.model))) ids.push(await this.store.accept(sub.id ?? "", event));
194
+ return ids;
195
+ }
196
+ async deliver(deliveryId, expectedGeneration) {
197
+ requireTenant();
198
+ const before = await this.store.delivery(deliveryId);
199
+ if (!before || expectedGeneration !== void 0 && before.generation !== expectedGeneration || [
200
+ "delivered",
201
+ "failed",
202
+ "cancelled"
203
+ ].includes(before.status)) return;
204
+ const token = await this.store.claim(deliveryId, before.generation);
205
+ if (!token) throw new Error("Webhook delivery is already leased.");
206
+ const delivery = await this.store.delivery(deliveryId);
207
+ if (!delivery) throw new Error("Webhook delivery unavailable.");
208
+ let responseStatus = null;
209
+ try {
210
+ const sub = await this.store.subscription(delivery.subscriptionId);
211
+ if (!sub?.enabled) {
212
+ await this.store.finish(deliveryId, token, "cancelled", null, "Subscription disabled.");
213
+ return;
214
+ }
215
+ assertHmacSecret(sub.secret);
216
+ const event = JSON.parse(delivery.payload);
217
+ assertWebhookEvent(event);
218
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
219
+ const signature = createHmac("sha256", sub.secret).update(`${timestamp}.${delivery.payload}`).digest("hex");
220
+ responseStatus = await this.transport({
221
+ url: sub.url,
222
+ body: delivery.payload,
223
+ headers: {
224
+ "content-type": "application/json",
225
+ "user-agent": "smrt-webhooks/1",
226
+ "x-smrt-event": event.event,
227
+ "x-smrt-delivery": deliveryId,
228
+ "x-smrt-timestamp": timestamp,
229
+ "x-smrt-signature": `sha256=${signature}`
230
+ }
231
+ });
232
+ if (responseStatus < 200 || responseStatus >= 300) throw new Error("Webhook rejected.");
233
+ await this.store.finish(deliveryId, token, "delivered", responseStatus, null);
234
+ } catch {
235
+ await this.store.finish(deliveryId, token, delivery.attempts >= 5 ? "failed" : "retrying", responseStatus, "Webhook delivery failed.");
236
+ throw new Error("Webhook delivery failed.");
237
+ }
238
+ }
239
+ };
240
+ async function runWebhookDeliveryJob(args) {
241
+ const runtime = typeof args.runtime === "string" ? runtimes.get(args.runtime) : void 0;
242
+ if (!runtime || typeof args.deliveryId !== "string" || args.tenantId !== requireTenant().tenantId) throw new Error("Invalid webhook job or unavailable runtime.");
243
+ await runtime.deliver(args.deliveryId, typeof args.generation === "number" ? args.generation : 0);
244
+ }
245
+ //#endregion
246
+ export { runWebhookDeliveryJob as a, registerWebhookRuntime as i, assertWebhookEvent as n, unregisterWebhookRuntime as o, dispatcher_exports as r, WebhookStore as s, WebhookDispatcher as t };
247
+
248
+ //# sourceMappingURL=dispatcher-C5AqtgtK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher-C5AqtgtK.js","names":[],"sources":["../../src/store.ts","../../src/dispatcher.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { SmrtJobCollection } from '@happyvertical/smrt-jobs';\nimport { requireTenant } from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n WebhookDeliveryCollection,\n type WebhookSubscription,\n WebhookSubscriptionCollection,\n} from './models.js';\nimport type { WebhookEventEnvelope } from './types.js';\n\n/** Explicit tenant predicates also protect deployments that have not enabled interceptors. */\nexport class WebhookStore {\n constructor(\n readonly db: DatabaseInterface,\n readonly runtime: string,\n ) {\n if (!runtime) throw new Error('Webhook runtime is required.');\n }\n async subscriptions() {\n const tenantId = requireTenant().tenantId;\n const collection = await WebhookSubscriptionCollection.create({\n db: this.db,\n });\n const result: WebhookSubscription[] = [];\n for (let offset = 0; ; offset += 1000) {\n const page = await collection.list({\n where: { tenantId },\n orderBy: 'id ASC',\n limit: 1000,\n offset,\n });\n result.push(...page);\n if (page.length < 1000) return result;\n }\n }\n async subscription(id: string) {\n const tenantId = requireTenant().tenantId;\n return (await WebhookSubscriptionCollection.create({ db: this.db })).get({\n id,\n tenantId,\n });\n }\n async delivery(id: string) {\n const tenantId = requireTenant().tenantId;\n return (await WebhookDeliveryCollection.create({ db: this.db })).get({\n id,\n tenantId,\n });\n }\n async deliveries() {\n const tenantId = requireTenant().tenantId;\n return (await WebhookDeliveryCollection.create({ db: this.db })).list({\n where: { tenantId },\n orderBy: 'created_at DESC',\n limit: 100,\n });\n }\n\n /** The unique outbox identity and jobs enqueue commit or roll back together. */\n async accept(\n subscriptionId: string,\n event: WebhookEventEnvelope,\n ): Promise<string> {\n const tenantId = requireTenant().tenantId;\n if (!(await this.subscription(subscriptionId)))\n throw new Error('Webhook subscription not found.');\n if (!this.db.transaction)\n throw new Error('Webhooks require a transactional database.');\n return this.db.transaction(async (tx) => {\n const id = randomUUID();\n const now = new Date().toISOString();\n const inserted = await tx.query(\n `INSERT INTO _smrt_webhook_deliveries\n (id, slug, context, tenant_id, subscription_id, event_id, payload, status, attempts, generation, created_at, updated_at)\n VALUES (?, ?, '', ?, ?, ?, ?, 'pending', 0, 0, ?, ?)\n ON CONFLICT (tenant_id, subscription_id, event_id) DO NOTHING RETURNING id`,\n id,\n id,\n tenantId,\n subscriptionId,\n event.id,\n JSON.stringify(event),\n now,\n now,\n );\n if (inserted.rows.length) {\n await this.enqueue(tx, tenantId, id);\n return id;\n }\n const existing = await tx.query(\n 'SELECT CAST(id AS VARCHAR) AS id FROM _smrt_webhook_deliveries WHERE tenant_id = ? AND subscription_id = ? AND event_id = ?',\n tenantId,\n subscriptionId,\n event.id,\n );\n return (existing.rows[0] as { id: string }).id;\n });\n }\n private async enqueue(\n db: DatabaseInterface,\n tenantId: string,\n deliveryId: string,\n generation = 0,\n ) {\n const jobs = await SmrtJobCollection.create({ db });\n await jobs.enqueueJob({\n tenantId,\n queue: 'webhooks',\n objectType: '@happyvertical/smrt-webhooks:WebhookDeliveryTask',\n objectId: null,\n method: 'deliver',\n args: { runtime: this.runtime, tenantId, deliveryId, generation },\n maxAttempts: 5,\n timeout: 30000,\n retryStrategy: {\n type: 'exponential',\n config: { initialDelay: 1000, multiplier: 2, maxDelay: 300000 },\n },\n });\n }\n /** A bounded lease prevents overlapping jobs; expired leases recover after process death. */\n async claim(id: string, generation = 0): Promise<string | null> {\n const tenantId = requireTenant().tenantId;\n const token = randomUUID();\n const now = new Date();\n const result = await this.db.query(\n `UPDATE _smrt_webhook_deliveries SET lease_token = ?, lease_until = ?, attempts = attempts + 1\n WHERE id = ? AND tenant_id = ? AND generation = ? AND status IN ('pending', 'retrying')\n AND (lease_until IS NULL OR lease_until <= ?) RETURNING id`,\n token,\n new Date(now.getTime() + 20000).toISOString(),\n id,\n tenantId,\n generation,\n now.toISOString(),\n );\n return result.rows.length ? token : null;\n }\n async finish(\n id: string,\n token: string,\n status: string,\n responseStatus: number | null,\n error: string | null,\n ) {\n const tenantId = requireTenant().tenantId;\n const result = await this.db.query(\n `UPDATE _smrt_webhook_deliveries SET status = ?, response_status = ?, error = ?, lease_token = NULL, lease_until = NULL, updated_at = ?\n WHERE id = ? AND tenant_id = ? AND lease_token = ? RETURNING id`,\n status,\n responseStatus,\n error,\n new Date().toISOString(),\n id,\n tenantId,\n token,\n );\n if (!result.rows.length) throw new Error('Webhook delivery lease lost.');\n }\n /** Reconcile exhausted/cancelled jobs, including process death before an attempt starts.\n * Tenant-scoped and lease-aware; safe to call on each admin refresh or operator sweep.\n */\n async reconcile(): Promise<void> {\n const tenantId = requireTenant().tenantId;\n const jobs = await SmrtJobCollection.create({ db: this.db });\n for (let offset = 0; ; offset += 1000) {\n const terminal = await jobs.list({\n where: { tenantId, queue: 'webhooks', status: ['failed', 'cancelled'] },\n orderBy: 'id ASC',\n limit: 1000,\n offset,\n });\n for (const job of terminal) {\n if (\n job.args.runtime !== this.runtime ||\n typeof job.args.deliveryId !== 'string'\n )\n continue;\n await this.db.query(\n `UPDATE _smrt_webhook_deliveries SET status = 'failed', error = 'Webhook job exhausted.', lease_token = NULL, lease_until = NULL\n WHERE id = ? AND tenant_id = ? AND generation = ? AND status IN ('pending', 'retrying') AND (lease_until IS NULL OR lease_until <= ?)`,\n job.args.deliveryId,\n tenantId,\n job.args.generation ?? 0,\n new Date().toISOString(),\n );\n }\n if (terminal.length < 1000) return;\n }\n }\n\n /** Explicit operator recovery for exhausted jobs. Repeated/concurrent replay is a no-op. */\n async replay(id: string): Promise<boolean> {\n const tenantId = requireTenant().tenantId;\n if (!this.db.transaction)\n throw new Error('Webhooks require a transactional database.');\n return this.db.transaction(async (tx) => {\n const changed = await tx.query(\n `UPDATE _smrt_webhook_deliveries SET status = 'pending', attempts = 0, generation = generation + 1, error = NULL, lease_token = NULL, lease_until = NULL\n WHERE id = ? AND tenant_id = ? AND status = 'failed' RETURNING generation`,\n id,\n tenantId,\n );\n if (!changed.rows.length) return false;\n await this.enqueue(\n tx,\n tenantId,\n id,\n Number((changed.rows[0] as { generation: number }).generation),\n );\n return true;\n });\n }\n}\n","import { createHmac } from 'node:crypto';\nimport { requireTenant } from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n assertHmacSecret,\n sendPublicWebhook,\n type WebhookTransport,\n} from './security.js';\nimport { WebhookStore } from './store.js';\nimport type { WebhookEventEnvelope } from './types.js';\n\nexport interface WebhookDispatcherOptions {\n db: DatabaseInterface;\n runtime: string;\n /** Trusted server integration seam. Default transport pins validated public DNS answers. */\n transport?: WebhookTransport;\n}\nconst runtimes = new Map<string, WebhookDispatcher>();\nexport function registerWebhookRuntime(\n name: string,\n dispatcher: WebhookDispatcher,\n): void {\n if (!name || runtimes.has(name))\n throw new Error('Webhook runtime already registered or invalid.');\n runtimes.set(name, dispatcher);\n}\nexport function unregisterWebhookRuntime(name: string): void {\n runtimes.delete(name);\n}\n\nexport function assertWebhookEvent(event: WebhookEventEnvelope): void {\n if (\n !event ||\n typeof event !== 'object' ||\n !event.data ||\n Array.isArray(event.data) ||\n typeof event.data !== 'object' ||\n [event.id, event.event, event.model].some(\n (v) => typeof v !== 'string' || !v || v.length > 200 || /[\\r\\n]/.test(v),\n ) ||\n typeof event.occurredAt !== 'string' ||\n !Number.isFinite(Date.parse(event.occurredAt)) ||\n Buffer.byteLength(JSON.stringify(event)) > 262144\n )\n throw new Error('Invalid webhook event.');\n}\n\n/** Durable tenant-scoped outbound events. Receivers deduplicate x-smrt-delivery. */\nexport class WebhookDispatcher {\n readonly store: WebhookStore;\n private readonly transport: WebhookTransport;\n constructor(options: WebhookDispatcherOptions) {\n this.store = new WebhookStore(options.db, options.runtime);\n this.transport = options.transport ?? sendPublicWebhook;\n }\n async dispatch(event: WebhookEventEnvelope): Promise<string[]> {\n requireTenant();\n assertWebhookEvent(event);\n const ids: string[] = [];\n for (const sub of await this.store.subscriptions()) {\n if (\n sub.enabled &&\n (!sub.events.length || sub.events.includes(event.event)) &&\n (!sub.models.length || sub.models.includes(event.model))\n ) {\n ids.push(await this.store.accept(sub.id ?? '', event));\n }\n }\n return ids;\n }\n async deliver(\n deliveryId: string,\n expectedGeneration?: number,\n ): Promise<void> {\n requireTenant();\n const before = await this.store.delivery(deliveryId);\n if (\n !before ||\n (expectedGeneration !== undefined &&\n before.generation !== expectedGeneration) ||\n ['delivered', 'failed', 'cancelled'].includes(before.status)\n )\n return;\n const token = await this.store.claim(deliveryId, before.generation);\n if (!token) throw new Error('Webhook delivery is already leased.');\n const delivery = await this.store.delivery(deliveryId);\n if (!delivery) throw new Error('Webhook delivery unavailable.');\n let responseStatus: number | null = null;\n try {\n const sub = await this.store.subscription(delivery.subscriptionId);\n if (!sub?.enabled) {\n await this.store.finish(\n deliveryId,\n token,\n 'cancelled',\n null,\n 'Subscription disabled.',\n );\n return;\n }\n assertHmacSecret(sub.secret);\n const event = JSON.parse(delivery.payload) as WebhookEventEnvelope;\n assertWebhookEvent(event);\n const timestamp = new Date().toISOString();\n const signature = createHmac('sha256', sub.secret)\n .update(`${timestamp}.${delivery.payload}`)\n .digest('hex');\n responseStatus = await this.transport({\n url: sub.url,\n body: delivery.payload,\n headers: {\n 'content-type': 'application/json',\n 'user-agent': 'smrt-webhooks/1',\n 'x-smrt-event': event.event,\n 'x-smrt-delivery': deliveryId,\n 'x-smrt-timestamp': timestamp,\n 'x-smrt-signature': `sha256=${signature}`,\n },\n });\n if (responseStatus < 200 || responseStatus >= 300)\n throw new Error('Webhook rejected.');\n await this.store.finish(\n deliveryId,\n token,\n 'delivered',\n responseStatus,\n null,\n );\n } catch {\n // Never persist/rethrow endpoint errors: URLs, response bodies and secrets may be embedded.\n await this.store.finish(\n deliveryId,\n token,\n delivery.attempts >= 5 ? 'failed' : 'retrying',\n responseStatus,\n 'Webhook delivery failed.',\n );\n throw new Error('Webhook delivery failed.');\n }\n }\n}\nexport async function runWebhookDeliveryJob(\n args: Record<string, unknown>,\n): Promise<void> {\n const runtime =\n typeof args.runtime === 'string' ? runtimes.get(args.runtime) : undefined;\n if (\n !runtime ||\n typeof args.deliveryId !== 'string' ||\n args.tenantId !== requireTenant().tenantId\n ) {\n throw new Error('Invalid webhook job or unavailable runtime.');\n }\n await runtime.deliver(\n args.deliveryId,\n typeof args.generation === 'number' ? args.generation : 0,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { DatabaseInterface } from '@happyvertical/sql';
2
+ import { type WebhookTransport } from './security.js';
3
+ import { WebhookStore } from './store.js';
4
+ import type { WebhookEventEnvelope } from './types.js';
5
+ export interface WebhookDispatcherOptions {
6
+ db: DatabaseInterface;
7
+ runtime: string;
8
+ /** Trusted server integration seam. Default transport pins validated public DNS answers. */
9
+ transport?: WebhookTransport;
10
+ }
11
+ export declare function registerWebhookRuntime(name: string, dispatcher: WebhookDispatcher): void;
12
+ export declare function unregisterWebhookRuntime(name: string): void;
13
+ export declare function assertWebhookEvent(event: WebhookEventEnvelope): void;
14
+ /** Durable tenant-scoped outbound events. Receivers deduplicate x-smrt-delivery. */
15
+ export declare class WebhookDispatcher {
16
+ readonly store: WebhookStore;
17
+ private readonly transport;
18
+ constructor(options: WebhookDispatcherOptions);
19
+ dispatch(event: WebhookEventEnvelope): Promise<string[]>;
20
+ deliver(deliveryId: string, expectedGeneration?: number): Promise<void>;
21
+ }
22
+ export declare function runWebhookDeliveryJob(args: Record<string, unknown>): Promise<void>;
23
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,iBAAiB,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,iBAAiB,GAC5B,IAAI,CAIN;AACD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAepE;AAED,oFAAoF;AACpF,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;gBACjC,OAAO,EAAE,wBAAwB;IAIvC,QAAQ,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAexD,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,IAAI,CAAC;CAmEjB;AACD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC,CAcf"}
@@ -0,0 +1,6 @@
1
+ import type { WebhookDispatcher } from './dispatcher.js';
2
+ /** Opt-in lifecycle binding. Explicit model allowlist prevents recursive/internal data publication.
3
+ * Payloads contain record identity only; use dispatch() for authorized custom projections.
4
+ */
5
+ export declare function registerWebhookModelEvents(dispatcher: WebhookDispatcher, models: readonly string[]): () => void;
6
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,iBAAiB,EAC7B,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,MAAM,IAAI,CA2DZ"}
@@ -0,0 +1,11 @@
1
+ import './__smrt-register__.js';
2
+ export { WebhookAdminService, type WebhookAdminView, webhookAdminView, } from './admin.js';
3
+ export { WebhookChangeFeed, WebhookHistoryExpiredError, } from './change-feed.js';
4
+ export { assertWebhookEvent, registerWebhookRuntime, runWebhookDeliveryJob, unregisterWebhookRuntime, WebhookDispatcher, } from './dispatcher.js';
5
+ export { registerWebhookModelEvents } from './events.js';
6
+ export { WebhookCursor, WebhookDelivery, WebhookDeliveryCollection, WebhookDeliveryTask, WebhookSubscription, WebhookSubscriptionCollection, } from './models.js';
7
+ export { WebhooksRecipe } from './recipes.js';
8
+ export { assertHmacSecret, assertWebhookUrl, isPublicAddress, sendPublicWebhook, type WebhookRequest, type WebhookTransport, } from './security.js';
9
+ export { WebhookStore } from './store.js';
10
+ export type { WebhookDeliveryRecord, WebhookEvent, WebhookEventEnvelope, WebhookSubscriptionRecord, } from './types.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,aAAa,EACb,eAAe,EACf,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ import { a as runWebhookDeliveryJob, i as registerWebhookRuntime, n as assertWebhookEvent, o as unregisterWebhookRuntime, s as WebhookStore, t as WebhookDispatcher } from "./chunks/dispatcher-C5AqtgtK.js";
2
+ import { a as WebhookDeliveryCollection, c as WebhookSubscriptionCollection, d as isPublicAddress, f as sendPublicWebhook, i as WebhookDelivery, l as assertHmacSecret, n as webhookAdminView, o as WebhookDeliveryTask, r as WebhookCursor, s as WebhookSubscription, t as WebhookAdminService, u as assertWebhookUrl } from "./chunks/admin-413jve1I.js";
3
+ import { GlobalInterceptors, ObjectRegistry, SmrtRecipe, getChangesSince } from "@happyvertical/smrt-core";
4
+ import { requireTenant } from "@happyvertical/smrt-tenancy";
5
+ import { randomUUID } from "node:crypto";
6
+ //#region src/__smrt-register__.ts
7
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-webhooks","packageVersion":"0.55.10","objects":{"@happyvertical/smrt-webhooks:WebhookSubscription":{"name":"webhooksubscription","className":"WebhookSubscription","qualifiedName":"@happyvertical/smrt-webhooks:WebhookSubscription","collection":"webhooksubscriptions","description":"Server-only subscription. Hosts authorize administration before accessing it.","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"url":{"type":"text","required":true,"_meta":{"required":true}},"events":{"type":"json","required":false,"_meta":{}},"models":{"type":"json","required":false,"_meta":{}},"secret":{"type":"text","required":true,"_meta":{"sensitive":true,"required":true},"sensitive":true},"enabled":{"type":"boolean","required":false,"default":true,"_meta":{"default":true}}},"methods":{"save":{"name":"save","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_webhook_subscriptions","api":false,"cli":false,"mcp":false,"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"WebhookSubscription","collectionExportName":"WebhookSubscriptionCollection","validationRules":[{"field":"url","rule":"required","fieldType":"text"},{"field":"secret","rule":"required","fieldType":"text"}],"schema":{"tableName":"_smrt_webhook_subscriptions","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_subscriptions\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"url\\" TEXT NOT NULL,\\n \\"events\\" JSON,\\n \\"models\\" JSON,\\n \\"secret\\" TEXT NOT NULL,\\n \\"enabled\\" BOOLEAN DEFAULT TRUE\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"url":{"type":"TEXT","notNull":true,"unique":false},"events":{"type":"JSON","notNull":false,"unique":false},"models":{"type":"JSON","notNull":false,"unique":false},"secret":{"type":"TEXT","notNull":true,"unique":false},"enabled":{"type":"BOOLEAN","notNull":false,"unique":false,"default":true}},"indexes":[{"name":"_smrt_webhook_subscriptions_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"_smrt_webhook_subscriptions_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"4e9ebebe"},"importPath":"@happyvertical/smrt-webhooks"},"@happyvertical/smrt-webhooks:WebhookSubscriptionCollection":{"name":"webhooksubscriptioncollection","className":"WebhookSubscriptionCollection","qualifiedName":"@happyvertical/smrt-webhooks:WebhookSubscriptionCollection","collection":"webhooksubscriptions","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{},"methods":{},"decoratorConfig":{"tableName":"_smrt_webhook_subscriptions","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"WebhookSubscription","exportName":"WebhookSubscriptionCollection","collectionExportName":"WebhookSubscriptionCollectionCollection","schema":{"tableName":"_smrt_webhook_subscriptions","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_subscriptions\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"url\\" TEXT NOT NULL,\\n \\"events\\" JSON,\\n \\"models\\" JSON,\\n \\"secret\\" TEXT NOT NULL,\\n \\"enabled\\" BOOLEAN DEFAULT TRUE\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"url":{"type":"TEXT","notNull":true,"unique":false},"events":{"type":"JSON","notNull":false,"unique":false},"models":{"type":"JSON","notNull":false,"unique":false},"secret":{"type":"TEXT","notNull":true,"unique":false},"enabled":{"type":"BOOLEAN","notNull":false,"unique":false,"default":true}},"indexes":[{"name":"_smrt_webhook_subscriptions_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"_smrt_webhook_subscriptions_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"4e9ebebe"},"importPath":"@happyvertical/smrt-webhooks"},"@happyvertical/smrt-webhooks:WebhookDelivery":{"name":"webhookdelivery","className":"WebhookDelivery","qualifiedName":"@happyvertical/smrt-webhooks:WebhookDelivery","collection":"webhookdeliveries","description":"Durable outbox and safe outcome metadata; payload never appears in public projections.","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"subscriptionId":{"type":"foreignKey","required":false,"related":"WebhookSubscription","_meta":{"onDelete":"RESTRICT","onUpdate":"RESTRICT"}},"eventId":{"type":"text","required":true,"_meta":{"required":true}},"payload":{"type":"text","required":true,"_meta":{"required":true,"sensitive":true},"sensitive":true},"status":{"type":"text","required":false,"default":"pending","_meta":{"default":"pending"}},"generation":{"type":"integer","required":false,"default":0,"_meta":{"default":0}},"attempts":{"type":"integer","required":false,"default":0,"_meta":{"default":0}},"responseStatus":{"type":"integer","required":false,"_meta":{"nullable":true}},"error":{"type":"text","required":false,"_meta":{"nullable":true}},"leaseToken":{"type":"text","required":false,"_meta":{"nullable":true,"sensitive":true},"sensitive":true},"leaseUntil":{"type":"datetime","required":false,"_meta":{"nullable":true}}},"methods":{},"decoratorConfig":{"tableName":"_smrt_webhook_deliveries","api":false,"cli":false,"mcp":false,"indexes":[{"name":"_smrt_webhook_delivery_identity","columns":["tenantId","subscriptionId","eventId"],"unique":true}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"WebhookDelivery","collectionExportName":"WebhookDeliveryCollection","validationRules":[{"field":"eventId","rule":"required","fieldType":"text"},{"field":"payload","rule":"required","fieldType":"text"}],"schema":{"tableName":"_smrt_webhook_deliveries","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_deliveries\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"subscription_id\\" UUID,\\n \\"event_id\\" TEXT NOT NULL,\\n \\"payload\\" TEXT NOT NULL,\\n \\"status\\" TEXT DEFAULT 'pending',\\n \\"generation\\" INTEGER DEFAULT 0,\\n \\"attempts\\" INTEGER DEFAULT 0,\\n \\"response_status\\" INTEGER,\\n \\"error\\" TEXT,\\n \\"lease_token\\" TEXT,\\n \\"lease_until\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"subscription_id":{"type":"UUID","referenceKind":"foreignKey","notNull":false,"unique":false,"foreignKey":{"table":"_smrt_webhook_subscriptions","column":"id","onDelete":"RESTRICT","onUpdate":"RESTRICT"}},"event_id":{"type":"TEXT","notNull":true,"unique":false},"payload":{"type":"TEXT","notNull":true,"unique":false},"status":{"type":"TEXT","notNull":false,"unique":false,"default":"pending"},"generation":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"attempts":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"response_status":{"type":"INTEGER","notNull":false,"unique":false},"error":{"type":"TEXT","notNull":false,"unique":false},"lease_token":{"type":"TEXT","notNull":false,"unique":false},"lease_until":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_webhook_deliveries_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"_smrt_webhook_delivery_identity","columns":["tenant_id","subscription_id","event_id"],"unique":true},{"name":"_smrt_webhook_deliveries_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"_smrt_webhook_deliveries_subscription_id_idx","columns":["subscription_id"]}],"version":"17d7bd27"},"importPath":"@happyvertical/smrt-webhooks"},"@happyvertical/smrt-webhooks:WebhookDeliveryCollection":{"name":"webhookdeliverycollection","className":"WebhookDeliveryCollection","qualifiedName":"@happyvertical/smrt-webhooks:WebhookDeliveryCollection","collection":"webhookdeliveries","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{},"methods":{},"decoratorConfig":{"tableName":"_smrt_webhook_deliveries","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"WebhookDelivery","exportName":"WebhookDeliveryCollection","collectionExportName":"WebhookDeliveryCollectionCollection","schema":{"tableName":"_smrt_webhook_deliveries","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_deliveries\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"subscription_id\\" UUID,\\n \\"event_id\\" TEXT NOT NULL,\\n \\"payload\\" TEXT NOT NULL,\\n \\"status\\" TEXT DEFAULT 'pending',\\n \\"generation\\" INTEGER DEFAULT 0,\\n \\"attempts\\" INTEGER DEFAULT 0,\\n \\"response_status\\" INTEGER,\\n \\"error\\" TEXT,\\n \\"lease_token\\" TEXT,\\n \\"lease_until\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"subscription_id":{"type":"UUID","referenceKind":"foreignKey","notNull":false,"unique":false,"foreignKey":{"table":"_smrt_webhook_subscriptions","column":"id","onDelete":"RESTRICT","onUpdate":"RESTRICT"}},"event_id":{"type":"TEXT","notNull":true,"unique":false},"payload":{"type":"TEXT","notNull":true,"unique":false},"status":{"type":"TEXT","notNull":false,"unique":false,"default":"pending"},"generation":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"attempts":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"response_status":{"type":"INTEGER","notNull":false,"unique":false},"error":{"type":"TEXT","notNull":false,"unique":false},"lease_token":{"type":"TEXT","notNull":false,"unique":false},"lease_until":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_webhook_deliveries_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"_smrt_webhook_delivery_identity","columns":["tenant_id","subscription_id","event_id"],"unique":true},{"name":"_smrt_webhook_deliveries_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"_smrt_webhook_deliveries_subscription_id_idx","columns":["subscription_id"]}],"version":"17d7bd27"},"importPath":"@happyvertical/smrt-webhooks"},"@happyvertical/smrt-webhooks:WebhookDeliveryTask":{"name":"webhookdeliverytask","className":"WebhookDeliveryTask","qualifiedName":"@happyvertical/smrt-webhooks:WebhookDeliveryTask","collection":"webhookdeliverytasks","description":"Jobs queue entry point; persisted arguments contain IDs only.","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{},"methods":{"deliver":{"name":"deliver","async":true,"parameters":[{"name":"args","type":"Record<string, unknown>","optional":true},{"name":"_context","type":"JobExecutionContext","optional":true}],"returnType":"Promise<void>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_webhook_delivery_tasks","api":false,"cli":false,"mcp":false},"extends":"SmrtObject","exportName":"WebhookDeliveryTask","collectionExportName":"WebhookDeliveryTaskCollection","schema":{"tableName":"_smrt_webhook_delivery_tasks","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_delivery_tasks\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"}},"indexes":[{"name":"_smrt_webhook_delivery_tasks_slug_context_idx","columns":["slug","context"],"unique":true},{"name":"_smrt_webhook_delivery_tasks_created_at_idx","columns":["created_at"]}],"version":"17ba7c43"},"importPath":"@happyvertical/smrt-webhooks"},"@happyvertical/smrt-webhooks:WebhookCursor":{"name":"webhookcursor","className":"WebhookCursor","qualifiedName":"@happyvertical/smrt-webhooks:WebhookCursor","collection":"webhookcursors","description":"Tenant-bound durable cursor for replayable model change-feed publication.","filePath":"/home/runner/work/smrt/smrt/packages/webhooks/src/models.ts","packageName":"@happyvertical/smrt-webhooks","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"runtime":{"type":"text","required":true,"_meta":{"required":true}},"position":{"type":"integer","required":false,"default":0,"_meta":{"default":0}}},"methods":{},"decoratorConfig":{"tableName":"_smrt_webhook_cursors","api":false,"cli":false,"mcp":false,"indexes":[{"name":"_smrt_webhook_cursor_identity","columns":["tenantId","runtime"],"unique":true}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"WebhookCursor","collectionExportName":"WebhookCursorCollection","validationRules":[{"field":"runtime","rule":"required","fieldType":"text"}],"schema":{"tableName":"_smrt_webhook_cursors","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_webhook_cursors\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"runtime\\" TEXT NOT NULL,\\n \\"position\\" INTEGER DEFAULT 0\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"runtime":{"type":"TEXT","notNull":true,"unique":false},"position":{"type":"INTEGER","notNull":false,"unique":false,"default":0}},"indexes":[{"name":"_smrt_webhook_cursors_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"_smrt_webhook_cursor_identity","columns":["tenant_id","runtime"],"unique":true},{"name":"_smrt_webhook_cursors_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"ba1ea19d"},"importPath":"@happyvertical/smrt-webhooks"}},"moduleType":"smrt","recipes":[{"id":"integrations.webhooks","className":"WebhooksRecipe","label":"Outbound webhooks","summary":"Deliver signed model events and inspect delivery attempts from a server.","synonyms":[],"models":["@happyvertical/smrt-webhooks:WebhookSubscription","@happyvertical/smrt-webhooks:WebhookDelivery","@happyvertical/smrt-webhooks:WebhookDeliveryTask","@happyvertical/smrt-webhooks:WebhookCursor"],"nav":[],"requires":[],"group":{"id":"integrations","label":"Integrations","summary":"Connect your application to external services."},"help":{"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","fieldRefs":[]},"surfaces":[{"kind":"settings-panel","export":"@happyvertical/smrt-webhooks/svelte#WebhookAdmin","label":"Outbound webhooks"}],"runtime":"server","demo":{"mode":"server","reasons":["The recipe declares runtime \\"server\\".","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)."]}}],"smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-jobs","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3615","#3624","#3627","#3717"],"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.","via":["@happyvertical/smrt-jobs","@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
8
+ //#endregion
9
+ //#region src/change-feed.ts
10
+ var WebhookHistoryExpiredError = class extends Error {
11
+ constructor(resumeCursor) {
12
+ super("Webhook change history expired; reconcile source state before resetting the cursor.");
13
+ this.resumeCursor = resumeCursor;
14
+ }
15
+ resumeCursor;
16
+ };
17
+ var WebhookChangeFeed = class {
18
+ constructor(dispatcher, models) {
19
+ this.dispatcher = dispatcher;
20
+ this.models = models;
21
+ if (!Object.keys(models).length || Object.keys(models).some((table) => table.startsWith("_smrt_")) || Object.values(models).some((model) => !model)) throw new Error("Explicit application model tables are required.");
22
+ }
23
+ dispatcher;
24
+ models;
25
+ /** Cursor only advances after all matched changes have durable outbox/jobs.
26
+ * A crash or partial failure replays the same stable identities on the next poll.
27
+ * History pruning fails loudly: the host must reconcile source state before reset.
28
+ */
29
+ async poll() {
30
+ const tenantId = requireTenant().tenantId;
31
+ const { db, runtime } = this.dispatcher.store;
32
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33
+ const id = randomUUID();
34
+ await db.query(`INSERT INTO _smrt_webhook_cursors (id, slug, context, tenant_id, runtime, position, created_at, updated_at)
35
+ VALUES (?, ?, '', ?, ?, 0, ?, ?) ON CONFLICT (tenant_id, runtime) DO NOTHING`, id, id, tenantId, runtime, now, now);
36
+ const rows = await db.query("SELECT position FROM _smrt_webhook_cursors WHERE tenant_id = ? AND runtime = ?", tenantId, runtime);
37
+ const since = Number(rows.rows[0].position);
38
+ const page = await getChangesSince(db, {
39
+ since,
40
+ tenantId,
41
+ tables: Object.keys(this.models),
42
+ limit: 500
43
+ });
44
+ if (page.resyncRequired) throw new WebhookHistoryExpiredError(page.resyncCursor ?? page.cursor);
45
+ let published = 0;
46
+ for (const change of page.changes) {
47
+ if (change.tenantId !== tenantId || !change.rowId || !this.models[change.table]) continue;
48
+ await this.dispatcher.dispatch({
49
+ id: `change:${change.seq}`,
50
+ event: change.operation,
51
+ model: this.models[change.table],
52
+ occurredAt: change.timestamp,
53
+ data: { id: change.rowId }
54
+ });
55
+ published++;
56
+ }
57
+ await db.query("UPDATE _smrt_webhook_cursors SET position = ?, updated_at = ? WHERE tenant_id = ? AND runtime = ? AND position = ?", page.cursor, (/* @__PURE__ */ new Date()).toISOString(), tenantId, runtime, since);
58
+ return {
59
+ cursor: page.cursor,
60
+ published
61
+ };
62
+ }
63
+ /** Operator-only recovery: call after reconciling retained source state externally. */
64
+ async resumeAfterResync(cursor) {
65
+ if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error("Invalid webhook change cursor.");
66
+ const tenantId = requireTenant().tenantId;
67
+ const { db, runtime } = this.dispatcher.store;
68
+ await db.query("UPDATE _smrt_webhook_cursors SET position = ? WHERE tenant_id = ? AND runtime = ?", cursor, tenantId, runtime);
69
+ }
70
+ };
71
+ //#endregion
72
+ //#region src/events.ts
73
+ function registerWebhookModelEvents(dispatcher, models) {
74
+ const internal = /* @__PURE__ */ new Set([
75
+ "WebhookSubscription",
76
+ "WebhookDelivery",
77
+ "WebhookDeliveryTask",
78
+ "WebhookCursor",
79
+ "SmrtJob",
80
+ "SmrtJobEvent",
81
+ "SmrtWorker"
82
+ ]);
83
+ if (models.some((name) => internal.has(name.split(":").at(-1) ?? name))) throw new Error("Internal queue and webhook models cannot publish lifecycle webhooks.");
84
+ const allowed = new Set(models);
85
+ const pending = /* @__PURE__ */ new WeakMap();
86
+ const matches = (context) => allowed.has(context.qualifiedClassName ?? context.className);
87
+ async function emit(instance, context, event) {
88
+ if (!matches(context)) return;
89
+ const tenantId = requireTenant().tenantId;
90
+ if ("tenantId" in instance && instance.tenantId !== tenantId) throw new Error("Webhook model tenant mismatch.");
91
+ await dispatcher.dispatch({
92
+ id: pending.get(context)?.id ?? randomUUID(),
93
+ event,
94
+ model: context.qualifiedClassName ?? context.className,
95
+ occurredAt: context.timestamp.toISOString(),
96
+ data: { id: instance.id }
97
+ });
98
+ }
99
+ const interceptor = {
100
+ name: `webhook-model-events:${randomUUID()}`,
101
+ beforeSave(instance, context) {
102
+ if (matches(context)) pending.set(context, {
103
+ event: instance.isPersisted ? "update" : "create",
104
+ id: randomUUID()
105
+ });
106
+ },
107
+ async afterSave(instance, context) {
108
+ await emit(instance, context, pending.get(context)?.event ?? "update");
109
+ },
110
+ async afterDelete(instance, context) {
111
+ await emit(instance, context, "delete");
112
+ }
113
+ };
114
+ GlobalInterceptors.register(interceptor);
115
+ return () => {
116
+ GlobalInterceptors.unregister(interceptor);
117
+ };
118
+ }
119
+ //#endregion
120
+ //#region src/recipes.ts
121
+ var WebhooksRecipe = class extends SmrtRecipe {
122
+ static id = "integrations.webhooks";
123
+ static label = "Outbound webhooks";
124
+ static summary = "Deliver signed model events and inspect delivery attempts from a server.";
125
+ static models = [
126
+ WebhookSubscription,
127
+ WebhookDelivery,
128
+ WebhookDeliveryTask,
129
+ WebhookCursor
130
+ ];
131
+ static group = {
132
+ id: "integrations",
133
+ label: "Integrations",
134
+ summary: "Connect your application to external services."
135
+ };
136
+ static runtime = "server";
137
+ static surfaces = [{
138
+ kind: "settings-panel",
139
+ export: "@happyvertical/smrt-webhooks/svelte#WebhookAdmin",
140
+ label: "Outbound webhooks"
141
+ }];
142
+ static nav = [];
143
+ static help = "./webhooks.recipe.md";
144
+ };
145
+ //#endregion
146
+ export { WebhookAdminService, WebhookChangeFeed, WebhookCursor, WebhookDelivery, WebhookDeliveryCollection, WebhookDeliveryTask, WebhookDispatcher, WebhookHistoryExpiredError, WebhookStore, WebhookSubscription, WebhookSubscriptionCollection, WebhooksRecipe, assertHmacSecret, assertWebhookEvent, assertWebhookUrl, isPublicAddress, registerWebhookModelEvents, registerWebhookRuntime, runWebhookDeliveryJob, sendPublicWebhook, unregisterWebhookRuntime, webhookAdminView };
147
+
148
+ //# sourceMappingURL=index.js.map