@friggframework/devtools 2.0.0--canary.461.9252e46.0 → 2.0.0--canary.461.ae53ecc.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.
@@ -99,10 +99,23 @@ class AuroraBuilder extends InfrastructureBuilder {
99
99
  // Clear granular option to prevent conflicts
100
100
  delete appDefinition.database.postgres.management;
101
101
 
102
- // Set management based on isolation strategy
102
+ // Set management based on isolation strategy AND existing stack resources
103
103
  if (vpcIsolation === 'isolated') {
104
- management = 'managed'; // New VPC = new Aurora
105
- console.log(` managementMode='managed' + vpcIsolation='isolated' creating new Aurora`);
104
+ // Check if CloudFormation stack already has Aurora (stage-specific)
105
+ // Only string endpoints mean resources are from stack (not CloudFormation refs)
106
+ const hasStackAurora = discoveredResources?.auroraEndpoint &&
107
+ typeof discoveredResources.auroraEndpoint === 'string';
108
+
109
+ if (hasStackAurora) {
110
+ // Stack has Aurora - reuse it (standard flow: stack → orphaned → create)
111
+ management = 'discover';
112
+ appDefinition.database.postgres.autoCreateCredentials = true;
113
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has Aurora, reusing`);
114
+ } else {
115
+ // No stack Aurora - create new isolated Aurora for this stage
116
+ management = 'managed';
117
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack Aurora, creating new`);
118
+ }
106
119
  } else {
107
120
  management = 'discover'; // Shared VPC = reuse Aurora
108
121
  appDefinition.database.postgres.autoCreateCredentials = true;
@@ -750,7 +750,53 @@ describe('AuroraBuilder', () => {
750
750
  });
751
751
 
752
752
  describe('Top-Level Management Mode', () => {
753
- it('should use managementMode=managed with vpcIsolation=isolated to create new Aurora', async () => {
753
+ it('should reuse stack Aurora when managementMode=managed + vpcIsolation=isolated AND stack has Aurora', async () => {
754
+ const appDefinition = {
755
+ managementMode: 'managed',
756
+ vpcIsolation: 'isolated',
757
+ database: {
758
+ postgres: {
759
+ enable: true,
760
+ management: 'managed', // Should be IGNORED
761
+ minCapacity: 0.5,
762
+ maxCapacity: 1,
763
+ },
764
+ },
765
+ };
766
+
767
+ // CloudFormation stack has Aurora (from previous deployment of this stage)
768
+ const discoveredResources = {
769
+ auroraEndpoint: 'stack-cluster-dev.us-east-1.rds.amazonaws.com', // String = from stack
770
+ auroraClusterEndpoint: 'stack-cluster-dev.us-east-1.rds.amazonaws.com', // Also set for discover mode
771
+ auroraPort: 5432,
772
+ auroraClusterPort: 5432,
773
+ auroraClusterIdentifier: 'stack-cluster-dev',
774
+ privateSubnetId1: 'subnet-1',
775
+ privateSubnetId2: 'subnet-2',
776
+ };
777
+
778
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
779
+
780
+ const result = await auroraBuilder.build(appDefinition, discoveredResources);
781
+
782
+ // Should warn about ignored options
783
+ expect(consoleLogSpy).toHaveBeenCalledWith(
784
+ expect.stringContaining("managementMode='managed' ignoring")
785
+ );
786
+
787
+ // Should log reusing stack Aurora
788
+ expect(consoleLogSpy).toHaveBeenCalledWith(
789
+ expect.stringContaining("stack has Aurora, reusing")
790
+ );
791
+
792
+ // Should REUSE stack Aurora (not create new)
793
+ expect(result.resources.FriggAuroraCluster).toBeUndefined();
794
+ expect(result.environment.DATABASE_URL).toBeDefined();
795
+
796
+ consoleLogSpy.mockRestore();
797
+ });
798
+
799
+ it('should create new Aurora when managementMode=managed + vpcIsolation=isolated AND stack has NO Aurora', async () => {
754
800
  const appDefinition = {
755
801
  managementMode: 'managed',
756
802
  vpcIsolation: 'isolated',
@@ -764,10 +810,11 @@ describe('AuroraBuilder', () => {
764
810
  },
765
811
  };
766
812
 
813
+ // No Aurora in CloudFormation stack (fresh deployment)
767
814
  const discoveredResources = {
768
- auroraClusterEndpoint: 'existing-cluster.us-east-1.rds.amazonaws.com',
769
815
  privateSubnetId1: 'subnet-1',
770
816
  privateSubnetId2: 'subnet-2',
817
+ // No auroraEndpoint
771
818
  };
772
819
 
773
820
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
@@ -779,6 +826,11 @@ describe('AuroraBuilder', () => {
779
826
  expect.stringContaining("managementMode='managed' ignoring")
780
827
  );
781
828
 
829
+ // Should log creating new Aurora
830
+ expect(consoleLogSpy).toHaveBeenCalledWith(
831
+ expect.stringContaining("no stack Aurora, creating new")
832
+ );
833
+
782
834
  // Should create new Aurora cluster (isolated mode)
783
835
  expect(result.resources.FriggAuroraCluster).toBeDefined();
784
836
  expect(result.environment.DATABASE_URL).toBeDefined();
@@ -126,7 +126,36 @@ class IntegrationBuilder extends InfrastructureBuilder {
126
126
 
127
127
  console.log(` Adding integration: ${integrationName}`);
128
128
 
129
- // Create HTTP API handler for integration
129
+ // Add webhook handler if enabled (BEFORE catch-all proxy route)
130
+ // CRITICAL: Webhook routes must be defined before the catch-all {proxy+} route
131
+ // to ensure proper route matching in AWS API Gateway/HTTP API
132
+ const webhookConfig = integration.Definition.webhooks;
133
+ if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
134
+ const webhookFunctionName = `${integrationName}Webhook`;
135
+
136
+ result.functions[webhookFunctionName] = {
137
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
138
+ skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
139
+ package: functionPackageConfig,
140
+ events: [
141
+ {
142
+ httpApi: {
143
+ path: `/api/${integrationName}-integration/webhooks`,
144
+ method: 'POST',
145
+ },
146
+ },
147
+ {
148
+ httpApi: {
149
+ path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
150
+ method: 'POST',
151
+ },
152
+ },
153
+ ],
154
+ };
155
+ console.log(` + Webhook handler enabled`);
156
+ }
157
+
158
+ // Create HTTP API handler for integration (catch-all route AFTER webhooks)
130
159
  result.functions[integrationName] = {
131
160
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
132
161
  skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
@@ -181,32 +210,6 @@ class IntegrationBuilder extends InfrastructureBuilder {
181
210
  };
182
211
 
183
212
  result.custom[queueReference] = queueName;
184
-
185
- // Add webhook handler if enabled
186
- const webhookConfig = integration.Definition.webhooks;
187
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
188
- const webhookFunctionName = `${integrationName}Webhook`;
189
-
190
- result.functions[webhookFunctionName] = {
191
- handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
192
- skipEsbuild: true, // Nested exports in node_modules - skip esbuild bundling
193
- events: [
194
- {
195
- httpApi: {
196
- path: `/api/${integrationName}-integration/webhooks`,
197
- method: 'POST',
198
- },
199
- },
200
- {
201
- httpApi: {
202
- path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
203
- method: 'POST',
204
- },
205
- },
206
- ],
207
- };
208
- console.log(` + Webhook handler enabled`);
209
- }
210
213
  }
211
214
 
212
215
  console.log(` ✅ Configured ${appDefinition.integrations.length} integrations`);
@@ -358,5 +358,236 @@ describe('IntegrationBuilder', () => {
358
358
  expect(integrationBuilder.getName()).toBe('IntegrationBuilder');
359
359
  });
360
360
  });
361
+
362
+ describe('Webhook Handler Configuration', () => {
363
+ it('should create webhook handler when webhooks enabled with boolean true', async () => {
364
+ const appDefinition = {
365
+ integrations: [
366
+ {
367
+ Definition: {
368
+ name: 'hubspot',
369
+ webhooks: true,
370
+ }
371
+ },
372
+ ],
373
+ };
374
+
375
+ const result = await integrationBuilder.build(appDefinition, {});
376
+
377
+ expect(result.functions.hubspotWebhook).toBeDefined();
378
+ expect(result.functions.hubspotWebhook.handler).toBe(
379
+ 'node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.hubspotWebhook.handler'
380
+ );
381
+ });
382
+
383
+ it('should create webhook handler when webhooks enabled with object', async () => {
384
+ const appDefinition = {
385
+ integrations: [
386
+ {
387
+ Definition: {
388
+ name: 'salesforce',
389
+ webhooks: { enabled: true },
390
+ }
391
+ },
392
+ ],
393
+ };
394
+
395
+ const result = await integrationBuilder.build(appDefinition, {});
396
+
397
+ expect(result.functions.salesforceWebhook).toBeDefined();
398
+ });
399
+
400
+ it('should NOT create webhook handler when webhooks disabled', async () => {
401
+ const appDefinition = {
402
+ integrations: [
403
+ {
404
+ Definition: {
405
+ name: 'slack',
406
+ webhooks: false,
407
+ }
408
+ },
409
+ ],
410
+ };
411
+
412
+ const result = await integrationBuilder.build(appDefinition, {});
413
+
414
+ expect(result.functions.slackWebhook).toBeUndefined();
415
+ });
416
+
417
+ it('should NOT create webhook handler when webhooks explicitly disabled in object', async () => {
418
+ const appDefinition = {
419
+ integrations: [
420
+ {
421
+ Definition: {
422
+ name: 'test',
423
+ webhooks: { enabled: false },
424
+ }
425
+ },
426
+ ],
427
+ };
428
+
429
+ const result = await integrationBuilder.build(appDefinition, {});
430
+
431
+ expect(result.functions.testWebhook).toBeUndefined();
432
+ });
433
+
434
+ it('should configure webhook with both base and ID-specific routes', async () => {
435
+ const appDefinition = {
436
+ integrations: [
437
+ {
438
+ Definition: {
439
+ name: 'stripe',
440
+ webhooks: true,
441
+ }
442
+ },
443
+ ],
444
+ };
445
+
446
+ const result = await integrationBuilder.build(appDefinition, {});
447
+
448
+ expect(result.functions.stripeWebhook.events).toEqual([
449
+ {
450
+ httpApi: {
451
+ path: '/api/stripe-integration/webhooks',
452
+ method: 'POST',
453
+ },
454
+ },
455
+ {
456
+ httpApi: {
457
+ path: '/api/stripe-integration/webhooks/{integrationId}',
458
+ method: 'POST',
459
+ },
460
+ },
461
+ ]);
462
+ });
463
+
464
+ it('should define webhook handler BEFORE catch-all proxy route (ordering bug fix)', async () => {
465
+ const appDefinition = {
466
+ integrations: [
467
+ {
468
+ Definition: {
469
+ name: 'asana',
470
+ webhooks: true,
471
+ }
472
+ },
473
+ ],
474
+ };
475
+
476
+ const result = await integrationBuilder.build(appDefinition, {});
477
+
478
+ // Get the keys (function names) in the order they were added
479
+ const functionKeys = Object.keys(result.functions);
480
+
481
+ // Webhook handler should be defined before the main integration handler
482
+ const webhookIndex = functionKeys.indexOf('asanaWebhook');
483
+ const integrationIndex = functionKeys.indexOf('asana');
484
+
485
+ expect(webhookIndex).toBeGreaterThanOrEqual(0);
486
+ expect(integrationIndex).toBeGreaterThan(0);
487
+ expect(webhookIndex).toBeLessThan(integrationIndex);
488
+ });
489
+
490
+ it('should maintain correct function order: webhook, integration, queue worker', async () => {
491
+ const appDefinition = {
492
+ integrations: [
493
+ {
494
+ Definition: {
495
+ name: 'test',
496
+ webhooks: true,
497
+ }
498
+ },
499
+ ],
500
+ };
501
+
502
+ const result = await integrationBuilder.build(appDefinition, {});
503
+
504
+ const functionKeys = Object.keys(result.functions);
505
+
506
+ // Expected order: webhook, integration, queueWorker
507
+ expect(functionKeys).toEqual([
508
+ 'testWebhook',
509
+ 'test',
510
+ 'testQueueWorker',
511
+ ]);
512
+ });
513
+
514
+ it('should handle multiple integrations with mixed webhook configurations', async () => {
515
+ const appDefinition = {
516
+ integrations: [
517
+ {
518
+ Definition: {
519
+ name: 'hubspot',
520
+ webhooks: true,
521
+ }
522
+ },
523
+ {
524
+ Definition: {
525
+ name: 'salesforce',
526
+ webhooks: false,
527
+ }
528
+ },
529
+ {
530
+ Definition: {
531
+ name: 'slack',
532
+ webhooks: { enabled: true },
533
+ }
534
+ },
535
+ ],
536
+ };
537
+
538
+ const result = await integrationBuilder.build(appDefinition, {});
539
+
540
+ // Hubspot: webhook enabled
541
+ expect(result.functions.hubspotWebhook).toBeDefined();
542
+ expect(result.functions.hubspot).toBeDefined();
543
+ expect(result.functions.hubspotQueueWorker).toBeDefined();
544
+
545
+ // Salesforce: webhook disabled
546
+ expect(result.functions.salesforceWebhook).toBeUndefined();
547
+ expect(result.functions.salesforce).toBeDefined();
548
+ expect(result.functions.salesforceQueueWorker).toBeDefined();
549
+
550
+ // Slack: webhook enabled via object
551
+ expect(result.functions.slackWebhook).toBeDefined();
552
+ expect(result.functions.slack).toBeDefined();
553
+ expect(result.functions.slackQueueWorker).toBeDefined();
554
+ });
555
+
556
+ it('should use skipEsbuild for webhook handlers', async () => {
557
+ const appDefinition = {
558
+ integrations: [
559
+ {
560
+ Definition: {
561
+ name: 'test',
562
+ webhooks: true,
563
+ }
564
+ },
565
+ ],
566
+ };
567
+
568
+ const result = await integrationBuilder.build(appDefinition, {});
569
+
570
+ expect(result.functions.testWebhook.skipEsbuild).toBe(true);
571
+ });
572
+
573
+ it('should apply package configuration to webhook handlers', async () => {
574
+ const appDefinition = {
575
+ integrations: [
576
+ {
577
+ Definition: {
578
+ name: 'test',
579
+ webhooks: true,
580
+ }
581
+ },
582
+ ],
583
+ };
584
+
585
+ const result = await integrationBuilder.build(appDefinition, {});
586
+
587
+ expect(result.functions.testWebhook.package).toBeDefined();
588
+ expect(result.functions.testWebhook.package.exclude).toContain('node_modules/aws-sdk/**');
589
+ expect(result.functions.testWebhook.package.exclude).toContain('node_modules/@prisma/**');
590
+ });
591
+ });
361
592
  });
362
593
 
@@ -141,12 +141,24 @@ class VpcBuilder extends InfrastructureBuilder {
141
141
  if (appDefinition.vpc.natGateway) delete appDefinition.vpc.natGateway.management;
142
142
  delete appDefinition.vpc.shareAcrossStages;
143
143
 
144
- // Set management based on isolation strategy
144
+ // Set management based on isolation strategy AND existing stack resources
145
145
  if (vpcIsolation === 'isolated') {
146
- management = 'create-new';
147
- appDefinition.vpc.natGateway = appDefinition.vpc.natGateway || {};
148
- appDefinition.vpc.natGateway.management = 'createAndManage';
149
- console.log(` managementMode='managed' + vpcIsolation='isolated' → creating new VPC`);
146
+ // Check if CloudFormation stack already has a VPC (stage-specific)
147
+ // Only string IDs mean resources are from stack (not CloudFormation refs)
148
+ const hasStackVpc = discoveredResources?.vpcId && typeof discoveredResources.vpcId === 'string';
149
+
150
+ if (hasStackVpc) {
151
+ // Stack has VPC - reuse it (standard flow: stack → orphaned → create)
152
+ management = 'discover';
153
+ appDefinition.vpc.selfHeal = true;
154
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has VPC, reusing`);
155
+ } else {
156
+ // No stack VPC - create new isolated VPC for this stage
157
+ management = 'create-new';
158
+ appDefinition.vpc.natGateway = appDefinition.vpc.natGateway || {};
159
+ appDefinition.vpc.natGateway.management = 'createAndManage';
160
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack VPC, creating new`);
161
+ }
150
162
  } else {
151
163
  management = 'discover';
152
164
  appDefinition.vpc.selfHeal = true;
@@ -837,7 +837,54 @@ describe('VpcBuilder', () => {
837
837
  });
838
838
 
839
839
  describe('Management Mode (Simplified API)', () => {
840
- it('should use managementMode=managed with vpcIsolation=isolated to create new VPC', async () => {
840
+ it('should reuse stack VPC when managementMode=managed + vpcIsolation=isolated AND stack has VPC', async () => {
841
+ const appDefinition = {
842
+ managementMode: 'managed',
843
+ vpcIsolation: 'isolated',
844
+ vpc: {
845
+ enable: true,
846
+ management: 'create-new', // Should be IGNORED
847
+ },
848
+ };
849
+
850
+ // CloudFormation stack has VPC (from previous deployment of this stage)
851
+ const discoveredResources = {
852
+ vpcId: 'vpc-stack-dev', // String = from stack (not default VPC)
853
+ defaultVpcId: 'vpc-stack-dev', // Also set for discover mode
854
+ privateSubnetId1: 'subnet-private-1',
855
+ privateSubnetId2: 'subnet-private-2',
856
+ publicSubnetId1: 'subnet-public-1',
857
+ publicSubnetId2: 'subnet-public-2',
858
+ };
859
+
860
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
861
+
862
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
863
+
864
+ // Should warn about ignored options
865
+ expect(consoleLogSpy).toHaveBeenCalledWith(
866
+ expect.stringContaining("managementMode='managed' ignoring")
867
+ );
868
+
869
+ // Should log reusing stack VPC
870
+ expect(consoleLogSpy).toHaveBeenCalledWith(
871
+ expect.stringContaining("stack has VPC, reusing")
872
+ );
873
+
874
+ // Should REUSE stack VPC (not create new)
875
+ expect(result.vpcId).toBe('vpc-stack-dev');
876
+ expect(result.resources.FriggVPC).toBeUndefined();
877
+
878
+ // Should REUSE stack subnets
879
+ expect(result.vpcConfig.subnetIds).toEqual([
880
+ 'subnet-private-1',
881
+ 'subnet-private-2'
882
+ ]);
883
+
884
+ consoleLogSpy.mockRestore();
885
+ });
886
+
887
+ it('should create new VPC when managementMode=managed + vpcIsolation=isolated AND stack has NO VPC', async () => {
841
888
  const appDefinition = {
842
889
  managementMode: 'managed',
843
890
  vpcIsolation: 'isolated',
@@ -847,9 +894,9 @@ describe('VpcBuilder', () => {
847
894
  },
848
895
  };
849
896
 
897
+ // No VPC in CloudFormation stack (fresh deployment)
850
898
  const discoveredResources = {
851
- defaultVpcId: 'vpc-existing',
852
- natGatewayId: 'nat-existing',
899
+ defaultVpcId: 'vpc-default', // Only default VPC exists (not from stack)
853
900
  };
854
901
 
855
902
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
@@ -860,8 +907,10 @@ describe('VpcBuilder', () => {
860
907
  expect(consoleLogSpy).toHaveBeenCalledWith(
861
908
  expect.stringContaining("managementMode='managed' ignoring")
862
909
  );
910
+
911
+ // Should log creating new VPC
863
912
  expect(consoleLogSpy).toHaveBeenCalledWith(
864
- expect.stringContaining("vpc.management")
913
+ expect.stringContaining("no stack VPC, creating new")
865
914
  );
866
915
 
867
916
  // Should create new isolated VPC
@@ -36,8 +36,10 @@ function createBaseDefinition(
36
36
  const skipEsbuildPackageConfig = {
37
37
  // Explicitly include project files that handlers need
38
38
  include: [
39
- // Include security folder if DocumentDB TLS is configured
40
- ...(AppDefinition.database?.documentDB?.enable ? ['security/**'] : []),
39
+ // Include DocumentDB TLS certificate if configured
40
+ ...(AppDefinition.database?.documentDB?.tlsCAFile
41
+ ? [AppDefinition.database.documentDB.tlsCAFile.replace(/^\.\//, '')]
42
+ : []),
41
43
  ],
42
44
  exclude: [
43
45
  // Exclude Prisma (provided via Lambda Layer)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.9252e46.0",
4
+ "version": "2.0.0--canary.461.ae53ecc.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -11,8 +11,8 @@
11
11
  "@babel/eslint-parser": "^7.18.9",
12
12
  "@babel/parser": "^7.25.3",
13
13
  "@babel/traverse": "^7.25.3",
14
- "@friggframework/schemas": "2.0.0--canary.461.9252e46.0",
15
- "@friggframework/test": "2.0.0--canary.461.9252e46.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.ae53ecc.0",
15
+ "@friggframework/test": "2.0.0--canary.461.ae53ecc.0",
16
16
  "@hapi/boom": "^10.0.1",
17
17
  "@inquirer/prompts": "^5.3.8",
18
18
  "axios": "^1.7.2",
@@ -34,8 +34,8 @@
34
34
  "serverless-http": "^2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@friggframework/eslint-config": "2.0.0--canary.461.9252e46.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.9252e46.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.ae53ecc.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.ae53ecc.0",
39
39
  "aws-sdk-client-mock": "^4.1.0",
40
40
  "aws-sdk-client-mock-jest": "^4.1.0",
41
41
  "jest": "^30.1.3",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "9252e462edd87b11d21f06040f679dafececfd92"
73
+ "gitHead": "ae53eccb40200f7624ba14908c90b0123051e5a0"
74
74
  }