@friggframework/devtools 2.0.0--canary.492.669f13d.0 → 2.0.0--canary.490.b58dfbe.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/networking/vpc-builder.js +58 -1
- package/infrastructure/domains/networking/vpc-resolver.js +57 -10
- package/infrastructure/domains/networking/vpc-resolver.test.js +40 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +156 -4
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +218 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +23 -0
- package/infrastructure/domains/shared/resource-discovery.js +17 -5
- package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
- package/package.json +7 -7
|
@@ -149,7 +149,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
149
149
|
physicalId = flatDiscovery.defaultVpcId;
|
|
150
150
|
} else if (logicalId === 'FriggLambdaSecurityGroup') {
|
|
151
151
|
resourceType = 'AWS::EC2::SecurityGroup';
|
|
152
|
-
physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
152
|
+
physicalId = flatDiscovery.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
153
153
|
} else if (logicalId === 'FriggPrivateSubnet1') {
|
|
154
154
|
resourceType = 'AWS::EC2::Subnet';
|
|
155
155
|
physicalId = flatDiscovery.privateSubnetId1;
|
|
@@ -184,6 +184,11 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
// Also check for external resources extracted via CloudFormation queries
|
|
189
|
+
// (e.g., VPC ID from security group query, subnets from route table associations)
|
|
190
|
+
// These are NOT in the stack but were discovered through stack resources
|
|
191
|
+
this._addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds);
|
|
187
192
|
} else {
|
|
188
193
|
// Resources discovered from AWS API (not CloudFormation)
|
|
189
194
|
// These go into external array
|
|
@@ -290,6 +295,58 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
290
295
|
return discovery;
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Add external resources that were discovered via CloudFormation queries
|
|
300
|
+
* (e.g., VPC ID extracted from security group, subnets from route table associations)
|
|
301
|
+
*
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
_addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds) {
|
|
305
|
+
// VPC ID extracted from SG or route table (NOT a stack resource)
|
|
306
|
+
if (flatDiscovery.defaultVpcId &&
|
|
307
|
+
typeof flatDiscovery.defaultVpcId === 'string' &&
|
|
308
|
+
!existingLogicalIds.includes('FriggVPC')) {
|
|
309
|
+
discovery.external.push({
|
|
310
|
+
physicalId: flatDiscovery.defaultVpcId,
|
|
311
|
+
resourceType: 'AWS::EC2::VPC',
|
|
312
|
+
source: 'cloudformation-query'
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Subnets extracted from route table associations (NOT stack resources)
|
|
317
|
+
if (flatDiscovery.privateSubnetId1 &&
|
|
318
|
+
typeof flatDiscovery.privateSubnetId1 === 'string' &&
|
|
319
|
+
!existingLogicalIds.includes('FriggPrivateSubnet1')) {
|
|
320
|
+
discovery.external.push({
|
|
321
|
+
physicalId: flatDiscovery.privateSubnetId1,
|
|
322
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
323
|
+
source: 'cloudformation-query'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (flatDiscovery.privateSubnetId2 &&
|
|
328
|
+
typeof flatDiscovery.privateSubnetId2 === 'string' &&
|
|
329
|
+
!existingLogicalIds.includes('FriggPrivateSubnet2')) {
|
|
330
|
+
discovery.external.push({
|
|
331
|
+
physicalId: flatDiscovery.privateSubnetId2,
|
|
332
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
333
|
+
source: 'cloudformation-query'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// NAT Gateway extracted from route table routes
|
|
338
|
+
if (flatDiscovery.existingNatGatewayId &&
|
|
339
|
+
typeof flatDiscovery.existingNatGatewayId === 'string' &&
|
|
340
|
+
!existingLogicalIds.includes('FriggNATGateway') &&
|
|
341
|
+
!existingLogicalIds.includes('FriggNatGateway')) {
|
|
342
|
+
discovery.external.push({
|
|
343
|
+
physicalId: flatDiscovery.existingNatGatewayId,
|
|
344
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
345
|
+
source: 'cloudformation-query'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
293
350
|
/**
|
|
294
351
|
* Translate legacy configuration (management modes) to new ownership-based configuration
|
|
295
352
|
* Provides backwards compatibility for existing app definitions
|
|
@@ -23,6 +23,7 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
23
23
|
*/
|
|
24
24
|
resolveVpc(appDefinition, discovery) {
|
|
25
25
|
const userIntent = appDefinition.vpc?.ownership?.vpc || 'auto';
|
|
26
|
+
const vpcManagement = appDefinition.vpc?.management; // Legacy config
|
|
26
27
|
|
|
27
28
|
// Explicit external
|
|
28
29
|
if (userIntent === 'external') {
|
|
@@ -43,20 +44,35 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// Auto-decide
|
|
46
|
-
|
|
47
|
+
const decision = this.resolveResourceOwnership(
|
|
47
48
|
'auto',
|
|
48
49
|
'FriggVPC',
|
|
49
50
|
'AWS::EC2::VPC',
|
|
50
51
|
discovery
|
|
51
52
|
);
|
|
53
|
+
|
|
54
|
+
// CRITICAL: If auto-resolution wants to create a VPC but management mode is 'discover' (or undefined),
|
|
55
|
+
// throw an error instead. Creating a VPC is expensive and should be explicit.
|
|
56
|
+
if (decision.ownership === ResourceOwnership.STACK &&
|
|
57
|
+
!decision.physicalId &&
|
|
58
|
+
vpcManagement !== 'create-new' &&
|
|
59
|
+
userIntent === 'auto') {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'VPC discovery failed: No VPC found. ' +
|
|
62
|
+
'Either set vpc.management to "create-new" or provide vpc.vpcId with vpc.management "use-existing".'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return decision;
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
55
70
|
* Resolve Security Group ownership
|
|
56
71
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
72
|
+
* Logic:
|
|
73
|
+
* - If FriggLambdaSecurityGroup exists in stack → STACK (keep it)
|
|
74
|
+
* - If default SG discovered from VPC → EXTERNAL (use it)
|
|
75
|
+
* - Otherwise → STACK (create FriggLambdaSecurityGroup)
|
|
60
76
|
*
|
|
61
77
|
* @param {Object} appDefinition - App definition
|
|
62
78
|
* @param {Object} discovery - Discovery result
|
|
@@ -65,7 +81,7 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
65
81
|
resolveSecurityGroup(appDefinition, discovery) {
|
|
66
82
|
const userIntent = appDefinition.vpc?.ownership?.securityGroup || 'auto';
|
|
67
83
|
|
|
68
|
-
// Explicit external -
|
|
84
|
+
// Explicit external - use provided SG IDs
|
|
69
85
|
if (userIntent === 'external') {
|
|
70
86
|
this.requireExternalIds(
|
|
71
87
|
appDefinition.vpc?.external?.securityGroupIds,
|
|
@@ -77,21 +93,52 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
77
93
|
);
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
//
|
|
81
|
-
|
|
96
|
+
// Explicit stack - always create FriggLambdaSecurityGroup
|
|
97
|
+
if (userIntent === 'stack') {
|
|
98
|
+
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
99
|
+
return this.createStackDecision(
|
|
100
|
+
inStack?.physicalId,
|
|
101
|
+
inStack
|
|
102
|
+
? 'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
103
|
+
: 'User specified ownership=stack - will create FriggLambdaSecurityGroup'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Auto mode: Check stack first, then check for discovered default SG
|
|
82
108
|
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
83
109
|
|
|
84
110
|
if (inStack) {
|
|
85
111
|
return this.createStackDecision(
|
|
86
112
|
inStack.physicalId,
|
|
87
|
-
'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
113
|
+
'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Also check flat discovery for lambdaSecurityGroupId (from CloudFormation extraction)
|
|
118
|
+
const structured = discovery._structured || discovery;
|
|
119
|
+
const lambdaSgId = structured.lambdaSecurityGroupId || discovery.lambdaSecurityGroupId;
|
|
120
|
+
|
|
121
|
+
if (lambdaSgId) {
|
|
122
|
+
return this.createStackDecision(
|
|
123
|
+
lambdaSgId,
|
|
124
|
+
'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for discovered default security group (from external VPC pattern)
|
|
129
|
+
const defaultSgId = structured.defaultSecurityGroupId || discovery.defaultSecurityGroupId;
|
|
130
|
+
|
|
131
|
+
if (defaultSgId) {
|
|
132
|
+
return this.createExternalDecision(
|
|
133
|
+
[defaultSgId],
|
|
134
|
+
'Found default security group via discovery - will reuse (matches canary behavior)'
|
|
88
135
|
);
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
//
|
|
138
|
+
// No SG found anywhere - create new FriggLambdaSecurityGroup
|
|
92
139
|
return this.createStackDecision(
|
|
93
140
|
null,
|
|
94
|
-
'No
|
|
141
|
+
'No security group found - will create FriggLambdaSecurityGroup in stack'
|
|
95
142
|
);
|
|
96
143
|
}
|
|
97
144
|
|
|
@@ -109,6 +109,46 @@ describe('VpcResourceResolver', () => {
|
|
|
109
109
|
expect(decision.physicalId).toBeUndefined();
|
|
110
110
|
expect(decision.reason).toContain('No existing resource found');
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it('should throw error when auto mode finds no VPC and management is not create-new', () => {
|
|
114
|
+
const appDefinition = {
|
|
115
|
+
vpc: {
|
|
116
|
+
enable: true,
|
|
117
|
+
// No management specified - defaults to discover
|
|
118
|
+
// No ownership specified - defaults to auto
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const discovery = {
|
|
122
|
+
stackManaged: [],
|
|
123
|
+
external: [],
|
|
124
|
+
fromCloudFormation: false
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Should throw error instead of trying to create VPC
|
|
128
|
+
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
129
|
+
'VPC discovery failed: No VPC found'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should allow creating VPC when management is create-new', () => {
|
|
134
|
+
const appDefinition = {
|
|
135
|
+
vpc: {
|
|
136
|
+
enable: true,
|
|
137
|
+
management: 'create-new'
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const discovery = {
|
|
141
|
+
stackManaged: [],
|
|
142
|
+
external: [],
|
|
143
|
+
fromCloudFormation: false
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Should NOT throw - create-new explicitly allows VPC creation
|
|
147
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
148
|
+
|
|
149
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
150
|
+
expect(decision.physicalId).toBeNull();
|
|
151
|
+
});
|
|
112
152
|
});
|
|
113
153
|
|
|
114
154
|
describe('resolveSecurityGroup', () => {
|
|
@@ -28,6 +28,9 @@ class CloudFormationDiscovery {
|
|
|
28
28
|
*/
|
|
29
29
|
async discoverFromStack(stackName) {
|
|
30
30
|
try {
|
|
31
|
+
// Store stack name for use in helper methods
|
|
32
|
+
this.currentStackName = stackName;
|
|
33
|
+
|
|
31
34
|
// Try to get the stack
|
|
32
35
|
const stack = await this.provider.describeStack(stackName);
|
|
33
36
|
|
|
@@ -111,6 +114,93 @@ class CloudFormationDiscovery {
|
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Extract external resource references from stack resource properties
|
|
119
|
+
*
|
|
120
|
+
* When VPC/subnets/NAT are external, they're referenced in routing resources' properties.
|
|
121
|
+
* We query EC2 to get the actual VPC ID, NAT Gateway ID, and subnet IDs from the route table.
|
|
122
|
+
*
|
|
123
|
+
* @private
|
|
124
|
+
* @param {Array} resources - CloudFormation stack resources
|
|
125
|
+
* @param {Object} discovered - Object to populate with discovered resources
|
|
126
|
+
*/
|
|
127
|
+
async _extractExternalReferencesFromStackResources(resources, discovered) {
|
|
128
|
+
if (!this.provider || !this.provider.getEC2Client) {
|
|
129
|
+
console.log(' ℹ Skipping external reference extraction (EC2 client not available)');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// If we found a route table in the stack, query EC2 for its details
|
|
135
|
+
// This gives us VPC ID, NAT Gateway ID, and subnet IDs
|
|
136
|
+
if (discovered.routeTableId) {
|
|
137
|
+
try {
|
|
138
|
+
console.log(` ℹ Querying route table ${discovered.routeTableId} for external references...`);
|
|
139
|
+
const { DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
|
|
140
|
+
const ec2 = this.provider.getEC2Client();
|
|
141
|
+
const rtResponse = await ec2.send(new DescribeRouteTablesCommand({
|
|
142
|
+
RouteTableIds: [discovered.routeTableId]
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
if (rtResponse.RouteTables && rtResponse.RouteTables.length > 0) {
|
|
146
|
+
const routeTable = rtResponse.RouteTables[0];
|
|
147
|
+
|
|
148
|
+
// Extract VPC ID
|
|
149
|
+
if (routeTable.VpcId && !discovered.defaultVpcId) {
|
|
150
|
+
discovered.defaultVpcId = routeTable.VpcId;
|
|
151
|
+
console.log(` ✓ Extracted VPC ID from route table: ${routeTable.VpcId}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Extract NAT Gateway ID from routes
|
|
155
|
+
const natRoute = routeTable.Routes?.find(r => r.NatGatewayId);
|
|
156
|
+
if (natRoute && natRoute.NatGatewayId && !discovered.natGatewayId) {
|
|
157
|
+
discovered.natGatewayId = natRoute.NatGatewayId;
|
|
158
|
+
discovered.existingNatGatewayId = natRoute.NatGatewayId;
|
|
159
|
+
console.log(` ✓ Extracted NAT Gateway ID from routes: ${natRoute.NatGatewayId}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Extract subnet IDs from route table associations
|
|
163
|
+
const associations = routeTable.Associations || [];
|
|
164
|
+
const subnetAssociations = associations.filter(a => a.SubnetId);
|
|
165
|
+
|
|
166
|
+
if (subnetAssociations.length >= 1 && !discovered.privateSubnetId1) {
|
|
167
|
+
discovered.privateSubnetId1 = subnetAssociations[0].SubnetId;
|
|
168
|
+
console.log(` ✓ Extracted private subnet 1 from associations: ${subnetAssociations[0].SubnetId}`);
|
|
169
|
+
}
|
|
170
|
+
if (subnetAssociations.length >= 2 && !discovered.privateSubnetId2) {
|
|
171
|
+
discovered.privateSubnetId2 = subnetAssociations[1].SubnetId;
|
|
172
|
+
console.log(` ✓ Extracted private subnet 2 from associations: ${subnetAssociations[1].SubnetId}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Query for default security group in the VPC (matches canary behavior)
|
|
176
|
+
if (routeTable.VpcId && !discovered.defaultSecurityGroupId) {
|
|
177
|
+
try {
|
|
178
|
+
const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
|
|
179
|
+
const sgResponse = await ec2.send(new DescribeSecurityGroupsCommand({
|
|
180
|
+
Filters: [
|
|
181
|
+
{ Name: 'vpc-id', Values: [routeTable.VpcId] },
|
|
182
|
+
{ Name: 'group-name', Values: ['default'] }
|
|
183
|
+
]
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
if (sgResponse.SecurityGroups && sgResponse.SecurityGroups.length > 0) {
|
|
187
|
+
discovered.defaultSecurityGroupId = sgResponse.SecurityGroups[0].GroupId;
|
|
188
|
+
console.log(` ✓ Extracted default security group: ${discovered.defaultSecurityGroupId}`);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn(` ⚠️ Could not query default security group: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.warn(` ⚠️ Could not query route table for external references: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn(` ⚠️ Error extracting external references: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
114
204
|
/**
|
|
115
205
|
* Extract discovered resources from CloudFormation stack resources
|
|
116
206
|
*
|
|
@@ -216,6 +306,30 @@ class CloudFormationDiscovery {
|
|
|
216
306
|
discovered.natGatewayId = PhysicalResourceId;
|
|
217
307
|
}
|
|
218
308
|
|
|
309
|
+
// Route Table (Lambda route table for external VPC pattern)
|
|
310
|
+
if (LogicalResourceId === 'FriggLambdaRouteTable' && ResourceType === 'AWS::EC2::RouteTable') {
|
|
311
|
+
discovered.routeTableId = PhysicalResourceId;
|
|
312
|
+
discovered.privateRouteTableId = PhysicalResourceId;
|
|
313
|
+
console.log(` ✓ Found route table in stack: ${PhysicalResourceId}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// NAT Route (proves NAT configuration exists) - support both naming patterns
|
|
317
|
+
if ((LogicalResourceId === 'FriggNATRoute' || LogicalResourceId === 'FriggPrivateRoute') &&
|
|
318
|
+
ResourceType === 'AWS::EC2::Route') {
|
|
319
|
+
discovered.natRoute = PhysicalResourceId;
|
|
320
|
+
console.log(` ✓ Found NAT route in stack: ${LogicalResourceId}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Route Table Associations (links subnets to route table)
|
|
324
|
+
if (LogicalResourceId.includes('RouteAssociation') &&
|
|
325
|
+
ResourceType === 'AWS::EC2::SubnetRouteTableAssociation') {
|
|
326
|
+
if (!discovered.routeTableAssociations) {
|
|
327
|
+
discovered.routeTableAssociations = [];
|
|
328
|
+
}
|
|
329
|
+
discovered.routeTableAssociations.push(PhysicalResourceId);
|
|
330
|
+
console.log(` ✓ Found route table association: ${LogicalResourceId}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
219
333
|
// VPC - direct extraction (primary method)
|
|
220
334
|
if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
|
|
221
335
|
discovered.defaultVpcId = PhysicalResourceId;
|
|
@@ -276,26 +390,64 @@ class CloudFormationDiscovery {
|
|
|
276
390
|
// VPC Endpoint Security Group
|
|
277
391
|
if (LogicalResourceId === 'FriggVPCEndpointSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
|
|
278
392
|
discovered.vpcEndpointSecurityGroupId = PhysicalResourceId;
|
|
393
|
+
console.log(` ✓ Found VPC endpoint security group in stack: ${PhysicalResourceId}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Lambda Security Group (if created in stack)
|
|
397
|
+
if (LogicalResourceId === 'FriggLambdaSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
|
|
398
|
+
discovered.lambdaSecurityGroupId = PhysicalResourceId;
|
|
399
|
+
// Also set as defaultSecurityGroupId so converter recognizes it
|
|
400
|
+
discovered.defaultSecurityGroupId = PhysicalResourceId;
|
|
401
|
+
console.log(` ✓ Found Lambda security group in stack: ${PhysicalResourceId}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// VPC Endpoints - support both old and new naming conventions
|
|
405
|
+
// Initialize vpcEndpoints object for structured access
|
|
406
|
+
if (!discovered.vpcEndpoints) {
|
|
407
|
+
discovered.vpcEndpoints = {};
|
|
279
408
|
}
|
|
280
409
|
|
|
281
|
-
//
|
|
282
|
-
if (LogicalResourceId === 'FriggS3VPCEndpoint'
|
|
410
|
+
// S3 Endpoint (both naming patterns)
|
|
411
|
+
if ((LogicalResourceId === 'FriggS3VPCEndpoint' || LogicalResourceId === 'VPCEndpointS3') &&
|
|
412
|
+
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
283
413
|
discovered.s3VpcEndpointId = PhysicalResourceId;
|
|
414
|
+
discovered.vpcEndpoints.s3 = PhysicalResourceId;
|
|
415
|
+
console.log(` ✓ Found S3 VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
284
416
|
}
|
|
285
|
-
|
|
417
|
+
|
|
418
|
+
// DynamoDB Endpoint (both naming patterns)
|
|
419
|
+
if ((LogicalResourceId === 'FriggDynamoDBVPCEndpoint' || LogicalResourceId === 'VPCEndpointDynamoDB') &&
|
|
420
|
+
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
286
421
|
discovered.dynamoDbVpcEndpointId = PhysicalResourceId;
|
|
422
|
+
discovered.vpcEndpoints.dynamodb = PhysicalResourceId;
|
|
423
|
+
console.log(` ✓ Found DynamoDB VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
287
424
|
}
|
|
288
|
-
|
|
425
|
+
|
|
426
|
+
// KMS Endpoint (both naming patterns)
|
|
427
|
+
if ((LogicalResourceId === 'FriggKMSVPCEndpoint' || LogicalResourceId === 'VPCEndpointKMS') &&
|
|
428
|
+
ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
289
429
|
discovered.kmsVpcEndpointId = PhysicalResourceId;
|
|
430
|
+
discovered.vpcEndpoints.kms = PhysicalResourceId;
|
|
431
|
+
console.log(` ✓ Found KMS VPC endpoint in stack: ${PhysicalResourceId}`);
|
|
290
432
|
}
|
|
433
|
+
|
|
434
|
+
// Secrets Manager Endpoint
|
|
291
435
|
if (LogicalResourceId === 'FriggSecretsManagerVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
292
436
|
discovered.secretsManagerVpcEndpointId = PhysicalResourceId;
|
|
437
|
+
discovered.vpcEndpoints.secretsManager = PhysicalResourceId;
|
|
293
438
|
}
|
|
439
|
+
|
|
440
|
+
// SQS Endpoint
|
|
294
441
|
if (LogicalResourceId === 'FriggSQSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
|
|
295
442
|
discovered.sqsVpcEndpointId = PhysicalResourceId;
|
|
443
|
+
discovered.vpcEndpoints.sqs = PhysicalResourceId;
|
|
296
444
|
}
|
|
297
445
|
}
|
|
298
446
|
|
|
447
|
+
// Extract VPC ID and other external references from routing resource properties
|
|
448
|
+
// This handles the pattern where VPC is external but routing is in the stack
|
|
449
|
+
await this._extractExternalReferencesFromStackResources(resources, discovered);
|
|
450
|
+
|
|
299
451
|
// If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
|
|
300
452
|
if (discovered.defaultVpcId && this.provider &&
|
|
301
453
|
!discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
|
|
@@ -586,5 +586,223 @@ describe('CloudFormationDiscovery', () => {
|
|
|
586
586
|
expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
|
|
587
587
|
});
|
|
588
588
|
});
|
|
589
|
+
|
|
590
|
+
describe('External VPC with routing infrastructure pattern', () => {
|
|
591
|
+
it('should discover routing resources when VPC is external', async () => {
|
|
592
|
+
// This tests the Frontify pattern: external VPC/subnets/KMS,
|
|
593
|
+
// but stack creates routing infrastructure (route table, NAT route, VPC endpoints)
|
|
594
|
+
const mockStack = {
|
|
595
|
+
StackName: 'create-frigg-app-production',
|
|
596
|
+
Outputs: [],
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const mockResources = [
|
|
600
|
+
{
|
|
601
|
+
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
602
|
+
PhysicalResourceId: 'rtb-0b83aca77ccde20a6',
|
|
603
|
+
ResourceType: 'AWS::EC2::RouteTable',
|
|
604
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
LogicalResourceId: 'FriggNATRoute',
|
|
608
|
+
PhysicalResourceId: 'rtb-0b83aca77ccde20a6|0.0.0.0/0',
|
|
609
|
+
ResourceType: 'AWS::EC2::Route',
|
|
610
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
LogicalResourceId: 'FriggSubnet1RouteAssociation',
|
|
614
|
+
PhysicalResourceId: 'rtbassoc-07245da0b447ca469',
|
|
615
|
+
ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
616
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
LogicalResourceId: 'FriggSubnet2RouteAssociation',
|
|
620
|
+
PhysicalResourceId: 'rtbassoc-0806f9783c4ea181f',
|
|
621
|
+
ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
622
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
LogicalResourceId: 'VPCEndpointS3',
|
|
626
|
+
PhysicalResourceId: 'vpce-0352ceac2124c14be',
|
|
627
|
+
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
628
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
LogicalResourceId: 'VPCEndpointDynamoDB',
|
|
632
|
+
PhysicalResourceId: 'vpce-0b06c4f631199ea68',
|
|
633
|
+
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
634
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
635
|
+
},
|
|
636
|
+
];
|
|
637
|
+
|
|
638
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
639
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
640
|
+
|
|
641
|
+
const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
|
|
642
|
+
|
|
643
|
+
// Verify routing infrastructure was discovered
|
|
644
|
+
expect(result.routeTableId).toBe('rtb-0b83aca77ccde20a6');
|
|
645
|
+
expect(result.privateRouteTableId).toBe('rtb-0b83aca77ccde20a6');
|
|
646
|
+
expect(result.natRoute).toBe('rtb-0b83aca77ccde20a6|0.0.0.0/0');
|
|
647
|
+
expect(result.routeTableAssociations).toEqual([
|
|
648
|
+
'rtbassoc-07245da0b447ca469',
|
|
649
|
+
'rtbassoc-0806f9783c4ea181f',
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
// Verify VPC endpoints were discovered (both naming conventions)
|
|
653
|
+
expect(result.vpcEndpoints).toBeDefined();
|
|
654
|
+
expect(result.vpcEndpoints.s3).toBe('vpce-0352ceac2124c14be');
|
|
655
|
+
expect(result.vpcEndpoints.dynamodb).toBe('vpce-0b06c4f631199ea68');
|
|
656
|
+
expect(result.s3VpcEndpointId).toBe('vpce-0352ceac2124c14be');
|
|
657
|
+
expect(result.dynamoDbVpcEndpointId).toBe('vpce-0b06c4f631199ea68');
|
|
658
|
+
|
|
659
|
+
// Verify NO VPC/KMS resources (they're external)
|
|
660
|
+
expect(result.defaultVpcId).toBeUndefined();
|
|
661
|
+
expect(result.defaultKmsKeyId).toBeUndefined();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should work with legacy VPC endpoint naming (FriggS3VPCEndpoint)', async () => {
|
|
665
|
+
const mockStack = {
|
|
666
|
+
StackName: 'test-stack',
|
|
667
|
+
Outputs: [],
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const mockResources = [
|
|
671
|
+
{
|
|
672
|
+
LogicalResourceId: 'FriggS3VPCEndpoint',
|
|
673
|
+
PhysicalResourceId: 'vpce-legacy-s3',
|
|
674
|
+
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
675
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
LogicalResourceId: 'FriggDynamoDBVPCEndpoint',
|
|
679
|
+
PhysicalResourceId: 'vpce-legacy-ddb',
|
|
680
|
+
ResourceType: 'AWS::EC2::VPCEndpoint',
|
|
681
|
+
ResourceStatus: 'CREATE_COMPLETE',
|
|
682
|
+
},
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
686
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
687
|
+
|
|
688
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
689
|
+
|
|
690
|
+
// Both naming conventions should work
|
|
691
|
+
expect(result.vpcEndpoints.s3).toBe('vpce-legacy-s3');
|
|
692
|
+
expect(result.vpcEndpoints.dynamodb).toBe('vpce-legacy-ddb');
|
|
693
|
+
expect(result.s3VpcEndpointId).toBe('vpce-legacy-s3');
|
|
694
|
+
expect(result.dynamoDbVpcEndpointId).toBe('vpce-legacy-ddb');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should extract FriggLambdaSecurityGroup from stack', async () => {
|
|
698
|
+
const mockStack = {
|
|
699
|
+
StackName: 'test-stack',
|
|
700
|
+
Outputs: [],
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const mockResources = [
|
|
704
|
+
{
|
|
705
|
+
LogicalResourceId: 'FriggLambdaSecurityGroup',
|
|
706
|
+
PhysicalResourceId: 'sg-01002240c6a446202',
|
|
707
|
+
ResourceType: 'AWS::EC2::SecurityGroup',
|
|
708
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
712
|
+
PhysicalResourceId: 'rtb-08af43bbf0775602d',
|
|
713
|
+
ResourceType: 'AWS::EC2::RouteTable',
|
|
714
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
719
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
720
|
+
|
|
721
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
722
|
+
|
|
723
|
+
// Lambda security group should be extracted
|
|
724
|
+
expect(result.lambdaSecurityGroupId).toBe('sg-01002240c6a446202');
|
|
725
|
+
expect(result.defaultSecurityGroupId).toBe('sg-01002240c6a446202');
|
|
726
|
+
expect(result.existingLogicalIds).toContain('FriggLambdaSecurityGroup');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('should support FriggPrivateRoute naming for NAT routes', async () => {
|
|
730
|
+
const mockStack = {
|
|
731
|
+
StackName: 'test-stack',
|
|
732
|
+
Outputs: [],
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const mockResources = [
|
|
736
|
+
{
|
|
737
|
+
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
738
|
+
PhysicalResourceId: 'rtb-123',
|
|
739
|
+
ResourceType: 'AWS::EC2::RouteTable',
|
|
740
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
LogicalResourceId: 'FriggPrivateRoute',
|
|
744
|
+
PhysicalResourceId: 'rtb-123|0.0.0.0/0',
|
|
745
|
+
ResourceType: 'AWS::EC2::Route',
|
|
746
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
747
|
+
},
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
751
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
752
|
+
|
|
753
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
754
|
+
|
|
755
|
+
// Both FriggNATRoute and FriggPrivateRoute should be recognized
|
|
756
|
+
expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
|
|
757
|
+
expect(result.routeTableId).toBe('rtb-123');
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('should extract external references from route table without stackName error', async () => {
|
|
761
|
+
const mockStack = {
|
|
762
|
+
StackName: 'test-stack',
|
|
763
|
+
Outputs: [],
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const mockResources = [
|
|
767
|
+
{
|
|
768
|
+
LogicalResourceId: 'FriggLambdaRouteTable',
|
|
769
|
+
PhysicalResourceId: 'rtb-real-id',
|
|
770
|
+
ResourceType: 'AWS::EC2::RouteTable',
|
|
771
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
772
|
+
},
|
|
773
|
+
];
|
|
774
|
+
|
|
775
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
776
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
777
|
+
|
|
778
|
+
// Mock EC2 DescribeRouteTables to return route table with VPC info
|
|
779
|
+
mockProvider.getEC2Client = jest.fn().mockReturnValue({
|
|
780
|
+
send: jest.fn().mockResolvedValue({
|
|
781
|
+
RouteTables: [{
|
|
782
|
+
RouteTableId: 'rtb-real-id',
|
|
783
|
+
VpcId: 'vpc-extracted',
|
|
784
|
+
Routes: [
|
|
785
|
+
{ NatGatewayId: 'nat-extracted', DestinationCidrBlock: '0.0.0.0/0' }
|
|
786
|
+
],
|
|
787
|
+
Associations: [
|
|
788
|
+
{ SubnetId: 'subnet-1' },
|
|
789
|
+
{ SubnetId: 'subnet-2' }
|
|
790
|
+
]
|
|
791
|
+
}]
|
|
792
|
+
})
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
796
|
+
|
|
797
|
+
// Should extract VPC, NAT, and subnets from route table
|
|
798
|
+
expect(result.defaultVpcId).toBe('vpc-extracted');
|
|
799
|
+
expect(result.existingNatGatewayId).toBe('nat-extracted');
|
|
800
|
+
expect(result.privateSubnetId1).toBe('subnet-1');
|
|
801
|
+
expect(result.privateSubnetId2).toBe('subnet-2');
|
|
802
|
+
|
|
803
|
+
// Should NOT throw 'stackName is not defined' error
|
|
804
|
+
expect(result).toBeDefined();
|
|
805
|
+
});
|
|
806
|
+
});
|
|
589
807
|
});
|
|
590
808
|
|
|
@@ -536,6 +536,29 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
536
536
|
return [];
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Describe a specific stack resource to get its full details including properties
|
|
542
|
+
* @param {string} stackName - Stack name
|
|
543
|
+
* @param {string} logicalResourceId - Logical resource ID
|
|
544
|
+
* @returns {Promise<Object>} Resource details
|
|
545
|
+
*/
|
|
546
|
+
async describeStackResource(stackName, logicalResourceId) {
|
|
547
|
+
const cf = this.getCloudFormationClient();
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const { DescribeStackResourceCommand } = require('@aws-sdk/client-cloudformation');
|
|
551
|
+
const response = await cf.send(new DescribeStackResourceCommand({
|
|
552
|
+
StackName: stackName,
|
|
553
|
+
LogicalResourceId: logicalResourceId,
|
|
554
|
+
}));
|
|
555
|
+
|
|
556
|
+
return response.StackResourceDetail || null;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
console.warn(`Failed to describe stack resource ${logicalResourceId}:`, error.message);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
539
562
|
}
|
|
540
563
|
|
|
541
564
|
module.exports = {
|
|
@@ -95,11 +95,23 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
95
95
|
const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
|
|
96
96
|
const stackResources = await cfDiscovery.discoverFromStack(stackName);
|
|
97
97
|
|
|
98
|
-
// Validate CF discovery results -
|
|
99
|
-
const hasVpcData = stackResources?.defaultVpcId;
|
|
100
|
-
const hasKmsData = stackResources?.defaultKmsKeyId;
|
|
101
|
-
const hasAuroraData = stackResources?.auroraClusterId;
|
|
102
|
-
|
|
98
|
+
// Validate CF discovery results - check for ANY useful infrastructure
|
|
99
|
+
const hasVpcData = stackResources?.defaultVpcId; // VPC resource in stack
|
|
100
|
+
const hasKmsData = stackResources?.defaultKmsKeyId; // KMS resource in stack
|
|
101
|
+
const hasAuroraData = stackResources?.auroraClusterId; // Aurora in stack
|
|
102
|
+
|
|
103
|
+
// Check for routing infrastructure (proves VPC config exists even with external VPC)
|
|
104
|
+
const hasRoutingInfra = stackResources?.routeTableId || // FriggLambdaRouteTable
|
|
105
|
+
stackResources?.natRoute || // FriggNATRoute
|
|
106
|
+
stackResources?.vpcEndpoints?.s3 || // VPC endpoints
|
|
107
|
+
stackResources?.vpcEndpoints?.dynamodb;
|
|
108
|
+
|
|
109
|
+
// Stack is useful if it has EITHER actual resources OR routing infrastructure
|
|
110
|
+
const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData || hasRoutingInfra;
|
|
111
|
+
|
|
112
|
+
if (hasRoutingInfra && !hasVpcData) {
|
|
113
|
+
console.log(' ✓ Found VPC routing infrastructure in stack (external VPC pattern)');
|
|
114
|
+
}
|
|
103
115
|
|
|
104
116
|
// Check if we're in isolated mode (each stage gets its own VPC/Aurora)
|
|
105
117
|
const isIsolatedMode = appDefinition.managementMode === 'managed' &&
|
|
@@ -415,6 +415,42 @@ describe('Resource Discovery', () => {
|
|
|
415
415
|
delete process.env.SLS_STAGE;
|
|
416
416
|
});
|
|
417
417
|
|
|
418
|
+
it('should recognize routing infrastructure as useful data', async () => {
|
|
419
|
+
const appDefinition = {
|
|
420
|
+
name: 'test-app',
|
|
421
|
+
vpc: { enable: true },
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
process.env.SLS_STAGE = 'production';
|
|
425
|
+
|
|
426
|
+
// Mock CloudFormation discovery to return routing infrastructure but no VPC resource
|
|
427
|
+
const mockCloudFormationDiscovery = {
|
|
428
|
+
discoverFromStack: jest.fn().mockResolvedValue({
|
|
429
|
+
fromCloudFormationStack: true,
|
|
430
|
+
routeTableId: 'rtb-123',
|
|
431
|
+
natRoute: 'rtb-123|0.0.0.0/0',
|
|
432
|
+
vpcEndpoints: {
|
|
433
|
+
s3: 'vpce-s3',
|
|
434
|
+
dynamodb: 'vpce-ddb'
|
|
435
|
+
},
|
|
436
|
+
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggNATRoute']
|
|
437
|
+
// NO defaultVpcId, NO defaultKmsKeyId, NO auroraClusterId
|
|
438
|
+
})
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
442
|
+
CloudFormationDiscovery.mockImplementation(() => mockCloudFormationDiscovery);
|
|
443
|
+
|
|
444
|
+
const result = await gatherDiscoveredResources(appDefinition);
|
|
445
|
+
|
|
446
|
+
// Should use CloudFormation data without falling back to AWS API
|
|
447
|
+
expect(result.routeTableId).toBe('rtb-123');
|
|
448
|
+
expect(result.vpcEndpoints.s3).toBe('vpce-s3');
|
|
449
|
+
|
|
450
|
+
// Should NOT call AWS API discovery
|
|
451
|
+
expect(mockVpcDiscovery.discover).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
|
|
418
454
|
it('should include secrets in SSM discovery by default', async () => {
|
|
419
455
|
const appDefinition = {
|
|
420
456
|
ssm: { enable: true },
|
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.
|
|
4
|
+
"version": "2.0.0--canary.490.b58dfbe.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"@babel/eslint-parser": "^7.18.9",
|
|
17
17
|
"@babel/parser": "^7.25.3",
|
|
18
18
|
"@babel/traverse": "^7.25.3",
|
|
19
|
-
"@friggframework/core": "2.0.0--canary.
|
|
20
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
21
|
-
"@friggframework/test": "2.0.0--canary.
|
|
19
|
+
"@friggframework/core": "2.0.0--canary.490.b58dfbe.0",
|
|
20
|
+
"@friggframework/schemas": "2.0.0--canary.490.b58dfbe.0",
|
|
21
|
+
"@friggframework/test": "2.0.0--canary.490.b58dfbe.0",
|
|
22
22
|
"@hapi/boom": "^10.0.1",
|
|
23
23
|
"@inquirer/prompts": "^5.3.8",
|
|
24
24
|
"axios": "^1.7.2",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"validate-npm-package-name": "^5.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
49
|
+
"@friggframework/eslint-config": "2.0.0--canary.490.b58dfbe.0",
|
|
50
|
+
"@friggframework/prettier-config": "2.0.0--canary.490.b58dfbe.0",
|
|
51
51
|
"aws-sdk-client-mock": "^4.1.0",
|
|
52
52
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
53
53
|
"jest": "^30.1.3",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "b58dfbe46be9442b592f4a92fb98a4340ec2d146"
|
|
83
83
|
}
|