@friggframework/devtools 2.0.0--canary.490.b61914a.0 → 2.0.0--canary.490.581e175.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.
@@ -27,10 +27,31 @@ class VpcResourceResolver extends BaseResourceResolver {
27
27
 
28
28
  // Explicit external
29
29
  if (userIntent === 'external') {
30
- this.requireExternalIds(appDefinition.vpc?.external?.vpcId, 'vpcId');
31
- return this.createExternalDecision(
32
- appDefinition.vpc.external.vpcId,
33
- 'User specified ownership=external for VPC'
30
+ const externalVpcId = appDefinition.vpc?.external?.vpcId;
31
+
32
+ // If hardcoded ID provided, use it
33
+ if (externalVpcId) {
34
+ return this.createExternalDecision(
35
+ externalVpcId,
36
+ 'User specified ownership=external with hardcoded vpcId'
37
+ );
38
+ }
39
+
40
+ // No hardcoded ID - try discovery
41
+ const discoveredVpcId = discovery.defaultVpcId;
42
+
43
+ if (discoveredVpcId) {
44
+ return this.createExternalDecision(
45
+ discoveredVpcId,
46
+ 'User specified ownership=external - using discovered VPC'
47
+ );
48
+ }
49
+
50
+ // Discovery found nothing - error
51
+ throw new Error(
52
+ "ownership='external' for VPC requires either:\n" +
53
+ " 1. Hardcoded external.vpcId, OR\n" +
54
+ " 2. A VPC discovered via AWS discovery"
34
55
  );
35
56
  }
36
57
 
@@ -81,15 +102,34 @@ class VpcResourceResolver extends BaseResourceResolver {
81
102
  resolveSecurityGroup(appDefinition, discovery) {
82
103
  const userIntent = appDefinition.vpc?.ownership?.securityGroup || 'auto';
83
104
 
84
- // Explicit external - use provided SG IDs
105
+ // Explicit external
85
106
  if (userIntent === 'external') {
86
- this.requireExternalIds(
87
- appDefinition.vpc?.external?.securityGroupIds,
88
- 'securityGroupIds'
89
- );
90
- return this.createExternalDecision(
91
- appDefinition.vpc.external.securityGroupIds,
92
- 'User specified ownership=external for security group'
107
+ const externalIds = appDefinition.vpc?.external?.securityGroupIds;
108
+
109
+ // If hardcoded IDs provided, use those
110
+ if (externalIds && externalIds.length > 0) {
111
+ return this.createExternalDecision(
112
+ externalIds,
113
+ 'User specified ownership=external with hardcoded securityGroupIds'
114
+ );
115
+ }
116
+
117
+ // No hardcoded IDs - try discovery
118
+ const structured = discovery._structured || discovery;
119
+ const defaultSgId = structured.defaultSecurityGroupId || discovery.defaultSecurityGroupId;
120
+
121
+ if (defaultSgId) {
122
+ return this.createExternalDecision(
123
+ [defaultSgId],
124
+ 'User specified ownership=external - using discovered default security group'
125
+ );
126
+ }
127
+
128
+ // Discovery found nothing - error
129
+ throw new Error(
130
+ "ownership='external' for securityGroup requires either:\n" +
131
+ " 1. Hardcoded external.securityGroupIds array, OR\n" +
132
+ " 2. A default security group discovered via AWS discovery"
93
133
  );
94
134
  }
95
135
 
@@ -153,13 +193,32 @@ class VpcResourceResolver extends BaseResourceResolver {
153
193
 
154
194
  // Explicit external
155
195
  if (userIntent === 'external') {
156
- this.requireExternalIds(
157
- appDefinition.vpc?.external?.subnetIds,
158
- 'subnetIds'
159
- );
160
- return this.createExternalDecision(
161
- appDefinition.vpc.external.subnetIds,
162
- 'User specified ownership=external for subnets'
196
+ const externalSubnetIds = appDefinition.vpc?.external?.subnetIds;
197
+
198
+ // If hardcoded IDs provided, use those
199
+ if (externalSubnetIds && externalSubnetIds.length >= 2) {
200
+ return this.createExternalDecision(
201
+ externalSubnetIds,
202
+ 'User specified ownership=external with hardcoded subnetIds'
203
+ );
204
+ }
205
+
206
+ // No hardcoded IDs - try discovery
207
+ const discoveredSubnet1 = discovery.privateSubnetId1;
208
+ const discoveredSubnet2 = discovery.privateSubnetId2;
209
+
210
+ if (discoveredSubnet1 && discoveredSubnet2) {
211
+ return this.createExternalDecision(
212
+ [discoveredSubnet1, discoveredSubnet2],
213
+ 'User specified ownership=external - using discovered subnets'
214
+ );
215
+ }
216
+
217
+ // Discovery found nothing - error
218
+ throw new Error(
219
+ "ownership='external' for subnets requires either:\n" +
220
+ " 1. Hardcoded external.subnetIds array (minimum 2), OR\n" +
221
+ " 2. At least 2 subnets discovered via AWS discovery"
163
222
  );
164
223
  }
165
224
 
@@ -247,13 +306,31 @@ class VpcResourceResolver extends BaseResourceResolver {
247
306
 
248
307
  // Explicit external
249
308
  if (userIntent === 'external') {
250
- this.requireExternalIds(
251
- appDefinition.vpc?.external?.natGatewayId,
252
- 'natGatewayId'
253
- );
254
- return this.createExternalDecision(
255
- appDefinition.vpc.external.natGatewayId,
256
- 'User specified ownership=external for NAT gateway'
309
+ const externalNatId = appDefinition.vpc?.external?.natGatewayId;
310
+
311
+ // If hardcoded ID provided, use it
312
+ if (externalNatId) {
313
+ return this.createExternalDecision(
314
+ externalNatId,
315
+ 'User specified ownership=external with hardcoded natGatewayId'
316
+ );
317
+ }
318
+
319
+ // No hardcoded ID - try discovery
320
+ const discoveredNatId = discovery.natGatewayId;
321
+
322
+ if (discoveredNatId) {
323
+ return this.createExternalDecision(
324
+ discoveredNatId,
325
+ 'User specified ownership=external - using discovered NAT gateway'
326
+ );
327
+ }
328
+
329
+ // Discovery found nothing - error
330
+ throw new Error(
331
+ "ownership='external' for NAT gateway requires either:\n" +
332
+ " 1. Hardcoded external.natGatewayId, OR\n" +
333
+ " 2. A NAT gateway discovered via AWS discovery"
257
334
  );
258
335
  }
259
336
 
@@ -9,7 +9,7 @@ describe('VpcResourceResolver', () => {
9
9
  });
10
10
 
11
11
  describe('resolveVpc', () => {
12
- it('should resolve to EXTERNAL when user specifies external', () => {
12
+ it('should resolve to EXTERNAL with hardcoded vpcId', () => {
13
13
  const appDefinition = {
14
14
  vpc: {
15
15
  ownership: { vpc: 'external' },
@@ -22,20 +22,46 @@ describe('VpcResourceResolver', () => {
22
22
 
23
23
  expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
24
24
  expect(decision.physicalId).toBe('vpc-external-123');
25
- expect(decision.reason).toContain('User specified ownership=external');
25
+ expect(decision.reason).toContain('hardcoded vpcId');
26
26
  });
27
27
 
28
- it('should throw when external specified but vpcId missing', () => {
28
+ it('should resolve to EXTERNAL using discovered VPC when no hardcoded ID', () => {
29
29
  const appDefinition = {
30
30
  vpc: {
31
- ownership: { vpc: 'external' },
32
- external: {}
31
+ ownership: { vpc: 'external' }
32
+ // No external.vpcId provided
33
33
  }
34
34
  };
35
- const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
35
+ const discovery = {
36
+ stackManaged: [],
37
+ external: [],
38
+ fromCloudFormation: false,
39
+ defaultVpcId: 'vpc-discovered-123'
40
+ };
41
+
42
+ const decision = resolver.resolveVpc(appDefinition, discovery);
43
+
44
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
45
+ expect(decision.physicalId).toBe('vpc-discovered-123');
46
+ expect(decision.reason).toContain('discovered VPC');
47
+ });
48
+
49
+ it('should throw when external specified but no vpcId and no discovery', () => {
50
+ const appDefinition = {
51
+ vpc: {
52
+ ownership: { vpc: 'external' }
53
+ // No external.vpcId provided
54
+ }
55
+ };
56
+ const discovery = {
57
+ stackManaged: [],
58
+ external: [],
59
+ fromCloudFormation: false
60
+ // No defaultVpcId discovered
61
+ };
36
62
 
37
63
  expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
38
- "ownership='external' for vpcId requires external.vpcId"
64
+ /ownership='external' for VPC requires either/
39
65
  );
40
66
  });
41
67
 
@@ -97,17 +123,17 @@ describe('VpcResourceResolver', () => {
97
123
  expect(decision.physicalId).toBe('vpc-external');
98
124
  });
99
125
 
100
- it('should auto-resolve to STACK when not found (create new)', () => {
126
+ it('should throw error when auto mode finds no VPC (changed behavior)', () => {
101
127
  const appDefinition = {
102
128
  vpc: { ownership: { vpc: 'auto' } }
129
+ // No management specified - defaults to discover
103
130
  };
104
131
  const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
105
132
 
106
- const decision = resolver.resolveVpc(appDefinition, discovery);
107
-
108
- expect(decision.ownership).toBe(ResourceOwnership.STACK);
109
- expect(decision.physicalId).toBeUndefined();
110
- expect(decision.reason).toContain('No existing resource found');
133
+ // NEW BEHAVIOR: Auto mode with no VPC found should throw error (prevent accidental VPC creation)
134
+ expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
135
+ 'VPC discovery failed: No VPC found'
136
+ );
111
137
  });
112
138
 
113
139
  it('should throw error when auto mode finds no VPC and management is not create-new', () => {
@@ -134,7 +160,8 @@ describe('VpcResourceResolver', () => {
134
160
  const appDefinition = {
135
161
  vpc: {
136
162
  enable: true,
137
- management: 'create-new'
163
+ management: 'create-new',
164
+ ownership: { vpc: 'auto' }
138
165
  }
139
166
  };
140
167
  const discovery = {
@@ -147,12 +174,12 @@ describe('VpcResourceResolver', () => {
147
174
  const decision = resolver.resolveVpc(appDefinition, discovery);
148
175
 
149
176
  expect(decision.ownership).toBe(ResourceOwnership.STACK);
150
- expect(decision.physicalId).toBeNull();
177
+ expect(decision.physicalId).toBeUndefined(); // resolveResourceOwnership returns undefined, not null
151
178
  });
152
179
  });
153
180
 
154
181
  describe('resolveSecurityGroup', () => {
155
- it('should resolve to EXTERNAL with user-provided IDs', () => {
182
+ it('should resolve to EXTERNAL with user-provided hardcoded IDs', () => {
156
183
  const appDefinition = {
157
184
  vpc: {
158
185
  ownership: { securityGroup: 'external' },
@@ -165,6 +192,47 @@ describe('VpcResourceResolver', () => {
165
192
 
166
193
  expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
167
194
  expect(decision.physicalIds).toEqual(['sg-1', 'sg-2']);
195
+ expect(decision.reason).toContain('hardcoded');
196
+ });
197
+
198
+ it('should resolve to EXTERNAL using discovered default SG when no hardcoded IDs', () => {
199
+ const appDefinition = {
200
+ vpc: {
201
+ ownership: { securityGroup: 'external' }
202
+ // No external.securityGroupIds provided
203
+ }
204
+ };
205
+ const discovery = {
206
+ stackManaged: [],
207
+ external: [],
208
+ fromCloudFormation: false,
209
+ defaultSecurityGroupId: 'sg-discovered-default'
210
+ };
211
+
212
+ const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
213
+
214
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
215
+ expect(decision.physicalIds).toEqual(['sg-discovered-default']);
216
+ expect(decision.reason).toContain('discovered default security group');
217
+ });
218
+
219
+ it('should throw error when ownership=external but no IDs and no discovery', () => {
220
+ const appDefinition = {
221
+ vpc: {
222
+ ownership: { securityGroup: 'external' }
223
+ // No external.securityGroupIds provided
224
+ }
225
+ };
226
+ const discovery = {
227
+ stackManaged: [],
228
+ external: [],
229
+ fromCloudFormation: false
230
+ // No defaultSecurityGroupId discovered
231
+ };
232
+
233
+ expect(() => resolver.resolveSecurityGroup(appDefinition, discovery)).toThrow(
234
+ /ownership='external' for securityGroup requires either/
235
+ );
168
236
  });
169
237
 
170
238
  it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
@@ -186,7 +254,7 @@ describe('VpcResourceResolver', () => {
186
254
  });
187
255
 
188
256
  describe('resolveSubnets', () => {
189
- it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
257
+ it('should resolve to EXTERNAL with user-provided hardcoded subnet IDs', () => {
190
258
  const appDefinition = {
191
259
  vpc: {
192
260
  ownership: { subnets: 'external' },
@@ -199,6 +267,48 @@ describe('VpcResourceResolver', () => {
199
267
 
200
268
  expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
201
269
  expect(decision.physicalIds).toEqual(['subnet-1', 'subnet-2', 'subnet-3']);
270
+ expect(decision.reason).toContain('hardcoded');
271
+ });
272
+
273
+ it('should resolve to EXTERNAL using discovered subnets when no hardcoded IDs', () => {
274
+ const appDefinition = {
275
+ vpc: {
276
+ ownership: { subnets: 'external' }
277
+ // No external.subnetIds provided
278
+ }
279
+ };
280
+ const discovery = {
281
+ stackManaged: [],
282
+ external: [],
283
+ fromCloudFormation: false,
284
+ privateSubnetId1: 'subnet-discovered-1',
285
+ privateSubnetId2: 'subnet-discovered-2'
286
+ };
287
+
288
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
289
+
290
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
291
+ expect(decision.physicalIds).toEqual(['subnet-discovered-1', 'subnet-discovered-2']);
292
+ expect(decision.reason).toContain('discovered subnets');
293
+ });
294
+
295
+ it('should throw error when ownership=external but no IDs and no discovery', () => {
296
+ const appDefinition = {
297
+ vpc: {
298
+ ownership: { subnets: 'external' }
299
+ // No external.subnetIds provided
300
+ }
301
+ };
302
+ const discovery = {
303
+ stackManaged: [],
304
+ external: [],
305
+ fromCloudFormation: false
306
+ // No privateSubnetId1/2 discovered
307
+ };
308
+
309
+ expect(() => resolver.resolveSubnets(appDefinition, discovery)).toThrow(
310
+ /ownership='external' for subnets requires either/
311
+ );
202
312
  });
203
313
 
204
314
  it('should resolve to STACK when subnets found in stack', () => {
@@ -267,7 +377,7 @@ describe('VpcResourceResolver', () => {
267
377
  expect(decision.reason).toContain('NAT Gateway disabled');
268
378
  });
269
379
 
270
- it('should resolve to EXTERNAL with user-provided ID', () => {
380
+ it('should resolve to EXTERNAL with user-provided hardcoded ID', () => {
271
381
  const appDefinition = {
272
382
  vpc: {
273
383
  ownership: { natGateway: 'external' },
@@ -280,6 +390,47 @@ describe('VpcResourceResolver', () => {
280
390
 
281
391
  expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
282
392
  expect(decision.physicalId).toBe('nat-external-123');
393
+ expect(decision.reason).toContain('hardcoded');
394
+ });
395
+
396
+ it('should resolve to EXTERNAL using discovered NAT when no hardcoded ID', () => {
397
+ const appDefinition = {
398
+ vpc: {
399
+ ownership: { natGateway: 'external' }
400
+ // No external.natGatewayId provided
401
+ }
402
+ };
403
+ const discovery = {
404
+ stackManaged: [],
405
+ external: [],
406
+ fromCloudFormation: false,
407
+ natGatewayId: 'nat-discovered-123'
408
+ };
409
+
410
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
411
+
412
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
413
+ expect(decision.physicalId).toBe('nat-discovered-123');
414
+ expect(decision.reason).toContain('discovered NAT gateway');
415
+ });
416
+
417
+ it('should throw error when ownership=external but no ID and no discovery', () => {
418
+ const appDefinition = {
419
+ vpc: {
420
+ ownership: { natGateway: 'external' }
421
+ // No external.natGatewayId provided
422
+ }
423
+ };
424
+ const discovery = {
425
+ stackManaged: [],
426
+ external: [],
427
+ fromCloudFormation: false
428
+ // No natGatewayId discovered
429
+ };
430
+
431
+ expect(() => resolver.resolveNatGateway(appDefinition, discovery)).toThrow(
432
+ /ownership='external' for NAT gateway requires either/
433
+ );
283
434
  });
284
435
 
285
436
  it('should auto-resolve to STACK when found in stack', () => {
@@ -404,7 +555,8 @@ describe('VpcResourceResolver', () => {
404
555
  dynamodb: 'vpce-ddb-456'
405
556
  }
406
557
  }
407
- }
558
+ },
559
+ database: { dynamodb: { enable: true } } // Enable DynamoDB
408
560
  };
409
561
  const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
410
562
 
@@ -418,7 +570,10 @@ describe('VpcResourceResolver', () => {
418
570
  });
419
571
 
420
572
  it('should auto-resolve to STACK when endpoints found in stack', () => {
421
- const appDefinition = { vpc: { ownership: { vpcEndpoints: 'auto' } } };
573
+ const appDefinition = {
574
+ vpc: { ownership: { vpcEndpoints: 'auto' } },
575
+ database: { dynamodb: { enable: true } } // Enable DynamoDB
576
+ };
422
577
  const discovery = {
423
578
  stackManaged: [
424
579
  { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
@@ -439,7 +594,8 @@ describe('VpcResourceResolver', () => {
439
594
  it('should auto-resolve mixed: some in stack, some new', () => {
440
595
  const appDefinition = {
441
596
  vpc: { ownership: { vpcEndpoints: 'auto' } },
442
- encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
597
+ encryption: { fieldLevelEncryptionMethod: 'kms' }, // Enable KMS endpoint
598
+ database: { dynamodb: { enable: true } } // Enable DynamoDB
443
599
  };
444
600
  const discovery = {
445
601
  stackManaged: [
@@ -536,7 +692,11 @@ describe('VpcResourceResolver', () => {
536
692
  describe('real-world scenarios', () => {
537
693
  it('scenario: fresh deploy, no resources exist', () => {
538
694
  const appDefinition = {
539
- vpc: { enable: true, ownership: {} }
695
+ vpc: {
696
+ enable: true,
697
+ ownership: {},
698
+ management: 'create-new' // Explicitly allow VPC creation
699
+ }
540
700
  };
541
701
  const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
542
702
 
@@ -246,8 +246,31 @@ class CloudFormationDiscovery {
246
246
  );
247
247
 
248
248
  if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
249
- discovered.defaultVpcId = sgDetails.SecurityGroups[0].VpcId;
250
- console.log(` ✓ Extracted VPC ID from security group: ${discovered.defaultVpcId}`);
249
+ const vpcId = sgDetails.SecurityGroups[0].VpcId;
250
+ discovered.defaultVpcId = vpcId;
251
+ console.log(` ✓ Extracted VPC ID from security group: ${vpcId}`);
252
+
253
+ // Now query for the default security group in this VPC
254
+ if (!discovered.defaultSecurityGroupId) {
255
+ try {
256
+ console.log(` Querying for default security group in VPC...`);
257
+ const defaultSgResponse = await ec2Client.send(
258
+ new DescribeSecurityGroupsCommand({
259
+ Filters: [
260
+ { Name: 'vpc-id', Values: [vpcId] },
261
+ { Name: 'group-name', Values: ['default'] }
262
+ ]
263
+ })
264
+ );
265
+
266
+ if (defaultSgResponse.SecurityGroups && defaultSgResponse.SecurityGroups.length > 0) {
267
+ discovered.defaultSecurityGroupId = defaultSgResponse.SecurityGroups[0].GroupId;
268
+ console.log(` ✓ Discovered default security group: ${discovered.defaultSecurityGroupId}`);
269
+ }
270
+ } catch (error) {
271
+ console.warn(` ⚠️ Could not query default security group: ${error.message}`);
272
+ }
273
+ }
251
274
  } else {
252
275
  console.warn(` ⚠️ Security group query returned no results`);
253
276
  }
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.490.b61914a.0",
4
+ "version": "2.0.0--canary.490.581e175.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.490.b61914a.0",
20
- "@friggframework/schemas": "2.0.0--canary.490.b61914a.0",
21
- "@friggframework/test": "2.0.0--canary.490.b61914a.0",
19
+ "@friggframework/core": "2.0.0--canary.490.581e175.0",
20
+ "@friggframework/schemas": "2.0.0--canary.490.581e175.0",
21
+ "@friggframework/test": "2.0.0--canary.490.581e175.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.490.b61914a.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.490.b61914a.0",
49
+ "@friggframework/eslint-config": "2.0.0--canary.490.581e175.0",
50
+ "@friggframework/prettier-config": "2.0.0--canary.490.581e175.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": "b61914aa96566c50e35642e1384a60c5b09800c9"
82
+ "gitHead": "581e17591e9029cfc6326cdfc4b647b625ed8572"
83
83
  }