@fonderie/webhooks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fonderie, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @fonderie/webhooks
2
+
3
+ Outgoing webhooks: let your users register endpoints, fan workspace events
4
+ out to them, and track every delivery attempt with retries and status.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @fonderie/webhooks
10
+ ```
11
+
12
+ ## Use
13
+
14
+ ```ts
15
+ import { FonderieApp, defineConfig } from '@fonderie/core';
16
+ import { WebhooksModule } from '@fonderie/webhooks';
17
+
18
+ const app = await new FonderieApp(defineConfig({}))
19
+ .register(new WebhooksModule())
20
+ .boot();
21
+ ```
22
+
23
+ ```ts
24
+ import type { IWebhookEndpoint, IWebhookDelivery, DeliveryStatus } from '@fonderie/webhooks';
25
+ ```
26
+
27
+ ## Why this exists
28
+
29
+ You've shipped this plumbing before — auth, teams, billing, messaging —
30
+ and the next project will ask for it again. Fonderie packages it once:
31
+ plain TypeScript modules for
32
+ [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
33
+ PostgreSQL-backed, self-hosted, MIT. No external control plane, no
34
+ per-seat anything. Register the modules you need; skip the ones you don't.
35
+
36
+ **This package owns** how the outside world listens. Your users register endpoints;
37
+ this brick fans workspace events out to them and tracks every delivery.
38
+
39
+ Browse the whole set at
40
+ [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
41
+ [@fonderiejs](https://x.com/fonderiejs)
42
+
43
+ ## License
44
+
45
+ MIT © Fonderie, Inc.
package/dist/index.cjs ADDED
@@ -0,0 +1,519 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ WebhooksModule: () => WebhooksModule
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/models/endpoint.model.ts
28
+ var COLS = `id, workspace_id as "workspaceId", url, secret, events,
29
+ enabled, created_at as "createdAt"`;
30
+ var EndpointModel = class {
31
+ constructor(store) {
32
+ this.store = store;
33
+ }
34
+ store;
35
+ async create(data) {
36
+ const [row] = await this.store.query(
37
+ `INSERT INTO fonderie_webhook_endpoints (workspace_id, url, secret, events)
38
+ VALUES ($1, $2, $3, $4)
39
+ RETURNING ${COLS}`,
40
+ [data.workspaceId, data.url, data.secret, data.events]
41
+ );
42
+ return row;
43
+ }
44
+ list(workspaceId) {
45
+ return this.store.query(
46
+ `SELECT ${COLS} FROM fonderie_webhook_endpoints
47
+ WHERE workspace_id = $1
48
+ ORDER BY created_at DESC`,
49
+ [workspaceId]
50
+ );
51
+ }
52
+ async findById(id, workspaceId) {
53
+ const [row] = await this.store.query(
54
+ `SELECT ${COLS} FROM fonderie_webhook_endpoints
55
+ WHERE id = $1 AND workspace_id = $2`,
56
+ [id, workspaceId]
57
+ );
58
+ return row ?? null;
59
+ }
60
+ async update(id, workspaceId, data) {
61
+ const sets = [];
62
+ const params = [id, workspaceId];
63
+ if (data.url !== void 0) {
64
+ params.push(data.url);
65
+ sets.push(`url = $${params.length}`);
66
+ }
67
+ if (data.events !== void 0) {
68
+ params.push(data.events);
69
+ sets.push(`events = $${params.length}`);
70
+ }
71
+ if (data.enabled !== void 0) {
72
+ params.push(data.enabled);
73
+ sets.push(`enabled = $${params.length}`);
74
+ }
75
+ if (sets.length === 0) return this.findById(id, workspaceId);
76
+ const [row] = await this.store.query(
77
+ `UPDATE fonderie_webhook_endpoints SET ${sets.join(", ")}
78
+ WHERE id = $1 AND workspace_id = $2
79
+ RETURNING ${COLS}`,
80
+ params
81
+ );
82
+ return row ?? null;
83
+ }
84
+ async delete(id, workspaceId) {
85
+ const rows = await this.store.query(
86
+ `DELETE FROM fonderie_webhook_endpoints WHERE id = $1 AND workspace_id = $2 RETURNING id`,
87
+ [id, workspaceId]
88
+ );
89
+ return rows.length > 0;
90
+ }
91
+ findForEvent(workspaceId, eventType) {
92
+ return this.store.query(
93
+ `SELECT ${COLS} FROM fonderie_webhook_endpoints
94
+ WHERE workspace_id = $1
95
+ AND enabled = true
96
+ AND (events = '{}' OR $2 = ANY(events))`,
97
+ [workspaceId, eventType]
98
+ );
99
+ }
100
+ };
101
+
102
+ // src/models/delivery.model.ts
103
+ var COLS2 = `id, endpoint_id as "endpointId", event_id as "eventId",
104
+ event_type as "eventType", payload, status, attempts,
105
+ response_status as "responseStatus", response_body as "responseBody",
106
+ next_attempt_at as "nextAttemptAt", delivered_at as "deliveredAt",
107
+ created_at as "createdAt"`;
108
+ var D_COLS = `d.id, d.endpoint_id as "endpointId", d.event_id as "eventId",
109
+ d.event_type as "eventType", d.payload, d.status, d.attempts,
110
+ d.response_status as "responseStatus", d.response_body as "responseBody",
111
+ d.next_attempt_at as "nextAttemptAt", d.delivered_at as "deliveredAt",
112
+ d.created_at as "createdAt"`;
113
+ var DeliveryModel = class {
114
+ constructor(store) {
115
+ this.store = store;
116
+ }
117
+ store;
118
+ async create(data) {
119
+ const [row] = await this.store.query(
120
+ `INSERT INTO fonderie_webhook_deliveries (endpoint_id, event_id, event_type, payload)
121
+ VALUES ($1, $2, $3, $4)
122
+ RETURNING ${COLS2}`,
123
+ [data.endpointId, data.eventId, data.eventType, JSON.stringify(data.payload)]
124
+ );
125
+ return row;
126
+ }
127
+ async markResult(id, result) {
128
+ await this.store.query(
129
+ `UPDATE fonderie_webhook_deliveries
130
+ SET status = $2,
131
+ attempts = attempts + 1,
132
+ response_status = $3,
133
+ response_body = $4,
134
+ next_attempt_at = $5,
135
+ delivered_at = $6
136
+ WHERE id = $1`,
137
+ [
138
+ id,
139
+ result.ok ? "delivered" : "failed",
140
+ result.responseStatus,
141
+ result.responseBody,
142
+ result.nextAttemptAt,
143
+ result.ok ? /* @__PURE__ */ new Date() : null
144
+ ]
145
+ );
146
+ }
147
+ listByEndpoint(endpointId, limit = 50) {
148
+ return this.store.query(
149
+ `SELECT ${COLS2} FROM fonderie_webhook_deliveries
150
+ WHERE endpoint_id = $1
151
+ ORDER BY created_at DESC
152
+ LIMIT $2`,
153
+ [endpointId, limit]
154
+ );
155
+ }
156
+ claimForRetry(limit = 10) {
157
+ return this.store.query(
158
+ `SELECT ${D_COLS},
159
+ e.url, e.secret
160
+ FROM fonderie_webhook_deliveries d
161
+ JOIN fonderie_webhook_endpoints e ON e.id = d.endpoint_id
162
+ WHERE d.status = 'failed'
163
+ AND d.next_attempt_at IS NOT NULL
164
+ AND d.next_attempt_at <= now()
165
+ AND e.enabled = true
166
+ ORDER BY d.next_attempt_at
167
+ LIMIT $1`,
168
+ [limit]
169
+ );
170
+ }
171
+ };
172
+
173
+ // src/signing.ts
174
+ var import_node_crypto = require("crypto");
175
+ function generateSecret() {
176
+ return (0, import_node_crypto.randomBytes)(32).toString("hex");
177
+ }
178
+ function signPayload(secret, body) {
179
+ return `sha256=${(0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex")}`;
180
+ }
181
+
182
+ // src/dispatcher.ts
183
+ var WebhookDispatcher = class {
184
+ constructor(store, config = {}) {
185
+ this.store = store;
186
+ this.config = config;
187
+ this.maxAttempts = config.maxAttempts ?? 3;
188
+ this.retryDelays = config.retryDelays ?? [6e4, 3e5, 18e5];
189
+ }
190
+ store;
191
+ config;
192
+ maxAttempts;
193
+ retryDelays;
194
+ // Called by the bus consumer for every event.
195
+ // Skips events that don't carry a workspaceId — not workspace-scoped.
196
+ async dispatch(payload, meta) {
197
+ const workspaceId = payload["workspaceId"];
198
+ if (typeof workspaceId !== "string") return;
199
+ const endpoints = await new EndpointModel(this.store).findForEvent(workspaceId, meta.type);
200
+ if (endpoints.length === 0) return;
201
+ const deliveries = new DeliveryModel(this.store);
202
+ await Promise.allSettled(endpoints.map((ep) => this.deliver(ep, payload, meta, deliveries)));
203
+ }
204
+ // Retries failed deliveries whose next_attempt_at has passed.
205
+ async retry() {
206
+ const deliveries = new DeliveryModel(this.store);
207
+ const pending = await deliveries.claimForRetry();
208
+ await Promise.allSettled(
209
+ pending.map(
210
+ ({ delivery, url, secret }) => this.attemptDelivery(url, secret, delivery, deliveries)
211
+ )
212
+ );
213
+ }
214
+ async deliver(endpoint, payload, meta, deliveries) {
215
+ const delivery = await deliveries.create({
216
+ endpointId: endpoint.id,
217
+ eventId: meta.id,
218
+ eventType: meta.type,
219
+ payload
220
+ });
221
+ await this.attemptDelivery(endpoint.url, endpoint.secret, delivery, deliveries);
222
+ }
223
+ async attemptDelivery(url, secret, delivery, deliveries) {
224
+ const body = JSON.stringify({
225
+ id: delivery.eventId,
226
+ type: delivery.eventType,
227
+ data: delivery.payload
228
+ });
229
+ const signature = signPayload(secret, body);
230
+ try {
231
+ const res = await fetch(url, {
232
+ method: "POST",
233
+ headers: {
234
+ "Content-Type": "application/json",
235
+ "X-Webhook-Signature": signature,
236
+ "X-Webhook-Event": delivery.eventType,
237
+ "X-Webhook-ID": delivery.id
238
+ },
239
+ body,
240
+ signal: AbortSignal.timeout(1e4)
241
+ });
242
+ const responseBody = await res.text().catch(() => "");
243
+ await deliveries.markResult(delivery.id, {
244
+ ok: res.ok,
245
+ responseStatus: res.status,
246
+ responseBody,
247
+ nextAttemptAt: res.ok ? null : this.nextRetryAt(delivery.attempts)
248
+ });
249
+ } catch (err) {
250
+ await deliveries.markResult(delivery.id, {
251
+ ok: false,
252
+ responseStatus: null,
253
+ responseBody: err instanceof Error ? err.message : String(err),
254
+ nextAttemptAt: this.nextRetryAt(delivery.attempts)
255
+ });
256
+ }
257
+ }
258
+ nextRetryAt(currentAttempts) {
259
+ if (currentAttempts + 1 >= this.maxAttempts) return null;
260
+ const delay = this.retryDelays[currentAttempts] ?? this.retryDelays[this.retryDelays.length - 1];
261
+ return new Date(Date.now() + delay);
262
+ }
263
+ };
264
+
265
+ // src/routes.ts
266
+ var import_core = require("@fonderie/core");
267
+ var import_middlewares = require("@fonderie/core/middlewares");
268
+ var import_middlewares2 = require("@fonderie/core/middlewares");
269
+
270
+ // src/dtos/webhook.ts
271
+ function toEndpointDTO(e) {
272
+ return {
273
+ id: e.id,
274
+ url: e.url,
275
+ events: e.events,
276
+ enabled: e.enabled,
277
+ createdAt: e.createdAt.toISOString()
278
+ };
279
+ }
280
+ function toEndpointCreatedDTO(e) {
281
+ return { ...toEndpointDTO(e), secret: e.secret };
282
+ }
283
+ function toDeliveryDTO(d) {
284
+ return {
285
+ id: d.id,
286
+ eventId: d.eventId,
287
+ eventType: d.eventType,
288
+ status: d.status,
289
+ attempts: d.attempts,
290
+ responseStatus: d.responseStatus,
291
+ deliveredAt: d.deliveredAt?.toISOString() ?? null,
292
+ createdAt: d.createdAt.toISOString()
293
+ };
294
+ }
295
+
296
+ // src/routes.ts
297
+ function buildWebhookRoutes(store, config = {}) {
298
+ return [
299
+ [
300
+ "POST",
301
+ "/webhooks",
302
+ import_middlewares.requireAuth,
303
+ import_middlewares2.withBody,
304
+ async (ctx) => {
305
+ if (!ctx.workspace)
306
+ return (0, import_core.setApiResponse)(
307
+ import_core.HTTP.UNPROCESSABLE,
308
+ "MISSING_WORKSPACE",
309
+ "Workspace context required"
310
+ );
311
+ const body = ctx.meta["body"];
312
+ if (!body?.url)
313
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "MISSING_FIELD", "url is required");
314
+ const endpoint = await new EndpointModel(store).create({
315
+ workspaceId: ctx.workspace.id,
316
+ url: body.url,
317
+ secret: generateSecret(),
318
+ events: body.events ?? []
319
+ });
320
+ return (0, import_core.setApiResponse)(
321
+ import_core.HTTP.CREATED,
322
+ "WEBHOOK_CREATED",
323
+ "Webhook endpoint registered.",
324
+ toEndpointCreatedDTO(endpoint)
325
+ );
326
+ }
327
+ ],
328
+ [
329
+ "GET",
330
+ "/webhooks",
331
+ import_middlewares.requireAuth,
332
+ async (ctx) => {
333
+ if (!ctx.workspace)
334
+ return (0, import_core.setApiResponse)(
335
+ import_core.HTTP.UNPROCESSABLE,
336
+ "MISSING_WORKSPACE",
337
+ "Workspace context required"
338
+ );
339
+ const list = await new EndpointModel(store).list(ctx.workspace.id);
340
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "WEBHOOKS_FETCHED", "Webhook endpoints retrieved.", {
341
+ endpoints: list.map(toEndpointDTO)
342
+ });
343
+ }
344
+ ],
345
+ [
346
+ "GET",
347
+ "/webhooks/:endpointId",
348
+ import_middlewares.requireAuth,
349
+ async (ctx) => {
350
+ if (!ctx.workspace)
351
+ return (0, import_core.setApiResponse)(
352
+ import_core.HTTP.UNPROCESSABLE,
353
+ "MISSING_WORKSPACE",
354
+ "Workspace context required"
355
+ );
356
+ const { endpointId } = ctx.meta["params"];
357
+ const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);
358
+ if (!endpoint)
359
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Webhook endpoint not found");
360
+ return (0, import_core.setApiResponse)(
361
+ import_core.HTTP.OK,
362
+ "WEBHOOK_FETCHED",
363
+ "Webhook endpoint retrieved.",
364
+ toEndpointDTO(endpoint)
365
+ );
366
+ }
367
+ ],
368
+ [
369
+ "PATCH",
370
+ "/webhooks/:endpointId",
371
+ import_middlewares.requireAuth,
372
+ import_middlewares2.withBody,
373
+ async (ctx) => {
374
+ if (!ctx.workspace)
375
+ return (0, import_core.setApiResponse)(
376
+ import_core.HTTP.UNPROCESSABLE,
377
+ "MISSING_WORKSPACE",
378
+ "Workspace context required"
379
+ );
380
+ const { endpointId } = ctx.meta["params"];
381
+ const body = ctx.meta["body"];
382
+ const patch = {};
383
+ if (body?.url !== void 0) patch.url = body.url;
384
+ if (body?.events !== void 0) patch.events = body.events;
385
+ if (body?.enabled !== void 0) patch.enabled = body.enabled;
386
+ const updated = await new EndpointModel(store).update(endpointId, ctx.workspace.id, patch);
387
+ if (!updated)
388
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Webhook endpoint not found");
389
+ return (0, import_core.setApiResponse)(
390
+ import_core.HTTP.OK,
391
+ "WEBHOOK_UPDATED",
392
+ "Webhook endpoint updated.",
393
+ toEndpointDTO(updated)
394
+ );
395
+ }
396
+ ],
397
+ [
398
+ "DELETE",
399
+ "/webhooks/:endpointId",
400
+ import_middlewares.requireAuth,
401
+ async (ctx) => {
402
+ if (!ctx.workspace)
403
+ return (0, import_core.setApiResponse)(
404
+ import_core.HTTP.UNPROCESSABLE,
405
+ "MISSING_WORKSPACE",
406
+ "Workspace context required"
407
+ );
408
+ const { endpointId } = ctx.meta["params"];
409
+ const deleted = await new EndpointModel(store).delete(endpointId, ctx.workspace.id);
410
+ if (!deleted)
411
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Webhook endpoint not found");
412
+ return new Response(null, { status: import_core.HTTP.NO_CONTENT });
413
+ }
414
+ ],
415
+ [
416
+ "GET",
417
+ "/webhooks/:endpointId/deliveries",
418
+ import_middlewares.requireAuth,
419
+ async (ctx) => {
420
+ if (!ctx.workspace)
421
+ return (0, import_core.setApiResponse)(
422
+ import_core.HTTP.UNPROCESSABLE,
423
+ "MISSING_WORKSPACE",
424
+ "Workspace context required"
425
+ );
426
+ const { endpointId } = ctx.meta["params"];
427
+ const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);
428
+ if (!endpoint)
429
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Webhook endpoint not found");
430
+ const list = await new DeliveryModel(store).listByEndpoint(endpointId);
431
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "DELIVERIES_FETCHED", "Deliveries retrieved.", {
432
+ deliveries: list.map(toDeliveryDTO)
433
+ });
434
+ }
435
+ ],
436
+ [
437
+ "POST",
438
+ "/webhooks/:endpointId/test",
439
+ import_middlewares.requireAuth,
440
+ async (ctx) => {
441
+ if (!ctx.workspace)
442
+ return (0, import_core.setApiResponse)(
443
+ import_core.HTTP.UNPROCESSABLE,
444
+ "MISSING_WORKSPACE",
445
+ "Workspace context required"
446
+ );
447
+ const { endpointId } = ctx.meta["params"];
448
+ const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);
449
+ if (!endpoint)
450
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Webhook endpoint not found");
451
+ const body = JSON.stringify({
452
+ id: `test-${Date.now()}`,
453
+ type: "webhook.test",
454
+ data: { workspaceId: ctx.workspace.id, message: "Test webhook delivery." }
455
+ });
456
+ try {
457
+ const res = await fetch(endpoint.url, {
458
+ method: "POST",
459
+ headers: {
460
+ "Content-Type": "application/json",
461
+ "X-Webhook-Signature": signPayload(endpoint.secret, body),
462
+ "X-Webhook-Event": "webhook.test"
463
+ },
464
+ body,
465
+ signal: AbortSignal.timeout(1e4)
466
+ });
467
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "TEST_SENT", "Test delivery attempted.", {
468
+ status: res.status,
469
+ ok: res.ok
470
+ });
471
+ } catch (err) {
472
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "TEST_SENT", "Test delivery attempted.", {
473
+ status: null,
474
+ ok: false,
475
+ error: err instanceof Error ? err.message : String(err)
476
+ });
477
+ }
478
+ }
479
+ ]
480
+ ];
481
+ }
482
+
483
+ // src/module.ts
484
+ var WebhooksModule = class {
485
+ constructor(store, config = {}, bus) {
486
+ this.store = store;
487
+ this.config = config;
488
+ this.bus = bus;
489
+ }
490
+ store;
491
+ config;
492
+ bus;
493
+ name = "@fonderie/webhooks";
494
+ deps = ["@fonderie/auth", "@fonderie/workspaces"];
495
+ retryTimer;
496
+ install(app) {
497
+ const dispatcher = new WebhookDispatcher(this.store, this.config);
498
+ this.bus?.on(
499
+ "*",
500
+ async (payload, meta) => {
501
+ await dispatcher.dispatch(payload, meta);
502
+ },
503
+ "webhooks"
504
+ );
505
+ const interval = this.config.retryInterval ?? 6e4;
506
+ this.retryTimer = setInterval(() => {
507
+ dispatcher.retry().catch((err) => console.error("[webhooks] retry error:", err));
508
+ }, interval);
509
+ const routes = buildWebhookRoutes(this.store, this.config);
510
+ for (const [method, path, ...handlers] of routes) {
511
+ app.addRoute(method, path, ...handlers);
512
+ }
513
+ }
514
+ };
515
+ // Annotate the CommonJS export names for ESM import in node:
516
+ 0 && (module.exports = {
517
+ WebhooksModule
518
+ });
519
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/models/endpoint.model.ts","../src/models/delivery.model.ts","../src/signing.ts","../src/dispatcher.ts","../src/routes.ts","../src/dtos/webhook.ts","../src/module.ts"],"sourcesContent":["export { WebhooksModule } from './module';\nexport type { IWebhooksConfig } from './config';\nexport type {\n\tIWebhookEndpoint,\n\tIWebhookDelivery,\n\tDeliveryStatus,\n} from './types';\nexport type {\n\tIWebhookEndpointDTO,\n\tIWebhookEndpointCreatedDTO,\n\tIWebhookDeliveryDTO,\n} from './dtos/webhook';\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IWebhookEndpoint } from '../types';\n\nconst COLS = `id, workspace_id as \"workspaceId\", url, secret, events,\n enabled, created_at as \"createdAt\"`;\n\nexport class EndpointModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync create(data: {\n\t\tworkspaceId: string;\n\t\turl: string;\n\t\tsecret: string;\n\t\tevents: string[];\n\t}): Promise<IWebhookEndpoint> {\n\t\tconst [row] = await this.store.query<IWebhookEndpoint>(\n\t\t\t`INSERT INTO fonderie_webhook_endpoints (workspace_id, url, secret, events)\n\t\t\t VALUES ($1, $2, $3, $4)\n\t\t\t RETURNING ${COLS}`,\n\t\t\t[data.workspaceId, data.url, data.secret, data.events],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tlist(workspaceId: string): Promise<IWebhookEndpoint[]> {\n\t\treturn this.store.query<IWebhookEndpoint>(\n\t\t\t`SELECT ${COLS} FROM fonderie_webhook_endpoints\n\t\t\t WHERE workspace_id = $1\n\t\t\t ORDER BY created_at DESC`,\n\t\t\t[workspaceId],\n\t\t);\n\t}\n\n\tasync findById(id: string, workspaceId: string): Promise<IWebhookEndpoint | null> {\n\t\tconst [row] = await this.store.query<IWebhookEndpoint>(\n\t\t\t`SELECT ${COLS} FROM fonderie_webhook_endpoints\n\t\t\t WHERE id = $1 AND workspace_id = $2`,\n\t\t\t[id, workspaceId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(\n\t\tid: string,\n\t\tworkspaceId: string,\n\t\tdata: { url?: string; events?: string[]; enabled?: boolean },\n\t): Promise<IWebhookEndpoint | null> {\n\t\tconst sets: string[] = [];\n\t\tconst params: unknown[] = [id, workspaceId];\n\n\t\tif (data.url !== undefined) {\n\t\t\tparams.push(data.url);\n\t\t\tsets.push(`url = $${params.length}`);\n\t\t}\n\t\tif (data.events !== undefined) {\n\t\t\tparams.push(data.events);\n\t\t\tsets.push(`events = $${params.length}`);\n\t\t}\n\t\tif (data.enabled !== undefined) {\n\t\t\tparams.push(data.enabled);\n\t\t\tsets.push(`enabled = $${params.length}`);\n\t\t}\n\n\t\tif (sets.length === 0) return this.findById(id, workspaceId);\n\n\t\tconst [row] = await this.store.query<IWebhookEndpoint>(\n\t\t\t`UPDATE fonderie_webhook_endpoints SET ${sets.join(', ')}\n\t\t\t WHERE id = $1 AND workspace_id = $2\n\t\t\t RETURNING ${COLS}`,\n\t\t\tparams,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync delete(id: string, workspaceId: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`DELETE FROM fonderie_webhook_endpoints WHERE id = $1 AND workspace_id = $2 RETURNING id`,\n\t\t\t[id, workspaceId],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\tfindForEvent(workspaceId: string, eventType: string): Promise<IWebhookEndpoint[]> {\n\t\treturn this.store.query<IWebhookEndpoint>(\n\t\t\t`SELECT ${COLS} FROM fonderie_webhook_endpoints\n\t\t\t WHERE workspace_id = $1\n\t\t\t AND enabled = true\n\t\t\t AND (events = '{}' OR $2 = ANY(events))`,\n\t\t\t[workspaceId, eventType],\n\t\t);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IWebhookDelivery } from '../types';\n\nconst COLS = `id, endpoint_id as \"endpointId\", event_id as \"eventId\",\n event_type as \"eventType\", payload, status, attempts,\n response_status as \"responseStatus\", response_body as \"responseBody\",\n next_attempt_at as \"nextAttemptAt\", delivered_at as \"deliveredAt\",\n created_at as \"createdAt\"`;\n\nconst D_COLS = `d.id, d.endpoint_id as \"endpointId\", d.event_id as \"eventId\",\n d.event_type as \"eventType\", d.payload, d.status, d.attempts,\n d.response_status as \"responseStatus\", d.response_body as \"responseBody\",\n d.next_attempt_at as \"nextAttemptAt\", d.delivered_at as \"deliveredAt\",\n d.created_at as \"createdAt\"`;\n\nexport interface IPendingRetry {\n\tdelivery: IWebhookDelivery;\n\turl: string;\n\tsecret: string;\n}\n\nexport class DeliveryModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync create(data: {\n\t\tendpointId: string;\n\t\teventId: string;\n\t\teventType: string;\n\t\tpayload: Record<string, unknown>;\n\t}): Promise<IWebhookDelivery> {\n\t\tconst [row] = await this.store.query<IWebhookDelivery>(\n\t\t\t`INSERT INTO fonderie_webhook_deliveries (endpoint_id, event_id, event_type, payload)\n\t\t\t VALUES ($1, $2, $3, $4)\n\t\t\t RETURNING ${COLS}`,\n\t\t\t[data.endpointId, data.eventId, data.eventType, JSON.stringify(data.payload)],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync markResult(\n\t\tid: string,\n\t\tresult: {\n\t\t\tok: boolean;\n\t\t\tresponseStatus: number | null;\n\t\t\tresponseBody: string;\n\t\t\tnextAttemptAt: Date | null;\n\t\t},\n\t): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_webhook_deliveries\n\t\t\t SET status = $2,\n\t\t\t attempts = attempts + 1,\n\t\t\t response_status = $3,\n\t\t\t response_body = $4,\n\t\t\t next_attempt_at = $5,\n\t\t\t delivered_at = $6\n\t\t\t WHERE id = $1`,\n\t\t\t[\n\t\t\t\tid,\n\t\t\t\tresult.ok ? 'delivered' : 'failed',\n\t\t\t\tresult.responseStatus,\n\t\t\t\tresult.responseBody,\n\t\t\t\tresult.nextAttemptAt,\n\t\t\t\tresult.ok ? new Date() : null,\n\t\t\t],\n\t\t);\n\t}\n\n\tlistByEndpoint(endpointId: string, limit = 50): Promise<IWebhookDelivery[]> {\n\t\treturn this.store.query<IWebhookDelivery>(\n\t\t\t`SELECT ${COLS} FROM fonderie_webhook_deliveries\n\t\t\t WHERE endpoint_id = $1\n\t\t\t ORDER BY created_at DESC\n\t\t\t LIMIT $2`,\n\t\t\t[endpointId, limit],\n\t\t);\n\t}\n\n\tclaimForRetry(limit = 10): Promise<IPendingRetry[]> {\n\t\treturn this.store.query<IPendingRetry>(\n\t\t\t`SELECT ${D_COLS},\n\t\t\t e.url, e.secret\n\t\t\t FROM fonderie_webhook_deliveries d\n\t\t\t JOIN fonderie_webhook_endpoints e ON e.id = d.endpoint_id\n\t\t\t WHERE d.status = 'failed'\n\t\t\t AND d.next_attempt_at IS NOT NULL\n\t\t\t AND d.next_attempt_at <= now()\n\t\t\t AND e.enabled = true\n\t\t\t ORDER BY d.next_attempt_at\n\t\t\t LIMIT $1`,\n\t\t\t[limit],\n\t\t);\n\t}\n}\n","import { createHmac, randomBytes } from 'node:crypto';\n\nexport function generateSecret(): string {\n\treturn randomBytes(32).toString('hex');\n}\n\nexport function signPayload(secret: string, body: string): string {\n\treturn `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IEventMeta } from '@fonderie/events';\n\nimport type { IWebhooksConfig } from './config';\nimport type { IWebhookEndpoint, IWebhookDelivery } from './types';\nimport { EndpointModel } from './models/endpoint.model';\nimport { DeliveryModel } from './models/delivery.model';\nimport { signPayload } from './signing';\n\nexport class WebhookDispatcher {\n\tprivate readonly maxAttempts: number;\n\tprivate readonly retryDelays: number[];\n\n\tconstructor(\n\t\tprivate readonly store: IStoreAdapter,\n\t\tprivate readonly config: IWebhooksConfig = {},\n\t) {\n\t\tthis.maxAttempts = config.maxAttempts ?? 3;\n\t\tthis.retryDelays = config.retryDelays ?? [60_000, 300_000, 1_800_000];\n\t}\n\n\t// Called by the bus consumer for every event.\n\t// Skips events that don't carry a workspaceId — not workspace-scoped.\n\tasync dispatch(payload: Record<string, unknown>, meta: IEventMeta): Promise<void> {\n\t\tconst workspaceId = payload['workspaceId'];\n\t\tif (typeof workspaceId !== 'string') return;\n\n\t\tconst endpoints = await new EndpointModel(this.store).findForEvent(workspaceId, meta.type);\n\t\tif (endpoints.length === 0) return;\n\n\t\tconst deliveries = new DeliveryModel(this.store);\n\t\tawait Promise.allSettled(endpoints.map((ep) => this.deliver(ep, payload, meta, deliveries)));\n\t}\n\n\t// Retries failed deliveries whose next_attempt_at has passed.\n\tasync retry(): Promise<void> {\n\t\tconst deliveries = new DeliveryModel(this.store);\n\t\tconst pending = await deliveries.claimForRetry();\n\t\tawait Promise.allSettled(\n\t\t\tpending.map(({ delivery, url, secret }) =>\n\t\t\t\tthis.attemptDelivery(url, secret, delivery, deliveries),\n\t\t\t),\n\t\t);\n\t}\n\n\tprivate async deliver(\n\t\tendpoint: IWebhookEndpoint,\n\t\tpayload: Record<string, unknown>,\n\t\tmeta: IEventMeta,\n\t\tdeliveries: DeliveryModel,\n\t): Promise<void> {\n\t\tconst delivery = await deliveries.create({\n\t\t\tendpointId: endpoint.id,\n\t\t\teventId: meta.id,\n\t\t\teventType: meta.type,\n\t\t\tpayload,\n\t\t});\n\t\tawait this.attemptDelivery(endpoint.url, endpoint.secret, delivery, deliveries);\n\t}\n\n\tasync attemptDelivery(\n\t\turl: string,\n\t\tsecret: string,\n\t\tdelivery: IWebhookDelivery,\n\t\tdeliveries: DeliveryModel,\n\t): Promise<void> {\n\t\tconst body = JSON.stringify({\n\t\t\tid: delivery.eventId,\n\t\t\ttype: delivery.eventType,\n\t\t\tdata: delivery.payload,\n\t\t});\n\n\t\tconst signature = signPayload(secret, body);\n\n\t\ttry {\n\t\t\tconst res = await fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t'X-Webhook-Signature': signature,\n\t\t\t\t\t'X-Webhook-Event': delivery.eventType,\n\t\t\t\t\t'X-Webhook-ID': delivery.id,\n\t\t\t\t},\n\t\t\t\tbody,\n\t\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t\t});\n\n\t\t\tconst responseBody = await res.text().catch(() => '');\n\n\t\t\tawait deliveries.markResult(delivery.id, {\n\t\t\t\tok: res.ok,\n\t\t\t\tresponseStatus: res.status,\n\t\t\t\tresponseBody,\n\t\t\t\tnextAttemptAt: res.ok ? null : this.nextRetryAt(delivery.attempts),\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tawait deliveries.markResult(delivery.id, {\n\t\t\t\tok: false,\n\t\t\t\tresponseStatus: null,\n\t\t\t\tresponseBody: err instanceof Error ? err.message : String(err),\n\t\t\t\tnextAttemptAt: this.nextRetryAt(delivery.attempts),\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate nextRetryAt(currentAttempts: number): Date | null {\n\t\tif (currentAttempts + 1 >= this.maxAttempts) return null;\n\t\tconst delay =\n\t\t\tthis.retryDelays[currentAttempts] ?? this.retryDelays[this.retryDelays.length - 1]!;\n\t\treturn new Date(Date.now() + delay);\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { EndpointModel } from './models/endpoint.model';\nimport { DeliveryModel } from './models/delivery.model';\nimport { WebhookDispatcher } from './dispatcher';\nimport { generateSecret, signPayload } from './signing';\nimport { toEndpointDTO, toEndpointCreatedDTO, toDeliveryDTO } from './dtos/webhook';\nimport type { IWebhooksConfig } from './config';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildWebhookRoutes(store: IStoreAdapter, config: IWebhooksConfig = {}): Route[] {\n\treturn [\n\t\t[\n\t\t\t'POST',\n\t\t\t'/webhooks',\n\t\t\trequireAuth,\n\t\t\twithBody,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst body = ctx.meta['body'] as { url?: string; events?: string[] } | undefined;\n\t\t\t\tif (!body?.url)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'MISSING_FIELD', 'url is required');\n\n\t\t\t\tconst endpoint = await new EndpointModel(store).create({\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\turl: body.url,\n\t\t\t\t\tsecret: generateSecret(),\n\t\t\t\t\tevents: body.events ?? [],\n\t\t\t\t});\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CREATED,\n\t\t\t\t\t'WEBHOOK_CREATED',\n\t\t\t\t\t'Webhook endpoint registered.',\n\t\t\t\t\ttoEndpointCreatedDTO(endpoint),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst list = await new EndpointModel(store).list(ctx.workspace.id);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'WEBHOOKS_FETCHED', 'Webhook endpoints retrieved.', {\n\t\t\t\t\tendpoints: list.map(toEndpointDTO),\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'WEBHOOK_FETCHED',\n\t\t\t\t\t'Webhook endpoint retrieved.',\n\t\t\t\t\ttoEndpointDTO(endpoint),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'PATCH',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\twithBody,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst body = ctx.meta['body'] as\n\t\t\t\t\t| { url?: string; events?: string[]; enabled?: boolean }\n\t\t\t\t\t| undefined;\n\n\t\t\t\tconst patch: { url?: string; events?: string[]; enabled?: boolean } = {};\n\t\t\t\tif (body?.url !== undefined) patch.url = body.url;\n\t\t\t\tif (body?.events !== undefined) patch.events = body.events;\n\t\t\t\tif (body?.enabled !== undefined) patch.enabled = body.enabled;\n\n\t\t\t\tconst updated = await new EndpointModel(store).update(endpointId, ctx.workspace.id, patch);\n\t\t\t\tif (!updated)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'WEBHOOK_UPDATED',\n\t\t\t\t\t'Webhook endpoint updated.',\n\t\t\t\t\ttoEndpointDTO(updated),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst deleted = await new EndpointModel(store).delete(endpointId, ctx.workspace.id);\n\t\t\t\tif (!deleted)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn new Response(null, { status: HTTP.NO_CONTENT });\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks/:endpointId/deliveries',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\tconst list = await new DeliveryModel(store).listByEndpoint(endpointId);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'DELIVERIES_FETCHED', 'Deliveries retrieved.', {\n\t\t\t\t\tdeliveries: list.map(toDeliveryDTO),\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'POST',\n\t\t\t'/webhooks/:endpointId/test',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\tconst body = JSON.stringify({\n\t\t\t\t\tid: `test-${Date.now()}`,\n\t\t\t\t\ttype: 'webhook.test',\n\t\t\t\t\tdata: { workspaceId: ctx.workspace.id, message: 'Test webhook delivery.' },\n\t\t\t\t});\n\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(endpoint.url, {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t'X-Webhook-Signature': signPayload(endpoint.secret, body),\n\t\t\t\t\t\t\t'X-Webhook-Event': 'webhook.test',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t\t\t\t});\n\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', {\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tok: res.ok,\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', {\n\t\t\t\t\t\tstatus: null,\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IWebhookEndpoint, IWebhookDelivery } from '../types';\n\nexport interface IWebhookEndpointDTO {\n\tid: string;\n\turl: string;\n\tevents: string[];\n\tenabled: boolean;\n\tcreatedAt: string;\n}\n\nexport interface IWebhookEndpointCreatedDTO extends IWebhookEndpointDTO {\n\tsecret: string;\n}\n\nexport interface IWebhookDeliveryDTO {\n\tid: string;\n\teventId: string;\n\teventType: string;\n\tstatus: string;\n\tattempts: number;\n\tresponseStatus: number | null;\n\tdeliveredAt: string | null;\n\tcreatedAt: string;\n}\n\nexport function toEndpointDTO(e: IWebhookEndpoint): IWebhookEndpointDTO {\n\treturn {\n\t\tid: e.id,\n\t\turl: e.url,\n\t\tevents: e.events,\n\t\tenabled: e.enabled,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function toEndpointCreatedDTO(e: IWebhookEndpoint): IWebhookEndpointCreatedDTO {\n\treturn { ...toEndpointDTO(e), secret: e.secret };\n}\n\nexport function toDeliveryDTO(d: IWebhookDelivery): IWebhookDeliveryDTO {\n\treturn {\n\t\tid: d.id,\n\t\teventId: d.eventId,\n\t\teventType: d.eventType,\n\t\tstatus: d.status,\n\t\tattempts: d.attempts,\n\t\tresponseStatus: d.responseStatus,\n\t\tdeliveredAt: d.deliveredAt?.toISOString() ?? null,\n\t\tcreatedAt: d.createdAt.toISOString(),\n\t};\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\n\nimport type { IWebhooksConfig } from './config';\nimport { WebhookDispatcher } from './dispatcher';\nimport { buildWebhookRoutes } from './routes';\n\nexport class WebhooksModule implements IFonderieModule {\n\treadonly name = '@fonderie/webhooks';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tprivate retryTimer?: ReturnType<typeof setInterval>;\n\n\tconstructor(\n\t\tprivate readonly store: IStoreAdapter,\n\t\tprivate readonly config: IWebhooksConfig = {},\n\t\tprivate readonly bus?: EventBus,\n\t) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst dispatcher = new WebhookDispatcher(this.store, this.config);\n\n\t\tthis.bus?.on<Record<string, unknown>>(\n\t\t\t'*',\n\t\t\tasync (payload, meta) => {\n\t\t\t\tawait dispatcher.dispatch(payload, meta);\n\t\t\t},\n\t\t\t'webhooks',\n\t\t);\n\n\t\tconst interval = this.config.retryInterval ?? 60_000;\n\t\tthis.retryTimer = setInterval(() => {\n\t\t\tdispatcher.retry().catch((err) => console.error('[webhooks] retry error:', err));\n\t\t}, interval);\n\n\t\tconst routes = buildWebhookRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["COLS","import_middlewares"]}