@friggframework/devtools 2.0.0-next.35 → 2.0.0-next.36

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.
@@ -1,26 +1,155 @@
1
- const { spawn, spawnSync } = require('child_process');
1
+ const { spawn } = require('child_process');
2
2
  const path = require('path');
3
+ const fs = require('fs');
3
4
 
4
- async function deployCommand(options) {
5
- console.log('Deploying the serverless application...');
5
+ // Configuration constants
6
+ const PATHS = {
7
+ APP_DEFINITION: 'index.js',
8
+ INFRASTRUCTURE: 'infrastructure.js'
9
+ };
10
+
11
+ const COMMANDS = {
12
+ SERVERLESS: 'serverless'
13
+ };
14
+
15
+ /**
16
+ * Constructs filtered environment variables for serverless deployment
17
+ * @param {string[]} appDefinedVariables - Array of environment variable names from app definition
18
+ * @returns {Object} Filtered environment variables object
19
+ */
20
+ function buildFilteredEnvironment(appDefinedVariables) {
21
+ return {
22
+ // Essential system variables needed to run serverless
23
+ PATH: process.env.PATH,
24
+ HOME: process.env.HOME,
25
+ USER: process.env.USER,
26
+
27
+ // AWS credentials and configuration (all AWS_ prefixed variables)
28
+ ...Object.fromEntries(
29
+ Object.entries(process.env).filter(([key]) =>
30
+ key.startsWith('AWS_')
31
+ )
32
+ ),
33
+
34
+ // App-defined environment variables
35
+ ...Object.fromEntries(
36
+ appDefinedVariables
37
+ .map((key) => [key, process.env[key]])
38
+ .filter(([_, value]) => value !== undefined)
39
+ ),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Loads and parses the app definition from index.js
45
+ * @returns {Object|null} App definition object or null if not found
46
+ */
47
+ function loadAppDefinition() {
48
+ const appDefPath = path.join(process.cwd(), PATHS.APP_DEFINITION);
49
+
50
+ if (!fs.existsSync(appDefPath)) {
51
+ return null;
52
+ }
53
+
54
+ try {
55
+ const { Definition } = require(appDefPath);
56
+ return Definition;
57
+ } catch (error) {
58
+ console.warn('Could not load appDefinition environment config:', error.message);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Extracts environment variable names from app definition
65
+ * @param {Object} appDefinition - App definition object
66
+ * @returns {string[]} Array of environment variable names
67
+ */
68
+ function extractEnvironmentVariables(appDefinition) {
69
+ if (!appDefinition?.environment) {
70
+ return [];
71
+ }
72
+
73
+ console.log('🔧 Loading environment configuration from appDefinition...');
74
+
75
+ const appDefinedVariables = Object.keys(appDefinition.environment).filter(
76
+ (key) => appDefinition.environment[key] === true
77
+ );
78
+
79
+ console.log(` Found ${appDefinedVariables.length} environment variables: ${appDefinedVariables.join(', ')}`);
80
+ return appDefinedVariables;
81
+ }
82
+
83
+ /**
84
+ * Handles environment validation warnings
85
+ * @param {Object} validation - Validation result object
86
+ * @param {Object} options - Deploy command options
87
+ */
88
+ function handleValidationWarnings(validation, options) {
89
+ if (validation.missing.length === 0 || options.skipEnvValidation) {
90
+ return;
91
+ }
6
92
 
7
- // AWS discovery is now handled directly in serverless-template.js
8
- console.log('🤘🏼 Deploying serverless application...');
9
- const backendPath = path.resolve(process.cwd());
10
- const infrastructurePath = 'infrastructure.js';
11
- const command = 'serverless';
93
+ console.warn(`⚠️ Warning: Missing ${validation.missing.length} environment variables: ${validation.missing.join(', ')}`);
94
+ console.warn(' These variables are optional and deployment will continue');
95
+ console.warn(' Run with --skip-env-validation to bypass this check');
96
+ }
97
+
98
+ /**
99
+ * Validates environment variables and builds filtered environment
100
+ * @param {Object} appDefinition - App definition object
101
+ * @param {Object} options - Deploy command options
102
+ * @returns {Object} Filtered environment variables
103
+ */
104
+ function validateAndBuildEnvironment(appDefinition, options) {
105
+ if (!appDefinition) {
106
+ return buildFilteredEnvironment([]);
107
+ }
108
+
109
+ const appDefinedVariables = extractEnvironmentVariables(appDefinition);
110
+
111
+ // Try to use the env-validator if available
112
+ try {
113
+ const { validateEnvironmentVariables } = require('@friggframework/devtools/infrastructure/env-validator');
114
+ const validation = validateEnvironmentVariables(appDefinition);
115
+
116
+ handleValidationWarnings(validation, options);
117
+ return buildFilteredEnvironment(appDefinedVariables);
118
+
119
+ } catch (validatorError) {
120
+ // Validator not available, do basic validation
121
+ const missingVariables = appDefinedVariables.filter((variable) => !process.env[variable]);
122
+
123
+ if (missingVariables.length > 0) {
124
+ console.warn(`⚠️ Warning: Missing ${missingVariables.length} environment variables: ${missingVariables.join(', ')}`);
125
+ console.warn(' These variables are optional and deployment will continue');
126
+ console.warn(' Set them in your CI/CD environment or .env file if needed');
127
+ }
128
+
129
+ return buildFilteredEnvironment(appDefinedVariables);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Executes the serverless deployment command
135
+ * @param {Object} environment - Environment variables to pass to serverless
136
+ * @param {Object} options - Deploy command options
137
+ */
138
+ function executeServerlessDeployment(environment, options) {
139
+ console.log('🚀 Deploying serverless application...');
140
+
12
141
  const serverlessArgs = [
13
142
  'deploy',
14
143
  '--config',
15
- infrastructurePath,
144
+ PATHS.INFRASTRUCTURE,
16
145
  '--stage',
17
146
  options.stage,
18
147
  ];
19
148
 
20
- const childProcess = spawn(command, serverlessArgs, {
21
- cwd: backendPath,
149
+ const childProcess = spawn(COMMANDS.SERVERLESS, serverlessArgs, {
150
+ cwd: path.resolve(process.cwd()),
22
151
  stdio: 'inherit',
23
- env: { ...process.env },
152
+ env: environment,
24
153
  });
25
154
 
26
155
  childProcess.on('error', (error) => {
@@ -34,4 +163,13 @@ async function deployCommand(options) {
34
163
  });
35
164
  }
36
165
 
166
+ async function deployCommand(options) {
167
+ console.log('Deploying the serverless application...');
168
+
169
+ const appDefinition = loadAppDefinition();
170
+ const environment = validateAndBuildEnvironment(appDefinition, options);
171
+
172
+ executeServerlessDeployment(environment, options);
173
+ }
174
+
37
175
  module.exports = { deployCommand };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Environment variable validator for Frigg applications
3
+ * Validates that required environment variables are present based on appDefinition
4
+ */
5
+
6
+ /**
7
+ * Validate environment variables against appDefinition
8
+ * @param {Object} AppDefinition - Application definition with environment config
9
+ * @returns {Object} Validation results with valid, missing, and warnings arrays
10
+ */
11
+ const validateEnvironmentVariables = (AppDefinition) => {
12
+ const results = {
13
+ valid: [],
14
+ missing: [],
15
+ warnings: [],
16
+ };
17
+
18
+ if (!AppDefinition.environment) {
19
+ return results;
20
+ }
21
+
22
+ console.log('🔍 Validating environment variables...');
23
+
24
+ for (const [key, value] of Object.entries(AppDefinition.environment)) {
25
+ if (value === true) {
26
+ if (process.env[key]) {
27
+ results.valid.push(key);
28
+ } else {
29
+ results.missing.push(key);
30
+ }
31
+ }
32
+ }
33
+
34
+ // Special handling for certain variables
35
+ if (results.missing.includes('NODE_ENV')) {
36
+ results.warnings.push('NODE_ENV not set, defaulting to "production"');
37
+ // Remove from missing since it has a default
38
+ results.missing = results.missing.filter((v) => v !== 'NODE_ENV');
39
+ }
40
+
41
+ // Report results
42
+ if (results.valid.length > 0) {
43
+ console.log(
44
+ ` ✅ Valid: ${results.valid.length} environment variables found`
45
+ );
46
+ }
47
+
48
+ if (results.missing.length > 0) {
49
+ console.log(` ⚠️ Missing: ${results.missing.join(', ')}`);
50
+ results.warnings.push(
51
+ `Missing ${results.missing.length} environment variables. These should be set in your CI/CD environment or .env file`
52
+ );
53
+ }
54
+
55
+ if (results.warnings.length > 0) {
56
+ results.warnings.forEach((warning) => {
57
+ console.log(` ⚠️ ${warning}`);
58
+ });
59
+ }
60
+
61
+ return results;
62
+ };
63
+
64
+ /**
65
+ * Check if all required environment variables are present
66
+ * @param {Object} AppDefinition - Application definition
67
+ * @returns {boolean} True if all required variables are present
68
+ */
69
+ const hasAllRequiredEnvVars = (AppDefinition) => {
70
+ const results = validateEnvironmentVariables(AppDefinition);
71
+ return results.missing.length === 0;
72
+ };
73
+
74
+ module.exports = {
75
+ validateEnvironmentVariables,
76
+ hasAllRequiredEnvVars,
77
+ };
@@ -180,6 +180,114 @@ function generateIAMCloudFormation(appDefinition, options = {}) {
180
180
  };
181
181
 
182
182
  // Add Core Deployment Policy (always needed)
183
+ const coreActions = [
184
+ // CloudFormation permissions
185
+ 'cloudformation:CreateStack',
186
+ 'cloudformation:UpdateStack',
187
+ 'cloudformation:DeleteStack',
188
+ 'cloudformation:DescribeStacks',
189
+ 'cloudformation:DescribeStackEvents',
190
+ 'cloudformation:DescribeStackResources',
191
+ 'cloudformation:DescribeStackResource',
192
+ 'cloudformation:ListStackResources',
193
+ 'cloudformation:GetTemplate',
194
+ 'cloudformation:DescribeChangeSet',
195
+ 'cloudformation:CreateChangeSet',
196
+ 'cloudformation:DeleteChangeSet',
197
+ 'cloudformation:ExecuteChangeSet',
198
+ 'cloudformation:ValidateTemplate',
199
+
200
+ // Lambda permissions
201
+ 'lambda:CreateFunction',
202
+ 'lambda:UpdateFunctionCode',
203
+ 'lambda:UpdateFunctionConfiguration',
204
+ 'lambda:DeleteFunction',
205
+ 'lambda:GetFunction',
206
+ 'lambda:ListFunctions',
207
+ 'lambda:PublishVersion',
208
+ 'lambda:CreateAlias',
209
+ 'lambda:UpdateAlias',
210
+ 'lambda:DeleteAlias',
211
+ 'lambda:GetAlias',
212
+ 'lambda:AddPermission',
213
+ 'lambda:RemovePermission',
214
+ 'lambda:GetPolicy',
215
+ 'lambda:PutProvisionedConcurrencyConfig',
216
+ 'lambda:DeleteProvisionedConcurrencyConfig',
217
+ 'lambda:PutConcurrency',
218
+ 'lambda:DeleteConcurrency',
219
+ 'lambda:TagResource',
220
+ 'lambda:UntagResource',
221
+ 'lambda:ListVersionsByFunction',
222
+
223
+ // IAM permissions
224
+ 'iam:CreateRole',
225
+ 'iam:DeleteRole',
226
+ 'iam:GetRole',
227
+ 'iam:PassRole',
228
+ 'iam:PutRolePolicy',
229
+ 'iam:DeleteRolePolicy',
230
+ 'iam:GetRolePolicy',
231
+ 'iam:AttachRolePolicy',
232
+ 'iam:DetachRolePolicy',
233
+ 'iam:TagRole',
234
+ 'iam:UntagRole',
235
+ 'iam:ListPolicyVersions',
236
+
237
+ // S3 permissions
238
+ 's3:CreateBucket',
239
+ 's3:PutObject',
240
+ 's3:GetObject',
241
+ 's3:DeleteObject',
242
+ 's3:PutBucketPolicy',
243
+ 's3:PutBucketVersioning',
244
+ 's3:PutBucketPublicAccessBlock',
245
+ 's3:GetBucketLocation',
246
+ 's3:ListBucket',
247
+
248
+ // SQS permissions
249
+ 'sqs:CreateQueue',
250
+ 'sqs:DeleteQueue',
251
+ 'sqs:GetQueueAttributes',
252
+ 'sqs:SetQueueAttributes',
253
+ 'sqs:GetQueueUrl',
254
+ 'sqs:TagQueue',
255
+ 'sqs:UntagQueue',
256
+
257
+ // SNS permissions
258
+ 'sns:CreateTopic',
259
+ 'sns:DeleteTopic',
260
+ 'sns:GetTopicAttributes',
261
+ 'sns:SetTopicAttributes',
262
+ 'sns:Subscribe',
263
+ 'sns:Unsubscribe',
264
+ 'sns:ListSubscriptionsByTopic',
265
+ 'sns:TagResource',
266
+ 'sns:UntagResource',
267
+
268
+ // CloudWatch and Logs permissions
269
+ 'cloudwatch:PutMetricAlarm',
270
+ 'cloudwatch:DeleteAlarms',
271
+ 'cloudwatch:DescribeAlarms',
272
+ 'logs:CreateLogGroup',
273
+ 'logs:CreateLogStream',
274
+ 'logs:DeleteLogGroup',
275
+ 'logs:DescribeLogGroups',
276
+ 'logs:DescribeLogStreams',
277
+ 'logs:FilterLogEvents',
278
+ 'logs:PutLogEvents',
279
+ 'logs:PutRetentionPolicy',
280
+
281
+ // API Gateway permissions
282
+ 'apigateway:POST',
283
+ 'apigateway:PUT',
284
+ 'apigateway:DELETE',
285
+ 'apigateway:GET',
286
+ 'apigateway:PATCH',
287
+ 'apigateway:TagResource',
288
+ 'apigateway:UntagResource',
289
+ ];
290
+
183
291
  const coreStatements = [
184
292
  {
185
293
  Sid: 'CloudFormationFriggStacks',
@@ -374,6 +482,8 @@ function generateIAMCloudFormation(appDefinition, options = {}) {
374
482
  'apigateway:DELETE',
375
483
  'apigateway:GET',
376
484
  'apigateway:PATCH',
485
+ 'apigateway:TagResource',
486
+ 'apigateway:UntagResource',
377
487
  ],
378
488
  Resource: [
379
489
  'arn:aws:apigateway:*::/restapis',
@@ -397,8 +507,6 @@ function generateIAMCloudFormation(appDefinition, options = {}) {
397
507
  'arn:aws:apigateway:*::/apis/*',
398
508
  'arn:aws:apigateway:*::/apis/*/stages',
399
509
  'arn:aws:apigateway:*::/apis/*/stages/*',
400
- 'arn:aws:apigateway:*::/apis/*/mappings',
401
- 'arn:aws:apigateway:*::/apis/*/mappings/*',
402
510
  'arn:aws:apigateway:*::/domainnames',
403
511
  'arn:aws:apigateway:*::/domainnames/*',
404
512
  'arn:aws:apigateway:*::/domainnames/*/apimappings',
@@ -199,13 +199,17 @@
199
199
  "apigateway:PUT",
200
200
  "apigateway:DELETE",
201
201
  "apigateway:GET",
202
- "apigateway:PATCH"
202
+ "apigateway:PATCH",
203
+ "apigateway:TagResource",
204
+ "apigateway:UntagResource"
203
205
  ],
204
206
  "Resource": [
205
207
  "arn:aws:apigateway:*::/restapis",
206
208
  "arn:aws:apigateway:*::/restapis/*",
207
209
  "arn:aws:apigateway:*::/apis",
208
210
  "arn:aws:apigateway:*::/apis/*",
211
+ "arn:aws:apigateway:*::/apis/*/stages",
212
+ "arn:aws:apigateway:*::/apis/*/stages/*",
209
213
  "arn:aws:apigateway:*::/domainnames",
210
214
  "arn:aws:apigateway:*::/domainnames/*"
211
215
  ]
@@ -199,13 +199,17 @@
199
199
  "apigateway:PUT",
200
200
  "apigateway:DELETE",
201
201
  "apigateway:GET",
202
- "apigateway:PATCH"
202
+ "apigateway:PATCH",
203
+ "apigateway:TagResource",
204
+ "apigateway:UntagResource"
203
205
  ],
204
206
  "Resource": [
205
207
  "arn:aws:apigateway:*::/restapis",
206
208
  "arn:aws:apigateway:*::/restapis/*",
207
209
  "arn:aws:apigateway:*::/apis",
208
210
  "arn:aws:apigateway:*::/apis/*",
211
+ "arn:aws:apigateway:*::/apis/*/stages",
212
+ "arn:aws:apigateway:*::/apis/*/stages/*",
209
213
  "arn:aws:apigateway:*::/domainnames",
210
214
  "arn:aws:apigateway:*::/domainnames/*"
211
215
  ]
@@ -16,6 +16,68 @@ const shouldRunDiscovery = (AppDefinition) => {
16
16
  );
17
17
  };
18
18
 
19
+ /**
20
+ * Extract environment variables from AppDefinition
21
+ * @param {Object} AppDefinition - Application definition
22
+ * @returns {Object} Environment variables to set in serverless
23
+ */
24
+ const getAppEnvironmentVars = (AppDefinition) => {
25
+ const envVars = {};
26
+
27
+ // AWS Lambda reserved environment variables that cannot be set (from official AWS docs)
28
+ const reservedVars = new Set([
29
+ '_HANDLER',
30
+ '_X_AMZN_TRACE_ID',
31
+ 'AWS_DEFAULT_REGION',
32
+ 'AWS_EXECUTION_ENV',
33
+ 'AWS_REGION',
34
+ 'AWS_LAMBDA_FUNCTION_NAME',
35
+ 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
36
+ 'AWS_LAMBDA_FUNCTION_VERSION',
37
+ 'AWS_LAMBDA_INITIALIZATION_TYPE',
38
+ 'AWS_LAMBDA_LOG_GROUP_NAME',
39
+ 'AWS_LAMBDA_LOG_STREAM_NAME',
40
+ 'AWS_ACCESS_KEY',
41
+ 'AWS_ACCESS_KEY_ID',
42
+ 'AWS_SECRET_ACCESS_KEY',
43
+ 'AWS_SESSION_TOKEN',
44
+ ]);
45
+
46
+ if (AppDefinition.environment) {
47
+ console.log('📋 Loading environment variables from appDefinition...');
48
+ const envKeys = [];
49
+ const skippedKeys = [];
50
+
51
+ for (const [key, value] of Object.entries(AppDefinition.environment)) {
52
+ if (value === true) {
53
+ if (reservedVars.has(key)) {
54
+ skippedKeys.push(key);
55
+ } else {
56
+ envVars[key] = `\${env:${key}, ''}`;
57
+ envKeys.push(key);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (envKeys.length > 0) {
63
+ console.log(
64
+ ` Found ${
65
+ envKeys.length
66
+ } environment variables: ${envKeys.join(', ')}`
67
+ );
68
+ }
69
+ if (skippedKeys.length > 0) {
70
+ console.log(
71
+ ` ⚠️ Skipped ${
72
+ skippedKeys.length
73
+ } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
74
+ );
75
+ }
76
+ }
77
+
78
+ return envVars;
79
+ };
80
+
19
81
  /**
20
82
  * Find the actual path to node_modules directory
21
83
  * Tries multiple methods to locate node_modules:
@@ -555,17 +617,8 @@ const composeServerlessDefinition = async (AppDefinition) => {
555
617
  }
556
618
  }
557
619
 
558
- // Debug: log keys of env vars available during deploy (to verify GA -> Serverless pass-through)
559
- try {
560
- const envKeys = Object.keys(process.env || {}).sort();
561
- console.log(
562
- 'Frigg deploy env keys (sample):',
563
- envKeys.slice(0, 30),
564
- `... total=${envKeys.length}`
565
- );
566
- } catch (e) {
567
- console.log('Frigg deploy env keys: <unavailable>', e?.message);
568
- }
620
+ // Get environment variables from appDefinition
621
+ const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
569
622
 
570
623
  const definition = {
571
624
  frameworkVersion: '>=3.17.0',
@@ -588,18 +641,8 @@ const composeServerlessDefinition = async (AppDefinition) => {
588
641
  environment: {
589
642
  STAGE: '${opt:stage, "dev"}',
590
643
  AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
591
- // Pass through FRIGG__ prefixed variables (stripping the prefix)
592
- // This avoids CloudFormation validation errors from system variables
593
- ...Object.fromEntries(
594
- Object.entries(process.env)
595
- .filter(([key]) => key.startsWith('FRIGG__'))
596
- .map(([key, value]) => [
597
- key.replace('FRIGG__', ''),
598
- value,
599
- ])
600
- ),
601
- // Also include essential non-prefixed variables
602
- ...(process.env.NODE_ENV && { NODE_ENV: process.env.NODE_ENV }),
644
+ // Add environment variables from appDefinition
645
+ ...appEnvironmentVars,
603
646
  // Add discovered resources to environment if available
604
647
  ...(discoveredResources.defaultVpcId && {
605
648
  AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId,
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-next.35",
4
+ "version": "2.0.0-next.36",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -9,8 +9,8 @@
9
9
  "@babel/eslint-parser": "^7.18.9",
10
10
  "@babel/parser": "^7.25.3",
11
11
  "@babel/traverse": "^7.25.3",
12
- "@friggframework/schemas": "2.0.0-next.35",
13
- "@friggframework/test": "2.0.0-next.35",
12
+ "@friggframework/schemas": "2.0.0-next.36",
13
+ "@friggframework/test": "2.0.0-next.36",
14
14
  "@hapi/boom": "^10.0.1",
15
15
  "@inquirer/prompts": "^5.3.8",
16
16
  "axios": "^1.7.2",
@@ -32,8 +32,8 @@
32
32
  "serverless-http": "^2.7.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@friggframework/eslint-config": "2.0.0-next.35",
36
- "@friggframework/prettier-config": "2.0.0-next.35",
35
+ "@friggframework/eslint-config": "2.0.0-next.36",
36
+ "@friggframework/prettier-config": "2.0.0-next.36",
37
37
  "prettier": "^2.7.1",
38
38
  "serverless": "3.39.0",
39
39
  "serverless-dotenv-plugin": "^6.0.0",
@@ -65,5 +65,5 @@
65
65
  "publishConfig": {
66
66
  "access": "public"
67
67
  },
68
- "gitHead": "2b5b01f23819faad6b2780f1aaabbd9bfb52fe4a"
68
+ "gitHead": "6cca53cb3091d6bd11ae37f6c459679f1b5b19a6"
69
69
  }