@friggframework/core 2.0.0-next.43 → 2.0.0-next.45
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/database/config.js +29 -1
- package/database/use-cases/test-encryption-use-case.js +6 -5
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/backend-utils.js +118 -3
- package/handlers/integration-event-dispatcher.test.js +68 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/integration-webhook-routers.test.js +126 -0
- package/handlers/webhook-flow.integration.test.js +356 -0
- package/handlers/workers/integration-defined-workers.test.js +184 -0
- package/index.js +16 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/integration-base.js +74 -3
- package/integrations/repositories/process-repository-factory.js +46 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/dummy-integration-class.js +1 -8
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/create-process.test.js +178 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/get-process.test.js +190 -0
- package/integrations/use-cases/index.js +8 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-metrics.test.js +308 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/use-cases/update-process-state.test.js +256 -0
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +44 -0
- package/prisma-postgresql/schema.prisma +45 -0
- package/queues/queuer-util.js +10 -0
- package/user/repositories/user-repository-mongo.js +53 -12
- package/user/repositories/user-repository-postgres.js +53 -14
- package/user/tests/use-cases/login-user.test.js +85 -5
- package/user/tests/user-password-encryption-isolation.test.js +237 -0
- package/user/tests/user-password-hashing.test.js +235 -0
- package/user/use-cases/login-user.js +1 -1
- package/user/user.js +2 -2
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# Webhook Handling in Frigg
|
|
2
|
+
|
|
3
|
+
This document explains how to implement webhook handling for your Frigg integrations using the built-in webhook infrastructure.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Frigg provides a scalable webhook architecture that:
|
|
8
|
+
- **Receives webhooks without database connections** for fast response times
|
|
9
|
+
- **Queues webhooks to SQS** for async processing
|
|
10
|
+
- **Processes webhooks with fully hydrated integrations** (with DB and API modules loaded)
|
|
11
|
+
- **Supports custom signature verification** for security
|
|
12
|
+
- **Throttles database connections** using SQS to handle webhook bursts
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
The webhook flow consists of two stages:
|
|
17
|
+
|
|
18
|
+
### Stage 1: HTTP Webhook Receiver (No DB)
|
|
19
|
+
```
|
|
20
|
+
Webhook → Lambda → WEBHOOK_RECEIVED event → Queue to SQS → 200 OK Response
|
|
21
|
+
```
|
|
22
|
+
- Fast response (no database query)
|
|
23
|
+
- Optional signature verification
|
|
24
|
+
- Messages queued for processing
|
|
25
|
+
|
|
26
|
+
### Stage 2: Queue Worker (DB-Connected)
|
|
27
|
+
```
|
|
28
|
+
SQS Queue → Lambda Worker → ON_WEBHOOK event → Process with hydrated integration
|
|
29
|
+
```
|
|
30
|
+
- Full database access
|
|
31
|
+
- API modules loaded
|
|
32
|
+
- Can use integration context
|
|
33
|
+
|
|
34
|
+
## Enabling Webhooks
|
|
35
|
+
|
|
36
|
+
### Simple Configuration
|
|
37
|
+
|
|
38
|
+
Add `webhooks: true` to your Integration Definition:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
class MyIntegration extends IntegrationBase {
|
|
42
|
+
static Definition = {
|
|
43
|
+
name: 'my-integration',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
modules: {
|
|
46
|
+
myapi: { definition: MyApiDefinition },
|
|
47
|
+
},
|
|
48
|
+
webhooks: true, // Enable webhook handling
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Advanced Configuration
|
|
54
|
+
|
|
55
|
+
For future extensibility, you can use object configuration:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
class MyIntegration extends IntegrationBase {
|
|
59
|
+
static Definition = {
|
|
60
|
+
name: 'my-integration',
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
modules: { /* ... */ },
|
|
63
|
+
webhooks: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
// Future options will be added here
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Webhook Routes
|
|
72
|
+
|
|
73
|
+
When webhooks are enabled, two routes are automatically created:
|
|
74
|
+
|
|
75
|
+
### General Webhook
|
|
76
|
+
```
|
|
77
|
+
POST /api/{integrationName}-integration/webhooks
|
|
78
|
+
```
|
|
79
|
+
- No integration ID required
|
|
80
|
+
- Useful for system-wide events
|
|
81
|
+
- Creates unhydrated integration instance
|
|
82
|
+
|
|
83
|
+
### Integration-Specific Webhook
|
|
84
|
+
```
|
|
85
|
+
POST /api/{integrationName}-integration/webhooks/:integrationId
|
|
86
|
+
```
|
|
87
|
+
- Includes integration ID in URL
|
|
88
|
+
- Worker loads full integration with DB and modules
|
|
89
|
+
- Recommended for most use cases
|
|
90
|
+
|
|
91
|
+
## Event Handlers
|
|
92
|
+
|
|
93
|
+
### WEBHOOK_RECEIVED Event
|
|
94
|
+
|
|
95
|
+
Triggered when a webhook HTTP request is received (no database connection).
|
|
96
|
+
|
|
97
|
+
#### Default Behavior
|
|
98
|
+
Queues the webhook to SQS and responds with `200 OK`:
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// Default handler (automatic)
|
|
102
|
+
async onWebhookReceived({ req, res }) {
|
|
103
|
+
await this.queueWebhook({
|
|
104
|
+
integrationId: req.params.integrationId || null,
|
|
105
|
+
body: req.body,
|
|
106
|
+
headers: req.headers,
|
|
107
|
+
query: req.query,
|
|
108
|
+
});
|
|
109
|
+
res.status(200).json({ received: true });
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Custom Signature Verification
|
|
114
|
+
|
|
115
|
+
Override `onWebhookReceived` for custom signature verification:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
class MyIntegration extends IntegrationBase {
|
|
119
|
+
static Definition = {
|
|
120
|
+
name: 'my-integration',
|
|
121
|
+
webhooks: true,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
async onWebhookReceived({ req, res }) {
|
|
125
|
+
// Verify webhook signature
|
|
126
|
+
const signature = req.headers['x-webhook-signature'];
|
|
127
|
+
const expectedSignature = this.calculateSignature(req.body);
|
|
128
|
+
|
|
129
|
+
if (signature !== expectedSignature) {
|
|
130
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Queue for processing
|
|
134
|
+
await this.queueWebhook({
|
|
135
|
+
integrationId: req.params.integrationId,
|
|
136
|
+
body: req.body,
|
|
137
|
+
headers: req.headers,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
res.status(200).json({ received: true, verified: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
calculateSignature(body) {
|
|
144
|
+
const crypto = require('crypto');
|
|
145
|
+
const secret = process.env.MY_WEBHOOK_SECRET;
|
|
146
|
+
return crypto
|
|
147
|
+
.createHmac('sha256', secret)
|
|
148
|
+
.update(JSON.stringify(body))
|
|
149
|
+
.digest('hex');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### ON_WEBHOOK Event
|
|
155
|
+
|
|
156
|
+
Triggered by the queue worker (with database connection and hydrated integration).
|
|
157
|
+
|
|
158
|
+
#### Default Behavior
|
|
159
|
+
Logs the webhook data (override this!):
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// Default handler (logs only)
|
|
163
|
+
async onWebhook({ data }) {
|
|
164
|
+
console.log('Webhook received:', data);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Custom Processing
|
|
169
|
+
|
|
170
|
+
Override `onWebhook` to process webhooks with full integration context:
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
class MyIntegration extends IntegrationBase {
|
|
174
|
+
static Definition = {
|
|
175
|
+
name: 'my-integration',
|
|
176
|
+
modules: {
|
|
177
|
+
myapi: { definition: MyApiDefinition },
|
|
178
|
+
},
|
|
179
|
+
webhooks: true,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
async onWebhook({ data }) {
|
|
183
|
+
const { body, headers, integrationId } = data;
|
|
184
|
+
|
|
185
|
+
// Access hydrated API modules
|
|
186
|
+
if (body.event === 'item.created') {
|
|
187
|
+
await this.myapi.api.createRecord({
|
|
188
|
+
externalId: body.data.id,
|
|
189
|
+
name: body.data.name,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Access integration config
|
|
194
|
+
const syncEnabled = this.config.syncEnabled;
|
|
195
|
+
if (syncEnabled) {
|
|
196
|
+
await this.performSync(body.data);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Update integration mappings
|
|
200
|
+
await this.upsertMapping(body.data.id, {
|
|
201
|
+
externalId: body.data.id,
|
|
202
|
+
syncedAt: new Date(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return { processed: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async performSync(data) {
|
|
209
|
+
// Custom sync logic
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
### Example 1: Slack Message Events
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
class SlackIntegration extends IntegrationBase {
|
|
220
|
+
static Definition = {
|
|
221
|
+
name: 'slack',
|
|
222
|
+
modules: {
|
|
223
|
+
slack: { definition: SlackApiDefinition },
|
|
224
|
+
},
|
|
225
|
+
webhooks: true,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
async onWebhookReceived({ req, res }) {
|
|
229
|
+
// Slack URL verification challenge
|
|
230
|
+
if (req.body.type === 'url_verification') {
|
|
231
|
+
return res.json({ challenge: req.body.challenge });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Verify Slack signature
|
|
235
|
+
const slackSignature = req.headers['x-slack-signature'];
|
|
236
|
+
if (!this.verifySlackSignature(req, slackSignature)) {
|
|
237
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await this.queueWebhook({
|
|
241
|
+
integrationId: req.params.integrationId,
|
|
242
|
+
body: req.body,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
res.status(200).json({ ok: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async onWebhook({ data }) {
|
|
249
|
+
const { body } = data;
|
|
250
|
+
|
|
251
|
+
if (body.event.type === 'message') {
|
|
252
|
+
// Process message with API access
|
|
253
|
+
await this.slack.api.postMessage({
|
|
254
|
+
channel: body.event.channel,
|
|
255
|
+
text: `Received: ${body.event.text}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
verifySlackSignature(req, signature) {
|
|
261
|
+
// Slack signature verification logic
|
|
262
|
+
const crypto = require('crypto');
|
|
263
|
+
const signingSecret = process.env.SLACK_SIGNING_SECRET;
|
|
264
|
+
const timestamp = req.headers['x-slack-request-timestamp'];
|
|
265
|
+
|
|
266
|
+
// Validate timestamp is recent (within 5 minutes)
|
|
267
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
268
|
+
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
|
|
269
|
+
return false; // Request is older than 5 minutes
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const hmac = crypto.createHmac('sha256', signingSecret);
|
|
273
|
+
hmac.update(`v0:${timestamp}:${JSON.stringify(req.body)}`);
|
|
274
|
+
const expected = `v0=${hmac.digest('hex')}`;
|
|
275
|
+
|
|
276
|
+
// Check lengths first to avoid errors in timingSafeEqual
|
|
277
|
+
const expectedBuffer = Buffer.from(expected)
|
|
278
|
+
const signatureBuffer = Buffer.from(signature)
|
|
279
|
+
|
|
280
|
+
if (expectedBuffer.length !== signatureBuffer.length) {
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return crypto.timingSafeEqual(expectedBuffer, signatureBuffer)
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Example 2: Stripe Webhook Events
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
class StripeIntegration extends IntegrationBase {
|
|
294
|
+
static Definition = {
|
|
295
|
+
name: 'stripe',
|
|
296
|
+
modules: {
|
|
297
|
+
stripe: { definition: StripeApiDefinition },
|
|
298
|
+
},
|
|
299
|
+
webhooks: true,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
async onWebhookReceived({ req, res }) {
|
|
303
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
304
|
+
const sig = req.headers['stripe-signature'];
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Stripe signature verification
|
|
308
|
+
const event = stripe.webhooks.constructEvent(
|
|
309
|
+
JSON.stringify(req.body),
|
|
310
|
+
sig,
|
|
311
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await this.queueWebhook({
|
|
315
|
+
integrationId: req.params.integrationId,
|
|
316
|
+
body: event,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
res.status(200).json({ received: true });
|
|
320
|
+
} catch (err) {
|
|
321
|
+
res.status(400).json({ error: `Webhook Error: ${err.message}` });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async onWebhook({ data }) {
|
|
326
|
+
const event = data.body;
|
|
327
|
+
|
|
328
|
+
switch (event.type) {
|
|
329
|
+
case 'payment_intent.succeeded':
|
|
330
|
+
await this.handlePaymentSuccess(event.data.object);
|
|
331
|
+
break;
|
|
332
|
+
case 'customer.subscription.created':
|
|
333
|
+
await this.handleSubscriptionCreated(event.data.object);
|
|
334
|
+
break;
|
|
335
|
+
default:
|
|
336
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async handlePaymentSuccess(paymentIntent) {
|
|
341
|
+
// Update your database, send notifications, etc.
|
|
342
|
+
await this.stripe.api.updatePaymentRecord(paymentIntent.id, {
|
|
343
|
+
status: 'succeeded',
|
|
344
|
+
amount: paymentIntent.amount,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async handleSubscriptionCreated(subscription) {
|
|
349
|
+
// Process new subscription
|
|
350
|
+
await this.upsertMapping(subscription.id, {
|
|
351
|
+
stripeSubscriptionId: subscription.id,
|
|
352
|
+
status: subscription.status,
|
|
353
|
+
createdAt: new Date(subscription.created * 1000),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Example 3: General Webhook (No Integration ID)
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
class SystemWebhookIntegration extends IntegrationBase {
|
|
363
|
+
static Definition = {
|
|
364
|
+
name: 'system-webhook',
|
|
365
|
+
webhooks: true,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
async onWebhook({ data }) {
|
|
369
|
+
const { body } = data;
|
|
370
|
+
|
|
371
|
+
// Process system-wide webhook without integration context
|
|
372
|
+
console.log('System webhook received:', body);
|
|
373
|
+
|
|
374
|
+
// Could trigger actions across multiple integrations
|
|
375
|
+
// or perform system-level operations
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Environment Variables
|
|
381
|
+
|
|
382
|
+
Webhook functionality requires queue URL environment variables:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Format: {INTEGRATION_NAME}_QUEUE_URL
|
|
386
|
+
SLACK_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789/slack-queue
|
|
387
|
+
STRIPE_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789/stripe-queue
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
These are automatically configured by the Frigg infrastructure when using the serverless template.
|
|
391
|
+
|
|
392
|
+
## Testing Webhooks
|
|
393
|
+
|
|
394
|
+
### Unit Test Example
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
describe('MyIntegration Webhooks', () => {
|
|
398
|
+
it('should verify webhook signature', async () => {
|
|
399
|
+
const integration = new MyIntegration();
|
|
400
|
+
const dispatcher = new IntegrationEventDispatcher(integration);
|
|
401
|
+
|
|
402
|
+
const req = {
|
|
403
|
+
body: { event: 'test' },
|
|
404
|
+
params: {},
|
|
405
|
+
headers: { 'x-webhook-signature': 'valid-sig' },
|
|
406
|
+
query: {},
|
|
407
|
+
};
|
|
408
|
+
const res = {
|
|
409
|
+
status: jest.fn().mockReturnThis(),
|
|
410
|
+
json: jest.fn(),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
await dispatcher.dispatchHttp({
|
|
414
|
+
event: 'WEBHOOK_RECEIVED',
|
|
415
|
+
req,
|
|
416
|
+
res,
|
|
417
|
+
next: jest.fn(),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should process webhook with hydrated instance', async () => {
|
|
424
|
+
const integration = new MyIntegration({
|
|
425
|
+
id: 'int-123',
|
|
426
|
+
userId: 'user-456',
|
|
427
|
+
modules: [],
|
|
428
|
+
});
|
|
429
|
+
const dispatcher = new IntegrationEventDispatcher(integration);
|
|
430
|
+
|
|
431
|
+
const result = await dispatcher.dispatchJob({
|
|
432
|
+
event: 'ON_WEBHOOK',
|
|
433
|
+
data: {
|
|
434
|
+
integrationId: 'int-123',
|
|
435
|
+
body: { event: 'item.created' },
|
|
436
|
+
},
|
|
437
|
+
context: {},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(result.processed).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Best Practices
|
|
446
|
+
|
|
447
|
+
### 1. Always Verify Signatures
|
|
448
|
+
```javascript
|
|
449
|
+
async onWebhookReceived({ req, res }) {
|
|
450
|
+
// Verify before queueing
|
|
451
|
+
if (!this.verifySignature(req)) {
|
|
452
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
453
|
+
}
|
|
454
|
+
await this.queueWebhook({ /* ... */ });
|
|
455
|
+
res.status(200).json({ received: true });
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### 2. Respond Quickly
|
|
460
|
+
The `WEBHOOK_RECEIVED` handler should complete in < 3 seconds:
|
|
461
|
+
- Verify signature
|
|
462
|
+
- Queue message
|
|
463
|
+
- Return 200 OK
|
|
464
|
+
|
|
465
|
+
Heavy processing goes in `ON_WEBHOOK`.
|
|
466
|
+
|
|
467
|
+
### 3. Handle Idempotency
|
|
468
|
+
```javascript
|
|
469
|
+
async onWebhook({ data }) {
|
|
470
|
+
const { body } = data;
|
|
471
|
+
const eventId = body.id;
|
|
472
|
+
|
|
473
|
+
// Check if already processed
|
|
474
|
+
const existing = await this.getMapping(eventId);
|
|
475
|
+
if (existing) {
|
|
476
|
+
console.log(`Event ${eventId} already processed`);
|
|
477
|
+
return { processed: false, duplicate: true };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Process and mark as complete
|
|
481
|
+
await this.processEvent(body);
|
|
482
|
+
await this.upsertMapping(eventId, { processedAt: new Date() });
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 4. Error Handling
|
|
487
|
+
```javascript
|
|
488
|
+
async onWebhook({ data }) {
|
|
489
|
+
try {
|
|
490
|
+
await this.processWebhookData(data.body);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
// Log error - message will go to DLQ after retries
|
|
493
|
+
console.error('Webhook processing failed:', error);
|
|
494
|
+
|
|
495
|
+
// Update integration status if needed
|
|
496
|
+
await this.updateIntegrationMessages.execute(
|
|
497
|
+
this.id,
|
|
498
|
+
'errors',
|
|
499
|
+
'Webhook Processing Error',
|
|
500
|
+
error.message,
|
|
501
|
+
Date.now()
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
throw error; // Re-throw for retry/DLQ
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Infrastructure
|
|
510
|
+
|
|
511
|
+
### Automatic Configuration
|
|
512
|
+
|
|
513
|
+
When `webhooks: true` is set, the Frigg infrastructure automatically creates:
|
|
514
|
+
|
|
515
|
+
1. **HTTP Lambda Function**
|
|
516
|
+
- Handler: `integration-webhook-routers.js`
|
|
517
|
+
- No database connection
|
|
518
|
+
- Fast cold start
|
|
519
|
+
|
|
520
|
+
2. **Webhook Routes**
|
|
521
|
+
- `POST /api/{name}-integration/webhooks`
|
|
522
|
+
- `POST /api/{name}-integration/webhooks/:integrationId`
|
|
523
|
+
|
|
524
|
+
3. **Queue Worker**
|
|
525
|
+
- Processes from existing integration queue
|
|
526
|
+
- Handles `ON_WEBHOOK` events
|
|
527
|
+
- Full database access
|
|
528
|
+
|
|
529
|
+
### Serverless Configuration (Automatic)
|
|
530
|
+
|
|
531
|
+
The following is generated automatically in `serverless.yml`:
|
|
532
|
+
|
|
533
|
+
```yaml
|
|
534
|
+
functions:
|
|
535
|
+
myintegrationWebhook:
|
|
536
|
+
handler: node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.myintegrationWebhook.handler
|
|
537
|
+
events:
|
|
538
|
+
- httpApi:
|
|
539
|
+
path: /api/myintegration-integration/webhooks
|
|
540
|
+
method: POST
|
|
541
|
+
- httpApi:
|
|
542
|
+
path: /api/myintegration-integration/webhooks/{integrationId}
|
|
543
|
+
method: POST
|
|
544
|
+
|
|
545
|
+
myintegrationQueueWorker:
|
|
546
|
+
handler: node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.myintegration.queueWorker
|
|
547
|
+
events:
|
|
548
|
+
- sqs:
|
|
549
|
+
arn: !GetAtt MyintegrationQueue.Arn
|
|
550
|
+
batchSize: 1
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Event Handler Reference
|
|
554
|
+
|
|
555
|
+
### onWebhookReceived({ req, res })
|
|
556
|
+
|
|
557
|
+
**Called:** When webhook HTTP request is received
|
|
558
|
+
**Context:** Unhydrated integration (no DB, no modules loaded)
|
|
559
|
+
**Purpose:** Signature verification, quick response
|
|
560
|
+
**Must:** Respond to `res` with status code
|
|
561
|
+
|
|
562
|
+
**Parameters:**
|
|
563
|
+
- `req` - Express request object
|
|
564
|
+
- `req.body` - Webhook payload
|
|
565
|
+
- `req.params.integrationId` - Integration ID (if in URL)
|
|
566
|
+
- `req.headers` - HTTP headers
|
|
567
|
+
- `req.query` - Query parameters
|
|
568
|
+
- `res` - Express response object
|
|
569
|
+
- Call `res.status(code).json(data)` to respond
|
|
570
|
+
|
|
571
|
+
### onWebhook({ data, context })
|
|
572
|
+
|
|
573
|
+
**Called:** When queue worker processes the webhook
|
|
574
|
+
**Context:** Hydrated integration (DB connected, modules loaded)
|
|
575
|
+
**Purpose:** Process webhook with full integration context
|
|
576
|
+
**Can:** Use `this.modules`, `this.config`, DB operations
|
|
577
|
+
|
|
578
|
+
**Parameters:**
|
|
579
|
+
- `data` - Queued webhook data
|
|
580
|
+
- `data.integrationId` - Integration ID (if provided)
|
|
581
|
+
- `data.body` - Original webhook payload
|
|
582
|
+
- `data.headers` - Original HTTP headers
|
|
583
|
+
- `data.query` - Original query parameters
|
|
584
|
+
- `context` - Lambda context object
|
|
585
|
+
|
|
586
|
+
## Queue Helper
|
|
587
|
+
|
|
588
|
+
### queueWebhook(data)
|
|
589
|
+
|
|
590
|
+
Utility method to queue webhook for processing:
|
|
591
|
+
|
|
592
|
+
```javascript
|
|
593
|
+
await this.queueWebhook({
|
|
594
|
+
integrationId: 'int-123', // optional
|
|
595
|
+
body: webhookPayload,
|
|
596
|
+
headers: requestHeaders,
|
|
597
|
+
query: queryParams,
|
|
598
|
+
customField: 'any additional data',
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Automatically uses the correct SQS queue URL based on integration name.
|
|
603
|
+
|
|
604
|
+
## Troubleshooting
|
|
605
|
+
|
|
606
|
+
### Queue URL Not Found
|
|
607
|
+
|
|
608
|
+
**Error:** `Queue URL not found for {NAME}_QUEUE_URL`
|
|
609
|
+
|
|
610
|
+
**Solution:** Ensure environment variable is set:
|
|
611
|
+
```bash
|
|
612
|
+
export MY_INTEGRATION_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/...
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Webhook Not Responding
|
|
616
|
+
|
|
617
|
+
**Check:**
|
|
618
|
+
1. Is `webhooks: true` in Definition?
|
|
619
|
+
2. Is webhook endpoint deployed?
|
|
620
|
+
3. Are you sending POST requests?
|
|
621
|
+
4. Check CloudWatch logs for errors
|
|
622
|
+
|
|
623
|
+
### Worker Not Processing
|
|
624
|
+
|
|
625
|
+
**Check:**
|
|
626
|
+
1. Is SQS queue receiving messages?
|
|
627
|
+
2. Is queue worker Lambda function deployed?
|
|
628
|
+
3. Check CloudWatch logs for worker errors
|
|
629
|
+
4. Verify integration can be loaded from DB (for ID-specific webhooks)
|
|
630
|
+
|
|
631
|
+
## Security Considerations
|
|
632
|
+
|
|
633
|
+
1. **Always verify signatures** in production
|
|
634
|
+
2. **Use HTTPS** for webhook endpoints
|
|
635
|
+
3. **Validate webhook payloads** before processing
|
|
636
|
+
4. **Rate limit** at API Gateway level if needed
|
|
637
|
+
5. **Monitor** failed webhook processing in DLQ
|
|
638
|
+
|
|
639
|
+
## Performance
|
|
640
|
+
|
|
641
|
+
- **HTTP Response:** < 100ms (signature check + queue)
|
|
642
|
+
- **Worker Processing:** Based on your logic
|
|
643
|
+
- **Concurrency:** Controlled by SQS worker `reservedConcurrency: 5`
|
|
644
|
+
- **Burst Handling:** Unlimited HTTP, throttled processing
|
|
645
|
+
|
|
646
|
+
## Related Files
|
|
647
|
+
|
|
648
|
+
- `packages/core/integrations/integration-base.js` - Event definitions and default handlers
|
|
649
|
+
- `packages/core/handlers/routers/integration-webhook-routers.js` - HTTP webhook routes
|
|
650
|
+
- `packages/core/handlers/backend-utils.js` - Queue worker with hydration logic
|
|
651
|
+
- `packages/core/handlers/integration-event-dispatcher.js` - Event dispatching
|
|
652
|
+
- `packages/devtools/infrastructure/serverless-template.js` - Automatic infrastructure generation
|
|
653
|
+
|