@fonderie/webhooks 1.1.0 → 1.1.2
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/brain/outcomes.md +53 -0
- package/brain/signatures.md +72 -0
- package/dist/migrations/sql/001_webhooks.sql +37 -0
- package/package.json +5 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/webhooks — outcomes
|
|
4
|
+
|
|
5
|
+
What this package does to a running app: tables its migrations create,
|
|
6
|
+
rows it seeds, routes it registers. Generated from the migration SQL and
|
|
7
|
+
route tables in source — trust this file instead of reading `dist/` or
|
|
8
|
+
downloading tarballs.
|
|
9
|
+
|
|
10
|
+
## Database tables (after all migrations)
|
|
11
|
+
|
|
12
|
+
### `fonderie_webhook_deliveries`
|
|
13
|
+
|
|
14
|
+
```sql
|
|
15
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
16
|
+
endpoint_id UUID NOT NULL REFERENCES fonderie_webhook_endpoints(id) ON DELETE CASCADE
|
|
17
|
+
event_id TEXT NOT NULL
|
|
18
|
+
event_type TEXT NOT NULL
|
|
19
|
+
payload JSONB NOT NULL
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
21
|
+
attempts INT NOT NULL DEFAULT 0
|
|
22
|
+
response_status INT
|
|
23
|
+
response_body TEXT
|
|
24
|
+
next_attempt_at TIMESTAMPTZ
|
|
25
|
+
delivered_at TIMESTAMPTZ
|
|
26
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `fonderie_webhook_endpoints`
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
33
|
+
workspace_id UUID NOT NULL
|
|
34
|
+
url TEXT NOT NULL
|
|
35
|
+
secret TEXT NOT NULL
|
|
36
|
+
events TEXT[] NOT NULL DEFAULT '{}'
|
|
37
|
+
enabled BOOLEAN NOT NULL DEFAULT true
|
|
38
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Raw SQL ships in `node_modules/@fonderie/webhooks/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
|
42
|
+
|
|
43
|
+
## HTTP routes registered
|
|
44
|
+
|
|
45
|
+
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| GET | `/webhooks` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const list = await new EndpointModel(store).list(ctx.workspace.id); return setApiResponse(HTTP.OK, 'WEBHOOKS_FETCHED', 'Webhook endpoints retrieved.', { endpoints: list.map(toEndpointDTO), }); }` |
|
|
48
|
+
| POST | `/webhooks` | `requireAuth → validate(createEndpointSchema) → withBody → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const body = ctx.meta['body'] as { url?: string; events?: string[] } | undefined; if (!body?.url) return setApiResponse(HTTP.UNPROCESSABLE, 'MISSING_FIELD', 'url is required'); const endpoint = await new EndpointModel(store).create({ workspaceId: ctx.workspace.id, url: body.url, secret: generateSecret(), events: body.events ?? [], }); return setApiResponse( HTTP.CREATED, 'WEBHOOK_CREATED', 'Webhook endpoint registered.', toEndpointCreatedDTO(endpoint), ); }` |
|
|
49
|
+
| DELETE | `/webhooks/:endpointId` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const { endpointId } = ctx.meta['params'] as { endpointId: string }; const deleted = await new EndpointModel(store).delete(endpointId, ctx.workspace.id); if (!deleted) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found'); return new Response(null, { status: HTTP.NO_CONTENT }); }` |
|
|
50
|
+
| GET | `/webhooks/:endpointId` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const { endpointId } = ctx.meta['params'] as { endpointId: string }; const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id); if (!endpoint) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found'); return setApiResponse( HTTP.OK, 'WEBHOOK_FETCHED', 'Webhook endpoint retrieved.', toEndpointDTO(endpoint), ); }` |
|
|
51
|
+
| PATCH | `/webhooks/:endpointId` | `requireAuth → validate(updateEndpointSchema) → withBody → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const { endpointId } = ctx.meta['params'] as { endpointId: string }; const body = ctx.meta['body'] as | { url?: string; events?: string[]; enabled?: boolean } | undefined; const patch: { url?: string; events?: string[]; enabled?: boolean } = {}; if (body?.url !== undefined) patch.url = body.url; if (body?.events !== undefined) patch.events = body.events; if (body?.enabled !== undefined) patch.enabled = body.enabled; const updated = await new EndpointModel(store).update(endpointId, ctx.workspace.id, patch); if (!updated) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found'); return setApiResponse( HTTP.OK, 'WEBHOOK_UPDATED', 'Webhook endpoint updated.', toEndpointDTO(updated), ); }` |
|
|
52
|
+
| GET | `/webhooks/:endpointId/deliveries` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const { endpointId } = ctx.meta['params'] as { endpointId: string }; const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id); if (!endpoint) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found'); const list = await new DeliveryModel(store).listByEndpoint(endpointId); return setApiResponse(HTTP.OK, 'DELIVERIES_FETCHED', 'Deliveries retrieved.', { deliveries: list.map(toDeliveryDTO), }); }` |
|
|
53
|
+
| POST | `/webhooks/:endpointId/test` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const { endpointId } = ctx.meta['params'] as { endpointId: string }; const endpoint = await new EndpointModel(store).findById(endpointId, ctx.workspace.id); if (!endpoint) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Webhook endpoint not found'); const body = JSON.stringify({ id: `test-${Date.now()}`, type: 'webhook.test', data: { workspaceId: ctx.workspace.id, message: 'Test webhook delivery.' }, }); try { const res = await fetch(endpoint.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signPayload(endpoint.secret, body), 'X-Webhook-Event': 'webhook.test', }, body, signal: AbortSignal.timeout(10_000), }); return setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', { status: res.status, ok: res.ok, }); } catch (err) { return setApiResponse(HTTP.OK, 'TEST_SENT', 'Test delivery attempted.', { status: null, ok: false, error: err instanceof Error ? err.message : String(err), }); } }` |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!-- GENERATED — do not edit. Regenerate with: npm run docs:signatures -->
|
|
2
|
+
|
|
3
|
+
# @fonderie/webhooks — signatures
|
|
4
|
+
|
|
5
|
+
## @fonderie/webhooks
|
|
6
|
+
|
|
7
|
+
Subpath exports: `@fonderie/webhooks/migrations`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
new WebhooksModule(store: IStoreAdapter, config?: IWebhooksConfig, bus?: EventBus | undefined): WebhooksModule
|
|
11
|
+
.name: "@fonderie/webhooks"
|
|
12
|
+
.deps: string[]
|
|
13
|
+
.install(app: IFonderieApp): void
|
|
14
|
+
|
|
15
|
+
interface IWebhooksConfig {
|
|
16
|
+
maxAttempts?: number;
|
|
17
|
+
retryDelays?: number[];
|
|
18
|
+
retryInterval?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface IWebhookEndpoint {
|
|
22
|
+
id: string;
|
|
23
|
+
workspaceId: string;
|
|
24
|
+
url: string;
|
|
25
|
+
secret: string;
|
|
26
|
+
events: string[];
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IWebhookDelivery {
|
|
32
|
+
id: string;
|
|
33
|
+
endpointId: string;
|
|
34
|
+
eventId: string;
|
|
35
|
+
eventType: string;
|
|
36
|
+
payload: Record<string, unknown>;
|
|
37
|
+
status: DeliveryStatus;
|
|
38
|
+
attempts: number;
|
|
39
|
+
responseStatus: number | null;
|
|
40
|
+
responseBody: string | null;
|
|
41
|
+
nextAttemptAt: Date | null;
|
|
42
|
+
deliveredAt: Date | null;
|
|
43
|
+
createdAt: Date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type DeliveryStatus = 'pending' | 'delivered' | 'failed';
|
|
47
|
+
|
|
48
|
+
interface IWebhookEndpointDTO {
|
|
49
|
+
id: string;
|
|
50
|
+
url: string;
|
|
51
|
+
events: string[];
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface IWebhookEndpointCreatedDTO extends IWebhookEndpointDTO {
|
|
57
|
+
secret: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface IWebhookDeliveryDTO {
|
|
61
|
+
id: string;
|
|
62
|
+
eventId: string;
|
|
63
|
+
eventType: string;
|
|
64
|
+
status: string;
|
|
65
|
+
attempts: number;
|
|
66
|
+
responseStatus: number | null;
|
|
67
|
+
deliveredAt: string | null;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
namespace schemas — exports: createEndpointSchema, updateEndpointSchema
|
|
72
|
+
```
|
|
@@ -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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/webhooks",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Outgoing webhook engine — register endpoints, fan out workspace-scoped events, track delivery, and retry on failure.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderie-js",
|
|
@@ -61,17 +61,18 @@
|
|
|
61
61
|
},
|
|
62
62
|
"files": [
|
|
63
63
|
"dist",
|
|
64
|
+
"brain",
|
|
64
65
|
"LICENSE",
|
|
65
66
|
"README.md"
|
|
66
67
|
],
|
|
67
68
|
"repository": {
|
|
68
69
|
"type": "git",
|
|
69
|
-
"url": "git+https://github.com/
|
|
70
|
+
"url": "git+https://github.com/fonderiejs/sdk.git",
|
|
70
71
|
"directory": "packages/webhooks"
|
|
71
72
|
},
|
|
72
|
-
"homepage": "https://github.com/
|
|
73
|
+
"homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/webhooks#readme",
|
|
73
74
|
"bugs": {
|
|
74
|
-
"url": "https://github.com/
|
|
75
|
+
"url": "https://github.com/fonderiejs/sdk/issues"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"zod": "^4.4.3"
|