@fonderie/webhooks 4.0.0 → 5.1.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/README.md CHANGED
@@ -29,7 +29,7 @@ import type { IWebhookEndpoint, IWebhookDelivery, DeliveryStatus } from '@fonder
29
29
  You've shipped this plumbing before — auth, teams, billing, messaging —
30
30
  and the next project will ask for it again. Fonderie packages it once:
31
31
  plain TypeScript modules for
32
- [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
32
+ [`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
33
33
  PostgreSQL-backed, self-hosted, MIT. No external control plane, no
34
34
  per-seat anything. Register the modules you need; skip the ones you don't.
35
35
 
@@ -37,7 +37,7 @@ per-seat anything. Register the modules you need; skip the ones you don't.
37
37
  this brick fans workspace events out to them and tracks every delivery.
38
38
 
39
39
  Browse the whole set at
40
- [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
40
+ [fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
41
41
  [@fonderiejs](https://x.com/fonderiejs)
42
42
 
43
43
  ## License
@@ -63,7 +63,10 @@ interface IWebhookDeliveryDTO {
63
63
  eventType: string;
64
64
  status: string;
65
65
  attempts: number;
66
+ payload: Record<string, unknown>;
66
67
  responseStatus: number | null;
68
+ responseBody: string | null;
69
+ nextAttemptAt: string | null;
67
70
  deliveredAt: string | null;
68
71
  createdAt: string;
69
72
  }
package/dist/index.cjs CHANGED
@@ -208,7 +208,7 @@ var WebhookDispatcher = class {
208
208
  const pending = await deliveries.claimForRetry();
209
209
  await Promise.allSettled(
210
210
  pending.map(
211
- ({ delivery, url: url2, secret }) => this.attemptDelivery(url2, secret, delivery, deliveries)
211
+ ({ url: url2, secret, ...delivery }) => this.attemptDelivery(url2, secret, delivery, deliveries)
212
212
  )
213
213
  );
214
214
  }
@@ -307,7 +307,10 @@ function toDeliveryDTO(d) {
307
307
  eventType: d.eventType,
308
308
  status: d.status,
309
309
  attempts: d.attempts,
310
+ payload: d.payload ?? {},
310
311
  responseStatus: d.responseStatus,
312
+ responseBody: d.responseBody ?? null,
313
+ nextAttemptAt: d.nextAttemptAt?.toISOString() ?? null,
311
314
  deliveredAt: d.deliveredAt?.toISOString() ?? null,
312
315
  createdAt: d.createdAt.toISOString()
313
316
  };
@@ -1 +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/schemas.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\n// Request validation — enforced contract for body-taking routes; exported\n// for docs generation and typed clients.\nexport * as schemas from './schemas';\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, validate } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\n\nimport { createEndpointSchema, updateEndpointSchema } from './schemas';\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\tvalidate(createEndpointSchema),\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\tvalidate(updateEndpointSchema),\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 { z } from 'zod';\n\n// Request schemas — the validation contract for webhooks' body-taking routes.\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst url = z.string().trim().pipe(z.url());\nconst events = z.array(z.string().min(1).max(200)).max(100);\n\nexport const createEndpointSchema = z.object({\n\turl,\n\tevents: events.optional(),\n});\n\nexport const updateEndpointSchema = z\n\t.object({\n\t\turl: url.optional(),\n\t\tevents: events.optional(),\n\t\tenabled: z.boolean().optional(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\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","url","import_middlewares"]}
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/schemas.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\n// Request validation — enforced contract for body-taking routes; exported\n// for docs generation and typed clients.\nexport * as schemas from './schemas';\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\n// A failed delivery joined with its endpoint's url/secret — FLAT, exactly as\n// the claim query's row comes back. (This was once declared as a nested\n// { delivery, url, secret } shape that no SQL row ever produced; the retry\n// loop then threw on `delivery.eventId` for every claimed row, so failed\n// deliveries were re-claimed forever and never actually retried.)\nexport type IPendingRetry = IWebhookDelivery & { url: string; secret: string };\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(({ url, secret, ...delivery }) =>\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, validate } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\n\nimport { createEndpointSchema, updateEndpointSchema } from './schemas';\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\tvalidate(createEndpointSchema),\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\tvalidate(updateEndpointSchema),\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 { z } from 'zod';\n\n// Request schemas — the validation contract for webhooks' body-taking routes.\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst url = z.string().trim().pipe(z.url());\nconst events = z.array(z.string().min(1).max(200)).max(100);\n\nexport const createEndpointSchema = z.object({\n\turl,\n\tevents: events.optional(),\n});\n\nexport const updateEndpointSchema = z\n\t.object({\n\t\turl: url.optional(),\n\t\tevents: events.optional(),\n\t\tenabled: z.boolean().optional(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\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\t// The event body that was delivered — what the endpoint received.\n\tpayload: Record<string, unknown>;\n\tresponseStatus: number | null;\n\t// The receiving endpoint's response body (useful when debugging failures).\n\tresponseBody: string | null;\n\t// When the next retry is due; null once delivered or exhausted.\n\tnextAttemptAt: string | 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\tpayload: d.payload ?? {},\n\t\tresponseStatus: d.responseStatus,\n\t\tresponseBody: d.responseBody ?? null,\n\t\tnextAttemptAt: d.nextAttemptAt?.toISOString() ?? null,\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","url","import_middlewares"]}
package/dist/index.d.cts CHANGED
@@ -61,7 +61,10 @@ interface IWebhookDeliveryDTO {
61
61
  eventType: string;
62
62
  status: string;
63
63
  attempts: number;
64
+ payload: Record<string, unknown>;
64
65
  responseStatus: number | null;
66
+ responseBody: string | null;
67
+ nextAttemptAt: string | null;
65
68
  deliveredAt: string | null;
66
69
  createdAt: string;
67
70
  }
package/dist/index.d.ts CHANGED
@@ -61,7 +61,10 @@ interface IWebhookDeliveryDTO {
61
61
  eventType: string;
62
62
  status: string;
63
63
  attempts: number;
64
+ payload: Record<string, unknown>;
64
65
  responseStatus: number | null;
66
+ responseBody: string | null;
67
+ nextAttemptAt: string | null;
65
68
  deliveredAt: string | null;
66
69
  createdAt: string;
67
70
  }
package/dist/index.js CHANGED
@@ -187,7 +187,7 @@ var WebhookDispatcher = class {
187
187
  const pending = await deliveries.claimForRetry();
188
188
  await Promise.allSettled(
189
189
  pending.map(
190
- ({ delivery, url: url2, secret }) => this.attemptDelivery(url2, secret, delivery, deliveries)
190
+ ({ url: url2, secret, ...delivery }) => this.attemptDelivery(url2, secret, delivery, deliveries)
191
191
  )
192
192
  );
193
193
  }
@@ -286,7 +286,10 @@ function toDeliveryDTO(d) {
286
286
  eventType: d.eventType,
287
287
  status: d.status,
288
288
  attempts: d.attempts,
289
+ payload: d.payload ?? {},
289
290
  responseStatus: d.responseStatus,
291
+ responseBody: d.responseBody ?? null,
292
+ nextAttemptAt: d.nextAttemptAt?.toISOString() ?? null,
290
293
  deliveredAt: d.deliveredAt?.toISOString() ?? null,
291
294
  createdAt: d.createdAt.toISOString()
292
295
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/models/endpoint.model.ts","../src/models/delivery.model.ts","../src/signing.ts","../src/dispatcher.ts","../src/routes.ts","../src/schemas.ts","../src/dtos/webhook.ts","../src/module.ts"],"sourcesContent":["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, validate } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\n\nimport { createEndpointSchema, updateEndpointSchema } from './schemas';\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\tvalidate(createEndpointSchema),\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\tvalidate(updateEndpointSchema),\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 { z } from 'zod';\n\n// Request schemas — the validation contract for webhooks' body-taking routes.\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst url = z.string().trim().pipe(z.url());\nconst events = z.array(z.string().min(1).max(200)).max(100);\n\nexport const createEndpointSchema = z.object({\n\turl,\n\tevents: events.optional(),\n});\n\nexport const updateEndpointSchema = z\n\t.object({\n\t\turl: url.optional(),\n\t\tevents: events.optional(),\n\t\tenabled: z.boolean().optional(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\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","url"]}
1
+ {"version":3,"sources":["../src/models/endpoint.model.ts","../src/models/delivery.model.ts","../src/signing.ts","../src/dispatcher.ts","../src/routes.ts","../src/schemas.ts","../src/dtos/webhook.ts","../src/module.ts"],"sourcesContent":["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\n// A failed delivery joined with its endpoint's url/secret — FLAT, exactly as\n// the claim query's row comes back. (This was once declared as a nested\n// { delivery, url, secret } shape that no SQL row ever produced; the retry\n// loop then threw on `delivery.eventId` for every claimed row, so failed\n// deliveries were re-claimed forever and never actually retried.)\nexport type IPendingRetry = IWebhookDelivery & { url: string; secret: string };\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(({ url, secret, ...delivery }) =>\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, validate } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\n\nimport { createEndpointSchema, updateEndpointSchema } from './schemas';\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\tvalidate(createEndpointSchema),\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\tvalidate(updateEndpointSchema),\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 { z } from 'zod';\n\n// Request schemas — the validation contract for webhooks' body-taking routes.\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst url = z.string().trim().pipe(z.url());\nconst events = z.array(z.string().min(1).max(200)).max(100);\n\nexport const createEndpointSchema = z.object({\n\turl,\n\tevents: events.optional(),\n});\n\nexport const updateEndpointSchema = z\n\t.object({\n\t\turl: url.optional(),\n\t\tevents: events.optional(),\n\t\tenabled: z.boolean().optional(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\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\t// The event body that was delivered — what the endpoint received.\n\tpayload: Record<string, unknown>;\n\tresponseStatus: number | null;\n\t// The receiving endpoint's response body (useful when debugging failures).\n\tresponseBody: string | null;\n\t// When the next retry is due; null once delivered or exhausted.\n\tnextAttemptAt: string | 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\tpayload: d.payload ?? {},\n\t\tresponseStatus: d.responseStatus,\n\t\tresponseBody: d.responseBody ?? null,\n\t\tnextAttemptAt: d.nextAttemptAt?.toISOString() ?? null,\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","url"]}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@fonderie/webhooks",
3
- "version": "4.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Outgoing webhook engine — register endpoints, fan out workspace-scoped events, track delivery, and retry on failure.",
5
5
  "keywords": [
6
- "fonderie-js",
6
+ "fonderiejs",
7
7
  "webhooks",
8
8
  "events",
9
9
  "saas",
@@ -38,8 +38,8 @@
38
38
  "check": "biome check --write src"
39
39
  },
40
40
  "peerDependencies": {
41
- "@fonderie/core": "^0.4.0",
42
- "@fonderie/events": "^4.0.0",
41
+ "@fonderie/core": "^0.5.0",
42
+ "@fonderie/events": "^5.0.0",
43
43
  "@fonderie/store": "^0.2.0"
44
44
  },
45
45
  "peerDependenciesMeta": {
@@ -51,9 +51,9 @@
51
51
  "@fonderie/core": "../core",
52
52
  "@fonderie/events": "../events",
53
53
  "@fonderie/store": "../store",
54
- "@types/node": "^25.6.0",
54
+ "@types/node": "^26.4.0",
55
55
  "tsup": "^8.5.1",
56
- "tsx": "^4.21.0",
56
+ "tsx": "^4.23.12",
57
57
  "typescript": "^6.0.3"
58
58
  },
59
59
  "publishConfig": {
@@ -67,14 +67,14 @@
67
67
  ],
68
68
  "repository": {
69
69
  "type": "git",
70
- "url": "git+https://github.com/fonderiejs/sdk.git",
70
+ "url": "git+https://github.com/fonderiejs/fonderie.git",
71
71
  "directory": "packages/webhooks"
72
72
  },
73
- "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/webhooks#readme",
73
+ "homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/webhooks#readme",
74
74
  "bugs": {
75
- "url": "https://github.com/fonderiejs/sdk/issues"
75
+ "url": "https://github.com/fonderiejs/fonderie/issues"
76
76
  },
77
77
  "dependencies": {
78
- "zod": "^4.4.3"
78
+ "zod": "^4.5.1"
79
79
  }
80
80
  }