@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.
Files changed (38) hide show
  1. package/database/config.js +29 -1
  2. package/database/use-cases/test-encryption-use-case.js +6 -5
  3. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  4. package/handlers/WEBHOOKS.md +653 -0
  5. package/handlers/backend-utils.js +118 -3
  6. package/handlers/integration-event-dispatcher.test.js +68 -0
  7. package/handlers/routers/integration-webhook-routers.js +67 -0
  8. package/handlers/routers/integration-webhook-routers.test.js +126 -0
  9. package/handlers/webhook-flow.integration.test.js +356 -0
  10. package/handlers/workers/integration-defined-workers.test.js +184 -0
  11. package/index.js +16 -0
  12. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  13. package/integrations/integration-base.js +74 -3
  14. package/integrations/repositories/process-repository-factory.js +46 -0
  15. package/integrations/repositories/process-repository-interface.js +90 -0
  16. package/integrations/repositories/process-repository-mongo.js +190 -0
  17. package/integrations/repositories/process-repository-postgres.js +217 -0
  18. package/integrations/tests/doubles/dummy-integration-class.js +1 -8
  19. package/integrations/use-cases/create-process.js +128 -0
  20. package/integrations/use-cases/create-process.test.js +178 -0
  21. package/integrations/use-cases/get-process.js +87 -0
  22. package/integrations/use-cases/get-process.test.js +190 -0
  23. package/integrations/use-cases/index.js +8 -0
  24. package/integrations/use-cases/update-process-metrics.js +201 -0
  25. package/integrations/use-cases/update-process-metrics.test.js +308 -0
  26. package/integrations/use-cases/update-process-state.js +119 -0
  27. package/integrations/use-cases/update-process-state.test.js +256 -0
  28. package/package.json +5 -5
  29. package/prisma-mongodb/schema.prisma +44 -0
  30. package/prisma-postgresql/schema.prisma +45 -0
  31. package/queues/queuer-util.js +10 -0
  32. package/user/repositories/user-repository-mongo.js +53 -12
  33. package/user/repositories/user-repository-postgres.js +53 -14
  34. package/user/tests/use-cases/login-user.test.js +85 -5
  35. package/user/tests/user-password-encryption-isolation.test.js +237 -0
  36. package/user/tests/user-password-hashing.test.js +235 -0
  37. package/user/use-cases/login-user.js +1 -1
  38. 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
+