@friggframework/devtools 2.0.0--canary.461.d2e26d0.0 → 2.0.0--canary.461.9667b72.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
+ // CloudFormation discovery sets 'auroraClusterId' (string) when found in stack
106
+ const hasStackAurora = discoveredResources?.auroraClusterId &&
107
+ typeof discoveredResources.auroraClusterId === '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,52 @@ 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
+ auroraClusterId: 'stack-cluster-dev', // CloudFormation discovery sets this
770
+ auroraClusterEndpoint: 'stack-cluster-dev.us-east-1.rds.amazonaws.com', // For discover mode
771
+ auroraClusterPort: 5432,
772
+ auroraClusterIdentifier: 'stack-cluster-dev',
773
+ privateSubnetId1: 'subnet-1',
774
+ privateSubnetId2: 'subnet-2',
775
+ };
776
+
777
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
778
+
779
+ const result = await auroraBuilder.build(appDefinition, discoveredResources);
780
+
781
+ // Should warn about ignored options
782
+ expect(consoleLogSpy).toHaveBeenCalledWith(
783
+ expect.stringContaining("managementMode='managed' ignoring")
784
+ );
785
+
786
+ // Should log reusing stack Aurora
787
+ expect(consoleLogSpy).toHaveBeenCalledWith(
788
+ expect.stringContaining("stack has Aurora, reusing")
789
+ );
790
+
791
+ // Should REUSE stack Aurora (not create new)
792
+ expect(result.resources.FriggAuroraCluster).toBeUndefined();
793
+ expect(result.environment.DATABASE_URL).toBeDefined();
794
+
795
+ consoleLogSpy.mockRestore();
796
+ });
797
+
798
+ it('should create new Aurora when managementMode=managed + vpcIsolation=isolated AND stack has NO Aurora', async () => {
754
799
  const appDefinition = {
755
800
  managementMode: 'managed',
756
801
  vpcIsolation: 'isolated',
@@ -764,10 +809,11 @@ describe('AuroraBuilder', () => {
764
809
  },
765
810
  };
766
811
 
812
+ // No Aurora in CloudFormation stack (fresh deployment)
767
813
  const discoveredResources = {
768
- auroraClusterEndpoint: 'existing-cluster.us-east-1.rds.amazonaws.com',
769
814
  privateSubnetId1: 'subnet-1',
770
815
  privateSubnetId2: 'subnet-2',
816
+ // No auroraEndpoint
771
817
  };
772
818
 
773
819
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
@@ -779,6 +825,11 @@ describe('AuroraBuilder', () => {
779
825
  expect.stringContaining("managementMode='managed' ignoring")
780
826
  );
781
827
 
828
+ // Should log creating new Aurora
829
+ expect(consoleLogSpy).toHaveBeenCalledWith(
830
+ expect.stringContaining("no stack Aurora, creating new")
831
+ );
832
+
782
833
  // Should create new Aurora cluster (isolated mode)
783
834
  expect(result.resources.FriggAuroraCluster).toBeDefined();
784
835
  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
+ // CloudFormation discovery sets 'defaultVpcId' (string) when found in stack
148
+ const hasStackVpc = discoveredResources?.defaultVpcId && typeof discoveredResources.defaultVpcId === '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,53 @@ 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
+ defaultVpcId: 'vpc-stack-dev', // CloudFormation discovery sets this
853
+ privateSubnetId1: 'subnet-private-1',
854
+ privateSubnetId2: 'subnet-private-2',
855
+ publicSubnetId1: 'subnet-public-1',
856
+ publicSubnetId2: 'subnet-public-2',
857
+ };
858
+
859
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
860
+
861
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
862
+
863
+ // Should warn about ignored options
864
+ expect(consoleLogSpy).toHaveBeenCalledWith(
865
+ expect.stringContaining("managementMode='managed' ignoring")
866
+ );
867
+
868
+ // Should log reusing stack VPC
869
+ expect(consoleLogSpy).toHaveBeenCalledWith(
870
+ expect.stringContaining("stack has VPC, reusing")
871
+ );
872
+
873
+ // Should REUSE stack VPC (not create new)
874
+ expect(result.vpcId).toBe('vpc-stack-dev');
875
+ expect(result.resources.FriggVPC).toBeUndefined();
876
+
877
+ // Should REUSE stack subnets
878
+ expect(result.vpcConfig.subnetIds).toEqual([
879
+ 'subnet-private-1',
880
+ 'subnet-private-2'
881
+ ]);
882
+
883
+ consoleLogSpy.mockRestore();
884
+ });
885
+
886
+ it('should create new VPC when managementMode=managed + vpcIsolation=isolated AND stack has NO VPC', async () => {
841
887
  const appDefinition = {
842
888
  managementMode: 'managed',
843
889
  vpcIsolation: 'isolated',
@@ -847,9 +893,9 @@ describe('VpcBuilder', () => {
847
893
  },
848
894
  };
849
895
 
896
+ // No VPC in CloudFormation stack (fresh deployment)
850
897
  const discoveredResources = {
851
- defaultVpcId: 'vpc-existing',
852
- natGatewayId: 'nat-existing',
898
+ defaultVpcId: 'vpc-default', // Only default VPC exists (not from stack)
853
899
  };
854
900
 
855
901
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
@@ -860,8 +906,10 @@ describe('VpcBuilder', () => {
860
906
  expect(consoleLogSpy).toHaveBeenCalledWith(
861
907
  expect.stringContaining("managementMode='managed' ignoring")
862
908
  );
909
+
910
+ // Should log creating new VPC
863
911
  expect(consoleLogSpy).toHaveBeenCalledWith(
864
- expect.stringContaining("vpc.management")
912
+ expect.stringContaining("no stack VPC, creating new")
865
913
  );
866
914
 
867
915
  // Should create new isolated VPC
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.d2e26d0.0",
4
+ "version": "2.0.0--canary.461.9667b72.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.d2e26d0.0",
15
- "@friggframework/test": "2.0.0--canary.461.d2e26d0.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.9667b72.0",
15
+ "@friggframework/test": "2.0.0--canary.461.9667b72.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.d2e26d0.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.d2e26d0.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.9667b72.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.9667b72.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": "d2e26d09ccf4457e3bb42559d6f7e400779cdba7"
73
+ "gitHead": "9667b7204b9faaeb6caf8d1a94841ab365f9b7a3"
74
74
  }