@fonderie/webhooks 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/index.cjs +519 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +492 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_webhooks.sql +37 -0
- package/package.json +76 -0
|
@@ -0,0 +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/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 } from '@fonderie/core/middlewares';\nimport { withBody } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { EndpointModel } from './models/endpoint.model';\nimport { DeliveryModel } from './models/delivery.model';\nimport { WebhookDispatcher } from './dispatcher';\nimport { generateSecret, signPayload } from './signing';\nimport { toEndpointDTO, toEndpointCreatedDTO, toDeliveryDTO } from './dtos/webhook';\nimport type { IWebhooksConfig } from './config';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildWebhookRoutes(store: IStoreAdapter, config: IWebhooksConfig = {}): Route[] {\n\treturn [\n\t\t[\n\t\t\t'POST',\n\t\t\t'/webhooks',\n\t\t\trequireAuth,\n\t\t\twithBody,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst body = ctx.meta['body'] as { url?: string; events?: string[] } | undefined;\n\t\t\t\tif (!body?.url)\n\t\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'MISSING_FIELD', 'url is required');\n\n\t\t\t\tconst endpoint = await new EndpointModel(store).create({\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\turl: body.url,\n\t\t\t\t\tsecret: generateSecret(),\n\t\t\t\t\tevents: body.events ?? [],\n\t\t\t\t});\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CREATED,\n\t\t\t\t\t'WEBHOOK_CREATED',\n\t\t\t\t\t'Webhook endpoint registered.',\n\t\t\t\t\ttoEndpointCreatedDTO(endpoint),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst list = await new EndpointModel(store).list(ctx.workspace.id);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'WEBHOOKS_FETCHED', 'Webhook endpoints retrieved.', {\n\t\t\t\t\tendpoints: list.map(toEndpointDTO),\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'WEBHOOK_FETCHED',\n\t\t\t\t\t'Webhook endpoint retrieved.',\n\t\t\t\t\ttoEndpointDTO(endpoint),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'PATCH',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\twithBody,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst body = ctx.meta['body'] as\n\t\t\t\t\t| { url?: string; events?: string[]; enabled?: boolean }\n\t\t\t\t\t| undefined;\n\n\t\t\t\tconst patch: { url?: string; events?: string[]; enabled?: boolean } = {};\n\t\t\t\tif (body?.url !== undefined) patch.url = body.url;\n\t\t\t\tif (body?.events !== undefined) patch.events = body.events;\n\t\t\t\tif (body?.enabled !== undefined) patch.enabled = body.enabled;\n\n\t\t\t\tconst updated = await new EndpointModel(store).update(endpointId, ctx.workspace.id, patch);\n\t\t\t\tif (!updated)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t'WEBHOOK_UPDATED',\n\t\t\t\t\t'Webhook endpoint updated.',\n\t\t\t\t\ttoEndpointDTO(updated),\n\t\t\t\t);\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'DELETE',\n\t\t\t'/webhooks/:endpointId',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst deleted = await new EndpointModel(store).delete(endpointId, ctx.workspace.id);\n\t\t\t\tif (!deleted)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\treturn new Response(null, { status: HTTP.NO_CONTENT });\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'GET',\n\t\t\t'/webhooks/:endpointId/deliveries',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\tconst list = await new DeliveryModel(store).listByEndpoint(endpointId);\n\t\t\t\treturn setApiResponse(HTTP.OK, 'DELIVERIES_FETCHED', 'Deliveries retrieved.', {\n\t\t\t\t\tdeliveries: list.map(toDeliveryDTO),\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\n\t\t[\n\t\t\t'POST',\n\t\t\t'/webhooks/:endpointId/test',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst { endpointId } = ctx.meta['params'] as { endpointId: string };\n\t\t\t\tconst endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id);\n\t\t\t\tif (!endpoint)\n\t\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found');\n\n\t\t\t\tconst body = JSON.stringify({\n\t\t\t\t\tid: `test-${Date.now()}`,\n\t\t\t\t\ttype: 'webhook.test',\n\t\t\t\t\tdata: { workspaceId: ctx.workspace.id, message: 'Test webhook delivery.' },\n\t\t\t\t});\n\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(endpoint.url, {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t'X-Webhook-Signature': signPayload(endpoint.secret, body),\n\t\t\t\t\t\t\t'X-Webhook-Event': 'webhook.test',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: AbortSignal.timeout(10_000),\n\t\t\t\t\t});\n\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', {\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tok: res.ok,\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', {\n\t\t\t\t\t\tstatus: null,\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IWebhookEndpoint, IWebhookDelivery } from '../types';\n\nexport interface IWebhookEndpointDTO {\n\tid: string;\n\turl: string;\n\tevents: string[];\n\tenabled: boolean;\n\tcreatedAt: string;\n}\n\nexport interface IWebhookEndpointCreatedDTO extends IWebhookEndpointDTO {\n\tsecret: string;\n}\n\nexport interface IWebhookDeliveryDTO {\n\tid: string;\n\teventId: string;\n\teventType: string;\n\tstatus: string;\n\tattempts: number;\n\tresponseStatus: number | null;\n\tdeliveredAt: string | null;\n\tcreatedAt: string;\n}\n\nexport function toEndpointDTO(e: IWebhookEndpoint): IWebhookEndpointDTO {\n\treturn {\n\t\tid: e.id,\n\t\turl: e.url,\n\t\tevents: e.events,\n\t\tenabled: e.enabled,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function toEndpointCreatedDTO(e: IWebhookEndpoint): IWebhookEndpointCreatedDTO {\n\treturn { ...toEndpointDTO(e), secret: e.secret };\n}\n\nexport function toDeliveryDTO(d: IWebhookDelivery): IWebhookDeliveryDTO {\n\treturn {\n\t\tid: d.id,\n\t\teventId: d.eventId,\n\t\teventType: d.eventType,\n\t\tstatus: d.status,\n\t\tattempts: d.attempts,\n\t\tresponseStatus: d.responseStatus,\n\t\tdeliveredAt: d.deliveredAt?.toISOString() ?? null,\n\t\tcreatedAt: d.createdAt.toISOString(),\n\t};\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\n\nimport type { IWebhooksConfig } from './config';\nimport { WebhookDispatcher } from './dispatcher';\nimport { buildWebhookRoutes } from './routes';\n\nexport class WebhooksModule implements IFonderieModule {\n\treadonly name = '@fonderie/webhooks';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tprivate retryTimer?: ReturnType<typeof setInterval>;\n\n\tconstructor(\n\t\tprivate readonly store: IStoreAdapter,\n\t\tprivate readonly config: IWebhooksConfig = {},\n\t\tprivate readonly bus?: EventBus,\n\t) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst dispatcher = new WebhookDispatcher(this.store, this.config);\n\n\t\tthis.bus?.on<Record<string, unknown>>(\n\t\t\t'*',\n\t\t\tasync (payload, meta) => {\n\t\t\t\tawait dispatcher.dispatch(payload, meta);\n\t\t\t},\n\t\t\t'webhooks',\n\t\t);\n\n\t\tconst interval = this.config.retryInterval ?? 60_000;\n\t\tthis.retryTimer = setInterval(() => {\n\t\t\tdispatcher.retry().catch((err) => console.error('[webhooks] retry error:', err));\n\t\t}, interval);\n\n\t\tconst routes = buildWebhookRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["COLS"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- fonderie_webhook_endpoints
|
|
2
|
+
CREATE TABLE IF NOT EXISTS fonderie_webhook_endpoints (
|
|
3
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
workspace_id UUID NOT NULL,
|
|
5
|
+
url TEXT NOT NULL,
|
|
6
|
+
secret TEXT NOT NULL,
|
|
7
|
+
events TEXT[] NOT NULL DEFAULT '{}',
|
|
8
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
9
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_fwhe_workspace
|
|
13
|
+
ON fonderie_webhook_endpoints (workspace_id)
|
|
14
|
+
WHERE enabled = true;
|
|
15
|
+
|
|
16
|
+
-- fonderie_webhook_deliveries
|
|
17
|
+
CREATE TABLE IF NOT EXISTS fonderie_webhook_deliveries (
|
|
18
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
19
|
+
endpoint_id UUID NOT NULL REFERENCES fonderie_webhook_endpoints(id) ON DELETE CASCADE,
|
|
20
|
+
event_id TEXT NOT NULL,
|
|
21
|
+
event_type TEXT NOT NULL,
|
|
22
|
+
payload JSONB NOT NULL,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
24
|
+
attempts INT NOT NULL DEFAULT 0,
|
|
25
|
+
response_status INT,
|
|
26
|
+
response_body TEXT,
|
|
27
|
+
next_attempt_at TIMESTAMPTZ,
|
|
28
|
+
delivered_at TIMESTAMPTZ,
|
|
29
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_fwhd_endpoint
|
|
33
|
+
ON fonderie_webhook_deliveries (endpoint_id, created_at DESC);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_fwhd_retry
|
|
36
|
+
ON fonderie_webhook_deliveries (next_attempt_at)
|
|
37
|
+
WHERE status = 'failed' AND next_attempt_at IS NOT NULL;
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fonderie/webhooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Outgoing webhook engine — register endpoints, fan out workspace-scoped events, track delivery, and retry on failure.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fonderie-js",
|
|
7
|
+
"webhooks",
|
|
8
|
+
"events",
|
|
9
|
+
"saas",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./migrations": {
|
|
24
|
+
"types": "./dist/migrations/index.d.ts",
|
|
25
|
+
"import": "./dist/migrations/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup && node -e \"require('node:fs').cpSync('src/migrations/sql', 'dist/migrations/sql', {recursive:true})\"",
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
36
|
+
"lint": "biome lint src",
|
|
37
|
+
"format": "biome format --write src",
|
|
38
|
+
"check": "biome check --write src"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@fonderie/core": "^0.1.0",
|
|
42
|
+
"@fonderie/events": "^1.0.0",
|
|
43
|
+
"@fonderie/store": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@fonderie/events": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@fonderie/core": "../core",
|
|
52
|
+
"@fonderie/events": "../events",
|
|
53
|
+
"@fonderie/store": "../store",
|
|
54
|
+
"@types/node": "^25.6.0",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"tsx": "^4.21.0",
|
|
57
|
+
"typescript": "^6.0.3"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"README.md"
|
|
66
|
+
],
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/fonderie-js/sdk.git",
|
|
70
|
+
"directory": "packages/webhooks"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/webhooks#readme",
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/fonderie-js/sdk/issues"
|
|
75
|
+
}
|
|
76
|
+
}
|