@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/oclif.manifest.json +1 -1
- package/package.json +7 -5
- package/src/commands/__fixtures__/env_invalid +2 -8
- package/src/commands/__fixtures__/sample_secrets_mesh.json +18 -0
- package/src/commands/__fixtures__/secrets_invalid.yaml +3 -0
- package/src/commands/__fixtures__/secrets_valid.yaml +2 -0
- package/src/commands/__fixtures__/secrets_with_batch_variables.yaml +4 -0
- package/src/commands/api-mesh/__tests__/create.test.js +316 -5
- package/src/commands/api-mesh/__tests__/run.test.js +146 -7
- package/src/commands/api-mesh/create.js +21 -1
- package/src/commands/api-mesh/run.js +17 -0
- package/src/commands/api-mesh/update.js +33 -7
- package/src/helpers.js +21 -0
- package/src/lib/devConsole.js +48 -0
- package/src/server.js +7 -0
- package/src/serverUtils.js +21 -0
- package/src/utils.js +149 -60
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|