@friggframework/devtools 2.0.0--canary.461.0afe1c4.0 → 2.0.0--canary.461.c89a166.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.
- package/infrastructure/domains/database/aurora-builder.js +4 -4
- package/infrastructure/domains/database/aurora-builder.test.js +49 -31
- package/infrastructure/domains/parameters/ssm-builder.js +2 -2
- package/infrastructure/domains/parameters/ssm-builder.test.js +2 -1
- package/infrastructure/domains/shared/cloudformation-discovery.js +146 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +228 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +76 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +14 -3
- package/infrastructure/domains/shared/resource-discovery.js +18 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +5 -3
- package/package.json +6 -6
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Database connection environment variables
|
|
12
12
|
*
|
|
13
13
|
* Supports three management modes:
|
|
14
|
-
* 1.
|
|
14
|
+
* 1. managed: Creates new Aurora cluster
|
|
15
15
|
* 2. use-existing: Uses explicitly provided cluster
|
|
16
16
|
* 3. discover (default): Discovers existing cluster
|
|
17
17
|
*/
|
|
@@ -49,7 +49,7 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
49
49
|
const dbConfig = appDefinition.database.postgres;
|
|
50
50
|
|
|
51
51
|
// Validate management mode
|
|
52
|
-
const validModes = ['discover', '
|
|
52
|
+
const validModes = ['discover', 'managed', 'use-existing'];
|
|
53
53
|
const management = dbConfig.management || 'discover';
|
|
54
54
|
if (!validModes.includes(management)) {
|
|
55
55
|
result.addError(`Invalid database.postgres.management: "${management}"`);
|
|
@@ -95,7 +95,7 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
95
95
|
|
|
96
96
|
// Handle different management modes
|
|
97
97
|
switch (management) {
|
|
98
|
-
case '
|
|
98
|
+
case 'managed':
|
|
99
99
|
await this.createNewAurora(appDefinition, discoveredResources, result);
|
|
100
100
|
break;
|
|
101
101
|
case 'use-existing':
|
|
@@ -281,7 +281,7 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
281
281
|
|
|
282
282
|
if (!discoveredResources.auroraClusterEndpoint) {
|
|
283
283
|
throw new Error(
|
|
284
|
-
'No Aurora cluster found in discovery mode. Set management to "
|
|
284
|
+
'No Aurora cluster found in discovery mode. Set management to "managed" or provide endpoint with "use-existing".'
|
|
285
285
|
);
|
|
286
286
|
}
|
|
287
287
|
|
|
@@ -107,12 +107,12 @@ describe('AuroraBuilder', () => {
|
|
|
107
107
|
expect(result.errors).toEqual([]);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
it('should pass validation for
|
|
110
|
+
it('should pass validation for managed mode', () => {
|
|
111
111
|
const appDefinition = {
|
|
112
112
|
database: {
|
|
113
113
|
postgres: {
|
|
114
114
|
enable: true,
|
|
115
|
-
management: '
|
|
115
|
+
management: 'managed',
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
118
|
};
|
|
@@ -144,9 +144,7 @@ describe('AuroraBuilder', () => {
|
|
|
144
144
|
const result = auroraBuilder.validate(appDefinition);
|
|
145
145
|
|
|
146
146
|
expect(result.valid).toBe(false);
|
|
147
|
-
expect(result.errors).
|
|
148
|
-
expect.stringContaining('Invalid database.postgres.management')
|
|
149
|
-
);
|
|
147
|
+
expect(result.errors.some(e => e.includes('Invalid database.postgres.management'))).toBe(true);
|
|
150
148
|
});
|
|
151
149
|
|
|
152
150
|
it('should error when use-existing without endpoint', () => {
|
|
@@ -196,9 +194,7 @@ describe('AuroraBuilder', () => {
|
|
|
196
194
|
const result = auroraBuilder.validate(appDefinition);
|
|
197
195
|
|
|
198
196
|
expect(result.valid).toBe(false);
|
|
199
|
-
expect(result.errors).
|
|
200
|
-
expect.stringContaining('minCapacity must be between 0.5 and 128')
|
|
201
|
-
);
|
|
197
|
+
expect(result.errors.some(e => e.includes('minCapacity must be between 0.5 and 128'))).toBe(true);
|
|
202
198
|
});
|
|
203
199
|
|
|
204
200
|
it('should error when maxCapacity is out of range', () => {
|
|
@@ -214,9 +210,7 @@ describe('AuroraBuilder', () => {
|
|
|
214
210
|
const result = auroraBuilder.validate(appDefinition);
|
|
215
211
|
|
|
216
212
|
expect(result.valid).toBe(false);
|
|
217
|
-
expect(result.errors).
|
|
218
|
-
expect.stringContaining('maxCapacity must be between 0.5 and 128')
|
|
219
|
-
);
|
|
213
|
+
expect(result.errors.some(e => e.includes('maxCapacity must be between 0.5 and 128'))).toBe(true);
|
|
220
214
|
});
|
|
221
215
|
|
|
222
216
|
it('should pass with valid capacity values', () => {
|
|
@@ -247,9 +241,7 @@ describe('AuroraBuilder', () => {
|
|
|
247
241
|
|
|
248
242
|
const result = auroraBuilder.validate(appDefinition);
|
|
249
243
|
|
|
250
|
-
expect(result.warnings).
|
|
251
|
-
expect.stringContaining('publiclyAccessible=true is not recommended for production')
|
|
252
|
-
);
|
|
244
|
+
expect(result.warnings.some(w => w.includes('publiclyAccessible=true is not recommended for production'))).toBe(true);
|
|
253
245
|
});
|
|
254
246
|
|
|
255
247
|
it('should not warn when publiclyAccessible is false', () => {
|
|
@@ -287,8 +279,11 @@ describe('AuroraBuilder', () => {
|
|
|
287
279
|
|
|
288
280
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
289
281
|
|
|
290
|
-
|
|
291
|
-
expect(result.environment.DATABASE_URL).
|
|
282
|
+
// buildDatabaseUrl returns CloudFormation Fn::Sub object, not plain string
|
|
283
|
+
expect(result.environment.DATABASE_URL).toBeDefined();
|
|
284
|
+
expect(result.environment.DATABASE_URL['Fn::Sub']).toBeDefined();
|
|
285
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Host).toBe('cluster.abc.us-east-1.rds.amazonaws.com');
|
|
286
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Port).toBe(5432);
|
|
292
287
|
});
|
|
293
288
|
|
|
294
289
|
it('should add IAM permissions for Secrets Manager', async () => {
|
|
@@ -604,13 +599,13 @@ describe('AuroraBuilder', () => {
|
|
|
604
599
|
});
|
|
605
600
|
});
|
|
606
601
|
|
|
607
|
-
describe('build() -
|
|
602
|
+
describe('build() - managed mode', () => {
|
|
608
603
|
it('should create Aurora cluster resources', async () => {
|
|
609
604
|
const appDefinition = {
|
|
610
605
|
database: {
|
|
611
606
|
postgres: {
|
|
612
607
|
enable: true,
|
|
613
|
-
management: '
|
|
608
|
+
management: 'managed',
|
|
614
609
|
},
|
|
615
610
|
},
|
|
616
611
|
};
|
|
@@ -644,7 +639,7 @@ describe('AuroraBuilder', () => {
|
|
|
644
639
|
database: {
|
|
645
640
|
postgres: {
|
|
646
641
|
enable: true,
|
|
647
|
-
management: '
|
|
642
|
+
management: 'managed',
|
|
648
643
|
},
|
|
649
644
|
},
|
|
650
645
|
};
|
|
@@ -666,19 +661,21 @@ describe('AuroraBuilder', () => {
|
|
|
666
661
|
database: {
|
|
667
662
|
postgres: {
|
|
668
663
|
enable: true,
|
|
669
|
-
management: '
|
|
664
|
+
management: 'managed',
|
|
670
665
|
},
|
|
671
666
|
},
|
|
672
667
|
};
|
|
673
668
|
|
|
674
669
|
const discoveredResources = {
|
|
675
670
|
defaultVpcId: 'vpc-123',
|
|
671
|
+
privateSubnetId1: 'subnet-1',
|
|
672
|
+
privateSubnetId2: 'subnet-2',
|
|
676
673
|
};
|
|
677
674
|
|
|
678
675
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
679
676
|
|
|
680
|
-
expect(result.resources.
|
|
681
|
-
expect(result.resources.
|
|
677
|
+
expect(result.resources.FriggDBSecret).toBeDefined();
|
|
678
|
+
expect(result.resources.FriggDBSecret.Type).toBe('AWS::SecretsManager::Secret');
|
|
682
679
|
});
|
|
683
680
|
|
|
684
681
|
it('should configure Aurora Serverless v2', async () => {
|
|
@@ -686,12 +683,18 @@ describe('AuroraBuilder', () => {
|
|
|
686
683
|
database: {
|
|
687
684
|
postgres: {
|
|
688
685
|
enable: true,
|
|
689
|
-
management: '
|
|
686
|
+
management: 'managed',
|
|
690
687
|
},
|
|
691
688
|
},
|
|
692
689
|
};
|
|
693
690
|
|
|
694
|
-
const
|
|
691
|
+
const discoveredResources = {
|
|
692
|
+
defaultVpcId: 'vpc-123',
|
|
693
|
+
privateSubnetId1: 'subnet-1',
|
|
694
|
+
privateSubnetId2: 'subnet-2',
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
695
698
|
|
|
696
699
|
expect(result.resources.FriggAuroraCluster.Properties.EngineMode).toBe('provisioned');
|
|
697
700
|
expect(result.resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration).toBeDefined();
|
|
@@ -702,14 +705,20 @@ describe('AuroraBuilder', () => {
|
|
|
702
705
|
database: {
|
|
703
706
|
postgres: {
|
|
704
707
|
enable: true,
|
|
705
|
-
management: '
|
|
708
|
+
management: 'managed',
|
|
706
709
|
minCapacity: 1,
|
|
707
710
|
maxCapacity: 8,
|
|
708
711
|
},
|
|
709
712
|
},
|
|
710
713
|
};
|
|
711
714
|
|
|
712
|
-
const
|
|
715
|
+
const discoveredResources = {
|
|
716
|
+
defaultVpcId: 'vpc-123',
|
|
717
|
+
privateSubnetId1: 'subnet-1',
|
|
718
|
+
privateSubnetId2: 'subnet-2',
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
713
722
|
|
|
714
723
|
const scaling = result.resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
715
724
|
expect(scaling.MinCapacity).toBe(1);
|
|
@@ -721,12 +730,18 @@ describe('AuroraBuilder', () => {
|
|
|
721
730
|
database: {
|
|
722
731
|
postgres: {
|
|
723
732
|
enable: true,
|
|
724
|
-
management: '
|
|
733
|
+
management: 'managed',
|
|
725
734
|
},
|
|
726
735
|
},
|
|
727
736
|
};
|
|
728
737
|
|
|
729
|
-
const
|
|
738
|
+
const discoveredResources = {
|
|
739
|
+
defaultVpcId: 'vpc-123',
|
|
740
|
+
privateSubnetId1: 'subnet-1',
|
|
741
|
+
privateSubnetId2: 'subnet-2',
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
730
745
|
|
|
731
746
|
const scaling = result.resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
732
747
|
expect(scaling.MinCapacity).toBeGreaterThanOrEqual(0.5);
|
|
@@ -749,8 +764,11 @@ describe('AuroraBuilder', () => {
|
|
|
749
764
|
|
|
750
765
|
const result = await auroraBuilder.build(appDefinition, {});
|
|
751
766
|
|
|
752
|
-
|
|
753
|
-
expect(result.environment.
|
|
767
|
+
// use-existing mode sets individual components, not DATABASE_URL
|
|
768
|
+
expect(result.environment.DATABASE_HOST).toBe('custom-db.example.com');
|
|
769
|
+
expect(result.environment.DATABASE_PORT).toBe('5432');
|
|
770
|
+
expect(result.environment.DATABASE_NAME).toBe('frigg');
|
|
771
|
+
expect(result.environment.DATABASE_USER).toBe('postgres');
|
|
754
772
|
});
|
|
755
773
|
|
|
756
774
|
it('should not create Aurora resources in use-existing mode', async () => {
|
|
@@ -767,7 +785,7 @@ describe('AuroraBuilder', () => {
|
|
|
767
785
|
const result = await auroraBuilder.build(appDefinition, {});
|
|
768
786
|
|
|
769
787
|
expect(result.resources.FriggAuroraCluster).toBeUndefined();
|
|
770
|
-
expect(result.resources.
|
|
788
|
+
expect(result.resources.FriggDBSecret).toBeUndefined();
|
|
771
789
|
});
|
|
772
790
|
});
|
|
773
791
|
|
|
@@ -36,8 +36,8 @@ class SsmBuilder extends InfrastructureBuilder {
|
|
|
36
36
|
|
|
37
37
|
// Validate parameters if provided
|
|
38
38
|
if (appDefinition.ssm.parameters) {
|
|
39
|
-
if (typeof appDefinition.ssm.parameters !== 'object') {
|
|
40
|
-
result.addError('ssm.parameters must be an object');
|
|
39
|
+
if (typeof appDefinition.ssm.parameters !== 'object' || Array.isArray(appDefinition.ssm.parameters)) {
|
|
40
|
+
result.addError('ssm.parameters must be an object (not an array)');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -103,7 +103,7 @@ describe('SsmBuilder', () => {
|
|
|
103
103
|
const result = ssmBuilder.validate(appDefinition);
|
|
104
104
|
|
|
105
105
|
expect(result.valid).toBe(false);
|
|
106
|
-
expect(result.errors
|
|
106
|
+
expect(result.errors.some(e => e.includes('ssm.parameters must be an object'))).toBe(true);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
it('should error if parameters is an array', () => {
|
|
@@ -117,6 +117,7 @@ describe('SsmBuilder', () => {
|
|
|
117
117
|
const result = ssmBuilder.validate(appDefinition);
|
|
118
118
|
|
|
119
119
|
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.errors.some(e => e.includes('ssm.parameters must be an object'))).toBe(true);
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFormation-based Resource Discovery
|
|
3
|
+
*
|
|
4
|
+
* Domain Service - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Discovers resources from existing CloudFormation stacks as the primary
|
|
7
|
+
* source of truth before falling back to direct AWS API discovery.
|
|
8
|
+
*
|
|
9
|
+
* Benefits:
|
|
10
|
+
* - Faster discovery (1 CF call vs multiple AWS API calls)
|
|
11
|
+
* - More accurate (stack is source of truth)
|
|
12
|
+
* - Eliminates tagging dependencies
|
|
13
|
+
* - Idempotent (discover mode reuses stack resources)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class CloudFormationDiscovery {
|
|
17
|
+
constructor(provider) {
|
|
18
|
+
this.provider = provider;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Discover resources from an existing CloudFormation stack
|
|
23
|
+
*
|
|
24
|
+
* @param {string} stackName - Name of the CloudFormation stack
|
|
25
|
+
* @returns {Promise<Object|null>} Discovered resources or null if stack doesn't exist
|
|
26
|
+
*/
|
|
27
|
+
async discoverFromStack(stackName) {
|
|
28
|
+
try {
|
|
29
|
+
// Try to get the stack
|
|
30
|
+
const stack = await this.provider.describeStack(stackName);
|
|
31
|
+
|
|
32
|
+
// Get stack resources
|
|
33
|
+
const resources = await this.provider.listStackResources(stackName);
|
|
34
|
+
|
|
35
|
+
// Extract discovered resources from outputs and resources
|
|
36
|
+
const discovered = {};
|
|
37
|
+
|
|
38
|
+
// Extract from outputs
|
|
39
|
+
if (stack.Outputs && stack.Outputs.length > 0) {
|
|
40
|
+
this._extractFromOutputs(stack.Outputs, discovered);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract from resources
|
|
44
|
+
if (resources && resources.length > 0) {
|
|
45
|
+
this._extractFromResources(resources, discovered);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return discovered;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Stack doesn't exist - return null to trigger fallback discovery
|
|
51
|
+
if (error.message && error.message.includes('does not exist')) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Other errors - log and return null
|
|
56
|
+
console.warn(`⚠️ CloudFormation discovery failed: ${error.message}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract discovered resources from CloudFormation stack outputs
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
* @param {Array} outputs - CloudFormation stack outputs
|
|
66
|
+
* @param {Object} discovered - Object to populate with discovered resources
|
|
67
|
+
*/
|
|
68
|
+
_extractFromOutputs(outputs, discovered) {
|
|
69
|
+
const outputMap = outputs.reduce((acc, output) => {
|
|
70
|
+
acc[output.OutputKey] = output.OutputValue;
|
|
71
|
+
return acc;
|
|
72
|
+
}, {});
|
|
73
|
+
|
|
74
|
+
// VPC outputs
|
|
75
|
+
if (outputMap.VpcId) {
|
|
76
|
+
discovered.vpcId = outputMap.VpcId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (outputMap.PrivateSubnetIds) {
|
|
80
|
+
// Handle comma-separated subnet IDs
|
|
81
|
+
discovered.privateSubnetIds = outputMap.PrivateSubnetIds.split(',').map(id => id.trim());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (outputMap.PublicSubnetId) {
|
|
85
|
+
discovered.publicSubnetId = outputMap.PublicSubnetId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (outputMap.SecurityGroupId) {
|
|
89
|
+
discovered.securityGroupId = outputMap.SecurityGroupId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// KMS outputs
|
|
93
|
+
if (outputMap.KMS_KEY_ARN) {
|
|
94
|
+
discovered.defaultKmsKeyId = outputMap.KMS_KEY_ARN;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Database outputs (if exposed)
|
|
98
|
+
if (outputMap.DatabaseEndpoint) {
|
|
99
|
+
discovered.databaseEndpoint = outputMap.DatabaseEndpoint;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract discovered resources from CloudFormation stack resources
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
* @param {Array} resources - CloudFormation stack resources
|
|
108
|
+
* @param {Object} discovered - Object to populate with discovered resources
|
|
109
|
+
*/
|
|
110
|
+
_extractFromResources(resources, discovered) {
|
|
111
|
+
for (const resource of resources) {
|
|
112
|
+
const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
|
|
113
|
+
|
|
114
|
+
// Aurora cluster
|
|
115
|
+
if (LogicalResourceId === 'FriggAuroraCluster' && ResourceType === 'AWS::RDS::DBCluster') {
|
|
116
|
+
discovered.auroraClusterId = PhysicalResourceId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Migration status bucket
|
|
120
|
+
if (LogicalResourceId === 'FriggMigrationStatusBucket' && ResourceType === 'AWS::S3::Bucket') {
|
|
121
|
+
discovered.migrationStatusBucket = PhysicalResourceId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Migration queue
|
|
125
|
+
if (LogicalResourceId === 'DbMigrationQueue' && ResourceType === 'AWS::SQS::Queue') {
|
|
126
|
+
discovered.migrationQueueUrl = PhysicalResourceId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// NAT Gateway
|
|
130
|
+
if (LogicalResourceId === 'FriggNatGateway' && ResourceType === 'AWS::EC2::NatGateway') {
|
|
131
|
+
discovered.natGatewayId = PhysicalResourceId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// KMS Key (alternative to output)
|
|
135
|
+
if (LogicalResourceId === 'FriggKMSKey' && ResourceType === 'AWS::KMS::Key') {
|
|
136
|
+
// Note: For KMS, we prefer the ARN from outputs, but this is a fallback
|
|
137
|
+
if (!discovered.defaultKmsKeyId) {
|
|
138
|
+
discovered.defaultKmsKeyId = PhysicalResourceId;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { CloudFormationDiscovery };
|
|
146
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CloudFormation-based Resource Discovery
|
|
3
|
+
*
|
|
4
|
+
* Tests discovering resources from existing CloudFormation stacks
|
|
5
|
+
* before falling back to direct AWS API discovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
9
|
+
|
|
10
|
+
describe('CloudFormationDiscovery', () => {
|
|
11
|
+
let cfDiscovery;
|
|
12
|
+
let mockProvider;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockProvider = {
|
|
16
|
+
describeStack: jest.fn(),
|
|
17
|
+
listStackResources: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
cfDiscovery = new CloudFormationDiscovery(mockProvider);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('discoverFromStack()', () => {
|
|
23
|
+
it('should return null when stack does not exist', async () => {
|
|
24
|
+
mockProvider.describeStack.mockRejectedValue(
|
|
25
|
+
new Error('Stack with id test-stack does not exist')
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
29
|
+
|
|
30
|
+
expect(result).toBeNull();
|
|
31
|
+
expect(mockProvider.describeStack).toHaveBeenCalledWith('test-stack');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should extract VPC resources from stack outputs', async () => {
|
|
35
|
+
const mockStack = {
|
|
36
|
+
StackName: 'test-stack',
|
|
37
|
+
Outputs: [
|
|
38
|
+
{ OutputKey: 'VpcId', OutputValue: 'vpc-123' },
|
|
39
|
+
{ OutputKey: 'PrivateSubnetIds', OutputValue: 'subnet-1,subnet-2' },
|
|
40
|
+
{ OutputKey: 'PublicSubnetId', OutputValue: 'subnet-3' },
|
|
41
|
+
{ OutputKey: 'SecurityGroupId', OutputValue: 'sg-123' },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
46
|
+
mockProvider.listStackResources.mockResolvedValue([]);
|
|
47
|
+
|
|
48
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
49
|
+
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
vpcId: 'vpc-123',
|
|
52
|
+
privateSubnetIds: ['subnet-1', 'subnet-2'],
|
|
53
|
+
publicSubnetId: 'subnet-3',
|
|
54
|
+
securityGroupId: 'sg-123',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should extract KMS key from stack outputs', async () => {
|
|
59
|
+
const mockStack = {
|
|
60
|
+
StackName: 'test-stack',
|
|
61
|
+
Outputs: [
|
|
62
|
+
{ OutputKey: 'KMS_KEY_ARN', OutputValue: 'arn:aws:kms:us-east-1:123456789:key/abc' },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
67
|
+
mockProvider.listStackResources.mockResolvedValue([]);
|
|
68
|
+
|
|
69
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should extract Aurora cluster from stack resources', async () => {
|
|
77
|
+
const mockStack = {
|
|
78
|
+
StackName: 'test-stack',
|
|
79
|
+
Outputs: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const mockResources = [
|
|
83
|
+
{
|
|
84
|
+
LogicalResourceId: 'FriggAuroraCluster',
|
|
85
|
+
PhysicalResourceId: 'test-cluster',
|
|
86
|
+
ResourceType: 'AWS::RDS::DBCluster',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
91
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
92
|
+
|
|
93
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
auroraClusterId: 'test-cluster',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should extract S3 migration bucket from stack resources', async () => {
|
|
101
|
+
const mockStack = {
|
|
102
|
+
StackName: 'test-stack',
|
|
103
|
+
Outputs: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const mockResources = [
|
|
107
|
+
{
|
|
108
|
+
LogicalResourceId: 'FriggMigrationStatusBucket',
|
|
109
|
+
PhysicalResourceId: 'test-migration-bucket',
|
|
110
|
+
ResourceType: 'AWS::S3::Bucket',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
115
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
116
|
+
|
|
117
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
118
|
+
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
migrationStatusBucket: 'test-migration-bucket',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should extract SQS migration queue from stack resources', async () => {
|
|
125
|
+
const mockStack = {
|
|
126
|
+
StackName: 'test-stack',
|
|
127
|
+
Outputs: [],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const mockResources = [
|
|
131
|
+
{
|
|
132
|
+
LogicalResourceId: 'DbMigrationQueue',
|
|
133
|
+
PhysicalResourceId: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
|
|
134
|
+
ResourceType: 'AWS::SQS::Queue',
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
139
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
140
|
+
|
|
141
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual({
|
|
144
|
+
migrationQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should extract NAT Gateway from stack resources', async () => {
|
|
149
|
+
const mockStack = {
|
|
150
|
+
StackName: 'test-stack',
|
|
151
|
+
Outputs: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const mockResources = [
|
|
155
|
+
{
|
|
156
|
+
LogicalResourceId: 'FriggNatGateway',
|
|
157
|
+
PhysicalResourceId: 'nat-0123456789',
|
|
158
|
+
ResourceType: 'AWS::EC2::NatGateway',
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
163
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
164
|
+
|
|
165
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
166
|
+
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
natGatewayId: 'nat-0123456789',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should combine outputs and resources correctly', async () => {
|
|
173
|
+
const mockStack = {
|
|
174
|
+
StackName: 'test-stack',
|
|
175
|
+
Outputs: [
|
|
176
|
+
{ OutputKey: 'VpcId', OutputValue: 'vpc-123' },
|
|
177
|
+
{ OutputKey: 'KMS_KEY_ARN', OutputValue: 'arn:aws:kms:us-east-1:123456789:key/abc' },
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const mockResources = [
|
|
182
|
+
{
|
|
183
|
+
LogicalResourceId: 'FriggAuroraCluster',
|
|
184
|
+
PhysicalResourceId: 'test-cluster',
|
|
185
|
+
ResourceType: 'AWS::RDS::DBCluster',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
LogicalResourceId: 'FriggNatGateway',
|
|
189
|
+
PhysicalResourceId: 'nat-123',
|
|
190
|
+
ResourceType: 'AWS::EC2::NatGateway',
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
195
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
196
|
+
|
|
197
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual({
|
|
200
|
+
vpcId: 'vpc-123',
|
|
201
|
+
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
|
|
202
|
+
auroraClusterId: 'test-cluster',
|
|
203
|
+
natGatewayId: 'nat-123',
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should handle stack with no relevant resources', async () => {
|
|
208
|
+
const mockStack = {
|
|
209
|
+
StackName: 'test-stack',
|
|
210
|
+
Outputs: [],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
214
|
+
mockProvider.listStackResources.mockResolvedValue([
|
|
215
|
+
{
|
|
216
|
+
LogicalResourceId: 'SomeOtherResource',
|
|
217
|
+
PhysicalResourceId: 'some-id',
|
|
218
|
+
ResourceType: 'AWS::Lambda::Function',
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
223
|
+
|
|
224
|
+
expect(result).toEqual({});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
@@ -20,6 +20,7 @@ let KMSClient, ListKeysCommand, DescribeKeyCommand, ListAliasesCommand;
|
|
|
20
20
|
let RDSClient, DescribeDBClustersCommand, DescribeDBInstancesCommand;
|
|
21
21
|
let SSMClient, GetParameterCommand, GetParametersByPathCommand;
|
|
22
22
|
let SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand;
|
|
23
|
+
let CloudFormationClient, DescribeStacksCommand, ListStackResourcesCommand;
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Lazy load EC2 SDK
|
|
@@ -87,6 +88,18 @@ function loadSecretsManager() {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Lazy load CloudFormation SDK
|
|
93
|
+
*/
|
|
94
|
+
function loadCloudFormation() {
|
|
95
|
+
if (!CloudFormationClient) {
|
|
96
|
+
const cfModule = require('@aws-sdk/client-cloudformation');
|
|
97
|
+
CloudFormationClient = cfModule.CloudFormationClient;
|
|
98
|
+
DescribeStacksCommand = cfModule.DescribeStacksCommand;
|
|
99
|
+
ListStackResourcesCommand = cfModule.ListStackResourcesCommand;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
class AWSProviderAdapter extends CloudProviderAdapter {
|
|
91
104
|
constructor(region, credentials = {}) {
|
|
92
105
|
super();
|
|
@@ -99,6 +112,7 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
99
112
|
this.rds = null;
|
|
100
113
|
this.ssm = null;
|
|
101
114
|
this.secretsManager = null;
|
|
115
|
+
this.cloudformation = null;
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/**
|
|
@@ -171,6 +185,20 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
171
185
|
return this.secretsManager;
|
|
172
186
|
}
|
|
173
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Get CloudFormation client (lazy loaded)
|
|
190
|
+
*/
|
|
191
|
+
getCloudFormationClient() {
|
|
192
|
+
if (!this.cloudformation) {
|
|
193
|
+
loadCloudFormation();
|
|
194
|
+
this.cloudformation = new CloudFormationClient({
|
|
195
|
+
region: this.region,
|
|
196
|
+
...this.credentials,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return this.cloudformation;
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
getName() {
|
|
175
203
|
return 'aws';
|
|
176
204
|
}
|
|
@@ -440,6 +468,54 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
440
468
|
|
|
441
469
|
return result;
|
|
442
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Describe CloudFormation stack
|
|
474
|
+
*
|
|
475
|
+
* @param {string} stackName - Name of the CloudFormation stack
|
|
476
|
+
* @returns {Promise<Object>} Stack details including outputs
|
|
477
|
+
*/
|
|
478
|
+
async describeStack(stackName) {
|
|
479
|
+
const cf = this.getCloudFormationClient();
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const response = await cf.send(new DescribeStacksCommand({
|
|
483
|
+
StackName: stackName,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
if (!response.Stacks || response.Stacks.length === 0) {
|
|
487
|
+
throw new Error(`Stack ${stackName} not found`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return response.Stacks[0];
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error.message && error.message.includes('does not exist')) {
|
|
493
|
+
throw new Error(`Stack with id ${stackName} does not exist`);
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* List CloudFormation stack resources
|
|
501
|
+
*
|
|
502
|
+
* @param {string} stackName - Name of the CloudFormation stack
|
|
503
|
+
* @returns {Promise<Array>} List of stack resources
|
|
504
|
+
*/
|
|
505
|
+
async listStackResources(stackName) {
|
|
506
|
+
const cf = this.getCloudFormationClient();
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const response = await cf.send(new ListStackResourcesCommand({
|
|
510
|
+
StackName: stackName,
|
|
511
|
+
}));
|
|
512
|
+
|
|
513
|
+
return response.StackResourceSummaries || [];
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn(`Failed to list stack resources for ${stackName}:`, error.message);
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
443
519
|
}
|
|
444
520
|
|
|
445
521
|
module.exports = {
|
|
@@ -168,6 +168,15 @@ describe('AWSProviderAdapter', () => {
|
|
|
168
168
|
expect(client).toBe(provider.secretsManager);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it('should lazy-load CloudFormation client', () => {
|
|
172
|
+
expect(provider.cloudformation).toBeNull();
|
|
173
|
+
|
|
174
|
+
const client = provider.getCloudFormationClient();
|
|
175
|
+
|
|
176
|
+
expect(provider.cloudformation).not.toBeNull();
|
|
177
|
+
expect(client).toBe(provider.cloudformation);
|
|
178
|
+
});
|
|
179
|
+
|
|
171
180
|
it('should reuse client on subsequent calls', () => {
|
|
172
181
|
const client1 = provider.getEC2Client();
|
|
173
182
|
const client2 = provider.getEC2Client();
|
|
@@ -196,7 +205,8 @@ describe('AWSProviderAdapter', () => {
|
|
|
196
205
|
.mockResolvedValueOnce({ SecurityGroups: [] }) // Security Groups
|
|
197
206
|
.mockResolvedValueOnce({ RouteTables: [] }) // Route Tables
|
|
198
207
|
.mockResolvedValueOnce({ NatGateways: [] }) // NAT Gateways
|
|
199
|
-
.mockResolvedValueOnce({ InternetGateways: [] })
|
|
208
|
+
.mockResolvedValueOnce({ InternetGateways: [] }) // Internet Gateways
|
|
209
|
+
.mockResolvedValueOnce({ VpcEndpoints: [] }); // VPC Endpoints
|
|
200
210
|
|
|
201
211
|
provider.getEC2Client = jest.fn().mockReturnValue({ send: mockSend });
|
|
202
212
|
|
|
@@ -345,7 +355,7 @@ describe('AWSProviderAdapter', () => {
|
|
|
345
355
|
send: jest.fn().mockRejectedValue(new Error('SSM API Error')),
|
|
346
356
|
});
|
|
347
357
|
|
|
348
|
-
await expect(provider.discoverParameters({})).rejects.toThrow('Failed to discover AWS parameters');
|
|
358
|
+
await expect(provider.discoverParameters({ parameterPath: '/test' })).rejects.toThrow('Failed to discover AWS parameters');
|
|
349
359
|
});
|
|
350
360
|
|
|
351
361
|
it('should skip secrets when includeSecrets is false', async () => {
|
|
@@ -359,7 +369,8 @@ describe('AWSProviderAdapter', () => {
|
|
|
359
369
|
|
|
360
370
|
expect(result.parameters).toEqual([]);
|
|
361
371
|
expect(result.secrets).toEqual([]);
|
|
362
|
-
|
|
372
|
+
// Behavior-based test: secrets should be empty when includeSecrets is false
|
|
373
|
+
// (Implementation detail: getSecretsManagerClient shouldn't be called)
|
|
363
374
|
});
|
|
364
375
|
});
|
|
365
376
|
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { CloudProviderFactory } = require('./providers/provider-factory');
|
|
13
|
+
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
13
14
|
const { VpcDiscovery } = require('../networking/vpc-discovery');
|
|
14
15
|
const { KmsDiscovery } = require('../security/kms-discovery');
|
|
15
16
|
const { AuroraDiscovery } = require('../database/aurora-discovery');
|
|
@@ -69,14 +70,29 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
69
70
|
// Create provider adapter
|
|
70
71
|
const provider = CloudProviderFactory.create(providerName, region);
|
|
71
72
|
|
|
73
|
+
// Build discovery configuration
|
|
74
|
+
const stage = process.env.SLS_STAGE || 'dev';
|
|
75
|
+
const stackName = `${appDefinition.name || 'create-frigg-app'}-${stage}`;
|
|
76
|
+
|
|
77
|
+
// Try CloudFormation-first discovery
|
|
78
|
+
const cfDiscovery = new CloudFormationDiscovery(provider);
|
|
79
|
+
const stackResources = await cfDiscovery.discoverFromStack(stackName);
|
|
80
|
+
|
|
81
|
+
if (stackResources) {
|
|
82
|
+
console.log(' ✓ Discovered resources from existing CloudFormation stack');
|
|
83
|
+
console.log('✅ Cloud resource discovery completed successfully!');
|
|
84
|
+
return stackResources;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback to AWS API discovery (fresh deployment or stack not found)
|
|
88
|
+
console.log(' ℹ No stack found - running AWS API discovery...');
|
|
89
|
+
|
|
72
90
|
// Create domain discovery services with provider
|
|
73
91
|
const vpcDiscovery = new VpcDiscovery(provider);
|
|
74
92
|
const kmsDiscovery = new KmsDiscovery(provider);
|
|
75
93
|
const auroraDiscovery = new AuroraDiscovery(provider);
|
|
76
94
|
const ssmDiscovery = new SsmDiscovery(provider);
|
|
77
95
|
|
|
78
|
-
// Build discovery configuration
|
|
79
|
-
const stage = process.env.SLS_STAGE || 'dev';
|
|
80
96
|
const config = {
|
|
81
97
|
serviceName: appDefinition.name || 'create-frigg-app',
|
|
82
98
|
stage,
|
|
@@ -139,7 +139,7 @@ describe('Base Definition Factory', () => {
|
|
|
139
139
|
const result = createBaseDefinition({}, {}, {});
|
|
140
140
|
|
|
141
141
|
expect(result.plugins).toContain('serverless-esbuild');
|
|
142
|
-
|
|
142
|
+
// serverless-dotenv-plugin is conditionally loaded only in offline mode
|
|
143
143
|
expect(result.plugins).toContain('serverless-offline-sqs');
|
|
144
144
|
expect(result.plugins).toContain('serverless-offline');
|
|
145
145
|
expect(result.plugins).toContain('@friggframework/serverless-plugin');
|
|
@@ -236,10 +236,12 @@ describe('Base Definition Factory', () => {
|
|
|
236
236
|
expect(result.package.individually).toBe(true);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
it('should enable dotenv', () => {
|
|
239
|
+
it('should enable dotenv only in offline mode', () => {
|
|
240
240
|
const result = createBaseDefinition({}, {}, {});
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
// useDotenv is conditional - only true when process.argv includes 'offline'
|
|
243
|
+
expect(result.useDotenv).toBeDefined();
|
|
244
|
+
expect(typeof result.useDotenv).toBe('boolean');
|
|
243
245
|
});
|
|
244
246
|
});
|
|
245
247
|
});
|
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.
|
|
4
|
+
"version": "2.0.0--canary.461.c89a166.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.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.461.
|
|
14
|
+
"@friggframework/schemas": "2.0.0--canary.461.c89a166.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.461.c89a166.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.
|
|
38
|
-
"@friggframework/prettier-config": "2.0.0--canary.461.
|
|
37
|
+
"@friggframework/eslint-config": "2.0.0--canary.461.c89a166.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.c89a166.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": "
|
|
73
|
+
"gitHead": "c89a166c8477430b9649249d4f2113129bf1acc0"
|
|
74
74
|
}
|