@adobe/aio-cli-plugin-api-mesh 3.4.0 → 3.5.0-beta.1

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/src/utils.js CHANGED
@@ -5,6 +5,11 @@ const { Flags } = require('@oclif/core');
5
5
  const { readFile } = require('fs/promises');
6
6
  const { interpolateMesh } = require('./helpers');
7
7
  const dotenv = require('dotenv');
8
+ const YAML = require('yaml');
9
+ const parseEnv = require('envsub/js/envsub-parser');
10
+ const os = require('os');
11
+ const chalk = require('chalk');
12
+ const crypto = require('crypto');
8
13
 
9
14
  /**
10
15
  * @returns returns the root directory of the project
@@ -41,6 +46,12 @@ const envFileFlag = Flags.string({
41
46
  default: '.env',
42
47
  });
43
48
 
49
+ const secretsFlag = Flags.string({
50
+ char: 's',
51
+ description: 'Path to secrets file',
52
+ default: false,
53
+ });
54
+
44
55
  const portNoFlag = Flags.integer({
45
56
  char: 'p',
46
57
  description: 'Port number for the local dev server',
@@ -297,61 +308,6 @@ function validateFileName(filesList) {
297
308
  }
298
309
  }
299
310
 
300
- /**validates the environment file content
301
- * @param {string} envContent
302
- * @returns {object} containing the status of validation
303
- * If validation is failed then the error property including the formatting errors is returned.
304
- */
305
- function validateEnvFileFormat(envContent) {
306
- //Key should start with a underscore or an alphabet followed by underscore/alphanumeric characters
307
- const envKeyRegex = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
308
-
309
- const envValueRegex = /^(?:"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|[^'"\s])+$/;
310
-
311
- /*
312
- The above regex matches one or more of below :
313
- (?:"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|[^'"\s])
314
- which is
315
- 1. ?:"(?:\\.|[^\\"])*" : Non capturing group starts and ends with '"'
316
- */
317
- const envDict = {};
318
- const lines = envContent.split(/\r?\n/);
319
- const errors = [];
320
-
321
- for (let index = 0; index < lines.length; index++) {
322
- const line = lines[index];
323
- const trimmedLine = line.trim();
324
- if (trimmedLine.startsWith('#') || trimmedLine === '') {
325
- // ignore comment or empty lines
326
- continue;
327
- }
328
-
329
- if (!trimmedLine.includes('=')) {
330
- errors.push(`Invalid format << ${trimmedLine} >> on line ${index + 1}`);
331
- } else {
332
- const [key, value] = trimmedLine.split('=', 2);
333
- if (!envKeyRegex.test(key) || !envValueRegex.test(value)) {
334
- // invalid format: key or value does not match regex
335
- errors.push(`Invalid format for key/value << ${trimmedLine} >> on line ${index + 1}`);
336
- }
337
- if (key in envDict) {
338
- // duplicate key found
339
- errors.push(`Duplicate key << ${key} >> on line ${index + 1}`);
340
- }
341
- envDict[key] = value;
342
- }
343
- }
344
- if (errors.length) {
345
- return {
346
- valid: false,
347
- error: errors.toString(),
348
- };
349
- }
350
- return {
351
- valid: true,
352
- };
353
- }
354
-
355
311
  /**
356
312
  * Read the environment file, checks for validation status and interpolate mesh
357
313
  * @param {string} inputMeshData
@@ -364,10 +320,10 @@ async function validateAndInterpolateMesh(inputMeshData, envFilePath, command) {
364
320
  const envFileContent = await readFileContents(envFilePath, command, 'env');
365
321
 
366
322
  //Validate the environment file
367
- const envFileValidity = validateEnvFileFormat(envFileContent);
368
- if (envFileValidity.valid) {
323
+ try {
369
324
  //load env file using dotenv and add 'env' as the root property in the object
370
- const envObj = { env: dotenv.config({ path: envFilePath }).parsed };
325
+ const config = dotenv.parse(envFileContent);
326
+ const envObj = { env: config };
371
327
  const { interpolationStatus, missingKeys, interpolatedMeshData } = await interpolateMesh(
372
328
  inputMeshData,
373
329
  envObj,
@@ -386,8 +342,138 @@ async function validateAndInterpolateMesh(inputMeshData, envFilePath, command) {
386
342
  command.log(interpolatedMeshData);
387
343
  command.error('Interpolated mesh is not a valid JSON. Please check the generated json file.');
388
344
  }
345
+ } catch (err) {
346
+ command.error(`Issue in ${envFilePath} file - ` + err.message);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Validate secrets file
352
+ *
353
+ * @param secretsFile Validates that secrets file extension is in yaml
354
+ */
355
+ async function validateSecretsFile(secretsFile) {
356
+ try {
357
+ const validExtensions = ['.yaml', '.yml'];
358
+ const fileExtension = secretsFile.split('.').pop().toLowerCase();
359
+ if (!validExtensions.includes('.' + fileExtension)) {
360
+ throw new Error(
361
+ chalk.red('Invalid file format. Please provide a YAML file (.yaml or .yml).'),
362
+ );
363
+ }
364
+ } catch (error) {
365
+ logger.error(error.message);
366
+ throw new Error(error.message);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Read the secrets file, checks validation and interpolate mesh
372
+ *
373
+ * @param secretsFilePath Secrets file path
374
+ * @param command
375
+ */
376
+ async function interpolateSecrets(secretsFilePath, command) {
377
+ try {
378
+ const secretsContent = await readFileContents(secretsFilePath, command, 'secrets');
379
+
380
+ // Check if environment variables are used in the file content
381
+ if (os.platform() === 'win32' && /\$({)?[a-zA-Z_][a-zA-Z0-9_]*}?/.test(secretsContent)) {
382
+ throw new Error(chalk.red('Batch variables are not supported in YAML files on Windows.'));
383
+ }
384
+ const secrets = await parseSecrets(secretsContent);
385
+ return secrets;
386
+ } catch (err) {
387
+ logger.error(err.message);
388
+ throw new Error(err.message);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Parse secrets YAML content.
394
+ *
395
+ * @param secretsFilePath Secrets file path
396
+ */
397
+ async function parseSecrets(secretsContent) {
398
+ try {
399
+ const envParserConfig = {
400
+ outputFile: null,
401
+ options: {
402
+ all: false,
403
+ diff: false,
404
+ protect: false,
405
+ syntax: 'dollar-both',
406
+ },
407
+ cli: false,
408
+ };
409
+ const compiledSecretsFileContent = parseEnv(secretsContent, envParserConfig);
410
+ const parsedSecrets = YAML.parse(compiledSecretsFileContent);
411
+ //check if secrets file is empty
412
+ if (!parsedSecrets) {
413
+ throw new Error(chalk.red('Invalid YAML file contents. Please verify and try again.'));
414
+ }
415
+ //check if parsedSecrets is string and not in k:v pair
416
+ if (typeof parsedSecrets === 'string') {
417
+ throw new Error(chalk.red('Please provide a valid YAML in key:value format.'));
418
+ }
419
+ const secretsYamlString = YAML.stringify(parsedSecrets);
420
+ return secretsYamlString; //TODO: here we will encrypt secrets and return.
421
+ } catch (err) {
422
+ throw new Error(chalk.red(getSecretsYamlParseError(err)));
423
+ }
424
+ }
425
+
426
+ /**
427
+ * This function returns user friendly errors that occurs while YAML.parse
428
+ *
429
+ * @param error errors from YAML.parse
430
+ */
431
+ function getSecretsYamlParseError(error) {
432
+ if (error.code === 'BAD_INDENT') {
433
+ return 'Invalid YAML - Bad Indentation: ' + error.message;
434
+ } else if (error.code === 'DUPLICATE_KEY') {
435
+ return 'Invalid YAML - Found Duplicate Keys: ' + error.message;
389
436
  } else {
390
- command.error(`Issue in ${envFilePath} file - ` + envFileValidity.error);
437
+ return 'Unexpected Error: ' + error.message;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Performs hybrid encryption of secrets(AES + RSA)
443
+ *
444
+ * @param publicKey Public key for (AES + RSA) encryption
445
+ * @param secrets Secrets Data that needs encryption
446
+ */
447
+ async function encryptSecrets(publicKey, secrets) {
448
+ if (!publicKey || typeof publicKey !== 'string' || !publicKey.trim()) {
449
+ throw new Error(chalk.red('Unable to encrypt secerts. Invalid Public Key.'));
450
+ }
451
+ try {
452
+ // Generate a random AES key and IV
453
+ const aesKey = crypto.randomBytes(32); // 256-bit key for AES-256
454
+ const iv = crypto.randomBytes(16); // Initialization vector
455
+ // Encrypt the secrets using AES-256-CBC
456
+ const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
457
+ let encryptedData = cipher.update(secrets, 'utf8', 'base64');
458
+ encryptedData += cipher.final('base64');
459
+ // Encrypt the AES key using RSA with OAEP padding
460
+ const encryptedAesKey = crypto.publicEncrypt(
461
+ {
462
+ key: publicKey,
463
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
464
+ },
465
+ aesKey,
466
+ );
467
+ // Package the encrypted AES key, IV, and encrypted data
468
+ const encryptedPackage = {
469
+ iv: iv.toString('base64'),
470
+ key: encryptedAesKey.toString('base64'),
471
+ data: encryptedData,
472
+ };
473
+ return JSON.stringify(encryptedPackage);
474
+ } catch (error) {
475
+ logger.error('Unable to encrypt secrets. Please try again. :', error.message);
476
+ throw new Error(`Unable to encrypt secerts. ${error.message}`);
391
477
  }
392
478
  }
393
479
 
@@ -399,10 +485,13 @@ module.exports = {
399
485
  envFileFlag,
400
486
  checkPlaceholders,
401
487
  readFileContents,
402
- validateEnvFileFormat,
403
488
  validateAndInterpolateMesh,
404
489
  getAppRootDir,
405
490
  portNoFlag,
406
491
  debugFlag,
407
492
  selectFlag,
493
+ secretsFlag,
494
+ interpolateSecrets,
495
+ validateSecretsFile,
496
+ encryptSecrets,
408
497
  };