@adobe/aio-cli-plugin-api-mesh 3.4.0 → 3.5.0-alpha.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.
@@ -1 +1 @@
1
- {"version":"3.4.0","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:init":{"id":"api-mesh:init","description":"This command will create a workspace where you can organise your API mesh configuration and other files","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[{"description":"API mesh workspace init","command":"aio api-mesh init commerce-mesh"},{"description":"API mesh workspace init with flags","command":"aio api-mesh init commerce-mesh --path ./mesh_projects/test_mesh --git y --packageManager yarn"}],"flags":{"path":{"name":"path","type":"option","char":"p","default":"."},"packageManager":{"name":"packageManager","type":"option","char":"m","options":["npm","yarn"]},"git":{"name":"git","type":"option","char":"g","options":["y","n"]}},"args":[{"name":"projectName","description":"Project name","required":true}]},"api-mesh:run":{"id":"api-mesh:run","description":"Run a local development server that builds and compiles a mesh locally","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[],"flags":{"port":{"name":"port","type":"option","char":"p","description":"Port number for the local dev server"},"debug":{"name":"debug","type":"boolean","description":"Enable debugging mode","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"select":{"name":"select","type":"boolean","description":"Retrieve existing artifacts from the mesh","allowNo":false}},"args":[{"name":"file","description":"Mesh File"}]},"api-mesh:status":{"id":"api-mesh:status","description":"Get a mesh status with a given meshid.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm install action prompt. CLI will not check ask user to install source.","allowNo":false}},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get -s=<version>@<source_name>","$ aio api-mesh:source:get -s<source_name>","$ aio api-mesh:source:get -m"],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm print action prompt. CLI will not check ask user to print source.","allowNo":false},"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]},"api-mesh:source:install":{"id":"api-mesh:source:install","description":"Command to install the source to your API mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:install <version>@<source_name>","$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>","$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm override action prompt. CLI will not check ask user to override source.","allowNo":false},"variable":{"name":"variable","type":"option","char":"v","description":"Variables required for the source"},"variable-file":{"name":"variable-file","type":"option","char":"f","description":"Variables file path"},"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"source"}]}}}
1
+ {"version":"3.5.0-alpha.1","commands":{"PLUGINNAME":{"id":"PLUGINNAME","description":"Your description here","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio PLUGINNAME:some_command"],"flags":{"someflag":{"name":"someflag","type":"option","char":"f","description":"this is some flag"}},"args":[]},"api-mesh:create":{"id":"api-mesh:create","description":"Create a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:delete":{"id":"api-mesh:delete","description":"Delete the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false}},"args":[]},"api-mesh:describe":{"id":"api-mesh:describe","description":"Get details of a mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:get":{"id":"api-mesh:get","description":"Get the config of a given mesh","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"json":{"name":"json","type":"boolean","description":"Output JSON","allowNo":false}},"args":[{"name":"file"}]},"api-mesh:init":{"id":"api-mesh:init","description":"This command will create a workspace where you can organise your API mesh configuration and other files","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[{"description":"API mesh workspace init","command":"aio api-mesh init commerce-mesh"},{"description":"API mesh workspace init with flags","command":"aio api-mesh init commerce-mesh --path ./mesh_projects/test_mesh --git y --packageManager yarn"}],"flags":{"path":{"name":"path","type":"option","char":"p","default":"."},"packageManager":{"name":"packageManager","type":"option","char":"m","options":["npm","yarn"]},"git":{"name":"git","type":"option","char":"g","options":["y","n"]}},"args":[{"name":"projectName","description":"Project name","required":true}]},"api-mesh:run":{"id":"api-mesh:run","description":"Run a local development server that builds and compiles a mesh locally","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":[],"flags":{"port":{"name":"port","type":"option","char":"p","description":"Port number for the local dev server"},"debug":{"name":"debug","type":"boolean","description":"Enable debugging mode","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"select":{"name":"select","type":"boolean","description":"Retrieve existing artifacts from the mesh","allowNo":false},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file","description":"Mesh File"}]},"api-mesh:status":{"id":"api-mesh:status","description":"Get a mesh status with a given meshid.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[]},"api-mesh:update":{"id":"api-mesh:update","description":"Update a mesh with the given config.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false},"autoConfirmAction":{"name":"autoConfirmAction","type":"boolean","char":"c","description":"Auto confirm action prompt. CLI will not check for user approval before executing the action.","allowNo":false},"env":{"name":"env","type":"option","char":"e","description":"Path to env file","default":".env"},"secrets":{"name":"secrets","type":"option","char":"s","description":"Path to secrets file","default":false}},"args":[{"name":"file"}]},"api-mesh:source:discover":{"id":"api-mesh:source:discover","description":"Return the list of avaliable sources","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm install action prompt. CLI will not check ask user to install source.","allowNo":false}},"args":[]},"api-mesh:source:get":{"id":"api-mesh:source:get","description":"Command returns the content of a specific source.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:get -s=<version>@<source_name>","$ aio api-mesh:source:get -s<source_name>","$ aio api-mesh:source:get -m"],"flags":{"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm print action prompt. CLI will not check ask user to print source.","allowNo":false},"source":{"name":"source","type":"option","char":"s","description":"Source name"},"multiple":{"name":"multiple","type":"boolean","char":"m","description":"Select multiple sources","allowNo":false}},"args":[]},"api-mesh:source:install":{"id":"api-mesh:source:install","description":"Command to install the source to your API mesh.","pluginName":"@adobe/aio-cli-plugin-api-mesh","pluginType":"core","aliases":[],"examples":["$ aio api-mesh:source:install <version>@<source_name>","$ aio api-mesh:source:install <source_name> -v <variable_name>=<variable_value>","$ aio api-mesh:source:install <source_name> -f <path_to_variables_file>"],"flags":{"source":{"name":"source","type":"option","char":"s","description":"Source name"},"confirm":{"name":"confirm","type":"boolean","char":"c","description":"Auto confirm override action prompt. CLI will not check ask user to override source.","allowNo":false},"variable":{"name":"variable","type":"option","char":"v","description":"Variables required for the source"},"variable-file":{"name":"variable-file","type":"option","char":"f","description":"Variables file path"},"ignoreCache":{"name":"ignoreCache","type":"boolean","char":"i","description":"Ignore cache and force manual org -> project -> workspace selection","allowNo":false}},"args":[{"name":"source"}]}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aio-cli-plugin-api-mesh",
3
- "version": "3.4.0",
3
+ "version": "3.5.0-alpha.1",
4
4
  "description": "Adobe I/O CLI plugin to develop and manage API mesh sources",
5
5
  "keywords": [
6
6
  "oclif-plugin"
@@ -37,7 +37,7 @@
37
37
  "version": "oclif-dev readme && git add README.md"
38
38
  },
39
39
  "dependencies": {
40
- "@adobe-apimesh/mesh-builder": "1.5.0",
40
+ "@adobe-apimesh/mesh-builder": "1.4.4",
41
41
  "@adobe/aio-cli-lib-console": "^4.0.0",
42
42
  "@adobe/aio-lib-core-config": "^3.0.0",
43
43
  "@adobe/aio-lib-core-logging": "^2.0.0",
@@ -52,6 +52,7 @@
52
52
  "@graphql-mesh/plugin-http-details-extensions": "0.1.21",
53
53
  "@graphql-mesh/runtime": "0.46.21",
54
54
  "@graphql-mesh/soap": "0.14.25",
55
+ "@graphql-mesh/store": "0.9.20",
55
56
  "@graphql-mesh/transform-encapsulate": "0.4.21",
56
57
  "@graphql-mesh/transform-federation": "0.11.14",
57
58
  "@graphql-mesh/transform-filter-schema": "0.15.23",
@@ -64,7 +65,6 @@
64
65
  "@graphql-mesh/transform-resolvers-composition": "0.13.20",
65
66
  "@graphql-mesh/transform-type-merging": "0.5.20",
66
67
  "@graphql-mesh/types": "0.91.12",
67
- "@graphql-mesh/store": "0.9.20",
68
68
  "@oclif/command": "^1.6.1",
69
69
  "@oclif/config": "^1.15.1",
70
70
  "@oclif/core": "^1.14.1",
@@ -74,6 +74,7 @@
74
74
  "child_process": "^1.0.2",
75
75
  "content-disposition": "^0.5.4",
76
76
  "dotenv": "^16.0.3",
77
+ "envsub": "^4.1.0",
77
78
  "eslint-plugin-promise": "^6.0.0",
78
79
  "eslint-plugin-security": "^1.5.0",
79
80
  "eslint-plugin-sonarjs": "^0.16.0",
@@ -92,7 +93,8 @@
92
93
  "pupa": "^3.1.0",
93
94
  "source-registry-storage-adapter": "github:devx-services/source-registry-storage-adapter#main",
94
95
  "util": "^0.12.5",
95
- "uuid": "^8.3.2"
96
+ "uuid": "^8.3.2",
97
+ "yaml": "^2.4.2"
96
98
  },
97
99
  "devDependencies": {
98
100
  "@babel/eslint-parser": "^7.15.8",
@@ -0,0 +1,18 @@
1
+ {
2
+ "meshConfig": {
3
+ "sources": [
4
+ {
5
+ "name": "Commerce",
6
+ "handler": {
7
+ "graphql": {
8
+ "endpoint": "https://venia.magento.com/graphql",
9
+ "operationHeaders": {
10
+ "Authorization": "{context.secrets.Token}"
11
+ },
12
+ "useGETForQueries": true
13
+ }
14
+ }
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ HOME: 'home'
2
+ TOKEN: "dummy-token"
3
+ TOKEN: "dummy-token-duplicate"
@@ -0,0 +1,2 @@
1
+ HOME: 'home'
2
+ TOKEN: "dummy-token"
@@ -0,0 +1,4 @@
1
+ HOME: 'home'
2
+ TOKEN: "dummy-token"
3
+ batchHome: '$HOME'
4
+ USER: '${USER}'
@@ -11,6 +11,7 @@ governing permissions and limitations under the License.
11
11
  */
12
12
 
13
13
  const mockConsoleCLIInstance = {};
14
+ const crypto = require('crypto');
14
15
 
15
16
  jest.mock('axios');
16
17
  jest.mock('@adobe/aio-lib-ims');
@@ -44,19 +45,56 @@ const {
44
45
  createAPIMeshCredentials,
45
46
  subscribeCredentialToMeshService,
46
47
  getTenantFeatures,
48
+ getPublicEncryptionKey,
47
49
  } = require('../../../lib/devConsole');
48
50
 
49
51
  const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type: 'entp' };
52
+
53
+ const os = require('os');
54
+
50
55
  const selectedProject = { id: '5678', title: 'Project01' };
51
56
  const selectedWorkspace = { id: '123456789', title: 'Workspace01' };
52
57
 
58
+ jest.mock('@adobe/aio-cli-lib-console', () => ({
59
+ init: jest.fn().mockResolvedValue(mockConsoleCLIInstance),
60
+ cleanStdOut: jest.fn(),
61
+ }));
62
+
63
+ jest.mock('axios');
64
+ jest.mock('@adobe/aio-lib-ims');
65
+ jest.mock('@adobe/aio-lib-env');
66
+ jest.mock('@adobe/aio-cli-lib-console');
67
+ jest.mock('../../../helpers', () => ({
68
+ initSdk: jest.fn().mockResolvedValue({}),
69
+ initRequestId: jest.fn().mockResolvedValue({}),
70
+ promptConfirm: jest.fn().mockResolvedValue(true),
71
+ interpolateMesh: jest.fn().mockResolvedValue({}),
72
+ importFiles: jest.fn().mockResolvedValue(),
73
+ }));
74
+ jest.mock('../../../lib/devConsole');
75
+ jest.mock('chalk', () => ({
76
+ red: jest.fn(text => text), // Return the input text without any color formatting
77
+ bold: jest.fn(text => text),
78
+ }));
79
+ jest.mock('crypto');
80
+
53
81
  let logSpy = null;
54
82
  let errorLogSpy = null;
55
83
  let parseSpy = null;
84
+ let platformSpy = null;
56
85
 
57
86
  const mockIgnoreCacheFlag = Promise.resolve(true);
58
87
  const mockAutoApproveAction = Promise.resolve(false);
59
88
 
89
+ // Mock randomBytes for aesKey and iv
90
+ const mockAesKey = Buffer.from('mockAesKey');
91
+ const mockIv = Buffer.from('mockIv');
92
+ const mockEncryptedAesKey = Buffer.from('mockEncryptedAesKey');
93
+ const mockCipher = {
94
+ update: jest.fn().mockReturnValueOnce('mockEncryptedData'),
95
+ final: jest.fn().mockReturnValueOnce(''),
96
+ };
97
+
60
98
  describe('create command tests', () => {
61
99
  beforeEach(() => {
62
100
  initSdk.mockResolvedValue({
@@ -69,6 +107,12 @@ describe('create command tests', () => {
69
107
  projectName: selectedProject.title,
70
108
  });
71
109
 
110
+ global.requestId = 'dummy_request_id';
111
+
112
+ logSpy = jest.spyOn(CreateCommand.prototype, 'log');
113
+ errorLogSpy = jest.spyOn(CreateCommand.prototype, 'error');
114
+ platformSpy = jest.spyOn(os, 'platform');
115
+
72
116
  createMesh.mockResolvedValue({
73
117
  mesh: {
74
118
  meshId: 'dummy_mesh_id',
@@ -95,6 +139,7 @@ describe('create command tests', () => {
95
139
  showCloudflareURL: false,
96
140
  });
97
141
 
142
+ getPublicEncryptionKey.mockResolvedValue('dummy_public_key');
98
143
  global.requestId = 'dummy_request_id';
99
144
 
100
145
  logSpy = jest.spyOn(CreateCommand.prototype, 'log');
@@ -109,6 +154,10 @@ describe('create command tests', () => {
109
154
  });
110
155
  });
111
156
 
157
+ afterEach(() => {
158
+ platformSpy.mockRestore();
159
+ });
160
+
112
161
  test('must return proper object structure used by adobe/generator-app-api-mesh', async () => {
113
162
  parseSpy.mockResolvedValueOnce({
114
163
  args: { file: 'src/commands/__fixtures__/sample_mesh.json' },
@@ -171,6 +220,15 @@ describe('create command tests', () => {
171
220
  "parse": [Function],
172
221
  "type": "boolean",
173
222
  },
223
+ "secrets": {
224
+ "char": "s",
225
+ "default": false,
226
+ "description": "Path to secrets file",
227
+ "input": [],
228
+ "multiple": false,
229
+ "parse": [Function],
230
+ "type": "option",
231
+ },
174
232
  }
175
233
  `);
176
234
  expect(CreateCommand.aliases).toMatchInlineSnapshot(`[]`);
@@ -1831,4 +1889,244 @@ describe('create command tests', () => {
1831
1889
  'https://graph.adobe.io/api/dummy_mesh_id/graphql?api_key=dummy_api_key',
1832
1890
  );
1833
1891
  });
1892
+
1893
+ test('should return error if mesh has placeholders and the provided secrets file is invalid', async () => {
1894
+ parseSpy.mockResolvedValueOnce({
1895
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
1896
+ flags: {
1897
+ ignoreCache: mockIgnoreCacheFlag,
1898
+ autoConfirmAction: Promise.resolve(true),
1899
+ secrets: 'src/commands/__fixtures__/secrets_invalid.yaml',
1900
+ },
1901
+ });
1902
+
1903
+ const runResult = CreateCommand.run();
1904
+
1905
+ await expect(runResult).rejects.toEqual(
1906
+ new Error('Unable to import secrets. Please check the file and try again.'),
1907
+ );
1908
+
1909
+ expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
1910
+ [
1911
+ [
1912
+ "Unable to import secrets. Please check the file and try again.",
1913
+ ],
1914
+ ]
1915
+ `);
1916
+ });
1917
+
1918
+ test('should return error if mesh has placeholders and the provided secrets file is not yaml or yml', async () => {
1919
+ parseSpy.mockResolvedValueOnce({
1920
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
1921
+ flags: {
1922
+ ignoreCache: mockIgnoreCacheFlag,
1923
+ autoConfirmAction: Promise.resolve(true),
1924
+ secrets: 'src/commands/__fixtures__/.secrets_file.env',
1925
+ },
1926
+ });
1927
+
1928
+ const runResult = CreateCommand.run();
1929
+
1930
+ await expect(runResult).rejects.toEqual(
1931
+ new Error('Unable to import secrets. Please check the file and try again.'),
1932
+ );
1933
+
1934
+ expect(logSpy.mock.calls).toMatchInlineSnapshot(`
1935
+ [
1936
+ [
1937
+ "Invalid file format. Please provide a YAML file (.yaml or .yml).",
1938
+ ],
1939
+ ]
1940
+ `);
1941
+
1942
+ expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
1943
+ [
1944
+ [
1945
+ "Unable to import secrets. Please check the file and try again.",
1946
+ ],
1947
+ ]
1948
+ `);
1949
+ });
1950
+
1951
+ test('should successfully create a mesh if provided secrets file is valid', async () => {
1952
+ parseSpy.mockResolvedValueOnce({
1953
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
1954
+ flags: {
1955
+ ignoreCache: mockIgnoreCacheFlag,
1956
+ autoConfirmAction: Promise.resolve(true),
1957
+ secrets: 'src/commands/__fixtures__/secrets_valid.yaml',
1958
+ },
1959
+ });
1960
+
1961
+ crypto.randomBytes.mockReturnValueOnce(mockAesKey).mockReturnValueOnce(mockIv);
1962
+ crypto.createCipheriv.mockReturnValueOnce(mockCipher);
1963
+ crypto.publicEncrypt.mockReturnValueOnce(mockEncryptedAesKey);
1964
+
1965
+ const runResult = await CreateCommand.run();
1966
+ expect(runResult).toMatchInlineSnapshot(`
1967
+ {
1968
+ "apiKey": "dummy_api_key",
1969
+ "mesh": {
1970
+ "meshConfig": {
1971
+ "sources": [
1972
+ {
1973
+ "handler": {
1974
+ "graphql": {
1975
+ "endpoint": "<gql_endpoint>",
1976
+ },
1977
+ },
1978
+ "name": "<api_name>",
1979
+ },
1980
+ ],
1981
+ },
1982
+ "meshId": "dummy_mesh_id",
1983
+ },
1984
+ "sdkList": [
1985
+ "dummy_service",
1986
+ ],
1987
+ }
1988
+ `);
1989
+ });
1990
+
1991
+ test('should return error if ran against windows platform with batch variables', async () => {
1992
+ platformSpy.mockReturnValue('win32');
1993
+ parseSpy.mockResolvedValueOnce({
1994
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
1995
+ flags: {
1996
+ ignoreCache: mockIgnoreCacheFlag,
1997
+ autoConfirmAction: Promise.resolve(true),
1998
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
1999
+ },
2000
+ });
2001
+
2002
+ const runResult = CreateCommand.run();
2003
+
2004
+ await expect(runResult).rejects.toEqual(
2005
+ new Error('Unable to import secrets. Please check the file and try again.'),
2006
+ );
2007
+
2008
+ expect(logSpy.mock.calls).toMatchInlineSnapshot(`
2009
+ [
2010
+ [
2011
+ "Batch variables are not supported in YAML files on Windows.",
2012
+ ],
2013
+ ]
2014
+ `);
2015
+ expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`
2016
+ [
2017
+ [
2018
+ "Unable to import secrets. Please check the file and try again.",
2019
+ ],
2020
+ ]
2021
+ `);
2022
+ });
2023
+
2024
+ test('should pass if ran against linux platform with batch variables', async () => {
2025
+ platformSpy.mockReturnValue('linux');
2026
+ parseSpy.mockResolvedValueOnce({
2027
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
2028
+ flags: {
2029
+ ignoreCache: mockIgnoreCacheFlag,
2030
+ autoConfirmAction: Promise.resolve(true),
2031
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
2032
+ },
2033
+ });
2034
+
2035
+ crypto.randomBytes.mockReturnValueOnce(mockAesKey).mockReturnValueOnce(mockIv);
2036
+ crypto.createCipheriv.mockReturnValueOnce(mockCipher);
2037
+ crypto.publicEncrypt.mockReturnValueOnce(mockEncryptedAesKey);
2038
+
2039
+ const runResult = await CreateCommand.run();
2040
+ expect(runResult).toMatchInlineSnapshot(`
2041
+ {
2042
+ "apiKey": "dummy_api_key",
2043
+ "mesh": {
2044
+ "meshConfig": {
2045
+ "sources": [
2046
+ {
2047
+ "handler": {
2048
+ "graphql": {
2049
+ "endpoint": "<gql_endpoint>",
2050
+ },
2051
+ },
2052
+ "name": "<api_name>",
2053
+ },
2054
+ ],
2055
+ },
2056
+ "meshId": "dummy_mesh_id",
2057
+ },
2058
+ "sdkList": [
2059
+ "dummy_service",
2060
+ ],
2061
+ }
2062
+ `);
2063
+ });
2064
+
2065
+ test('should pass if ran against darwin(macOS) platform with batch variables', async () => {
2066
+ platformSpy.mockReturnValue('darwin');
2067
+ parseSpy.mockResolvedValueOnce({
2068
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
2069
+ flags: {
2070
+ ignoreCache: mockIgnoreCacheFlag,
2071
+ autoConfirmAction: Promise.resolve(true),
2072
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
2073
+ },
2074
+ });
2075
+
2076
+ crypto.randomBytes.mockReturnValueOnce(mockAesKey).mockReturnValueOnce(mockIv);
2077
+ crypto.createCipheriv.mockReturnValueOnce(mockCipher);
2078
+ crypto.publicEncrypt.mockReturnValueOnce(mockEncryptedAesKey);
2079
+
2080
+ const runResult = await CreateCommand.run();
2081
+ expect(runResult).toMatchInlineSnapshot(`
2082
+ {
2083
+ "apiKey": "dummy_api_key",
2084
+ "mesh": {
2085
+ "meshConfig": {
2086
+ "sources": [
2087
+ {
2088
+ "handler": {
2089
+ "graphql": {
2090
+ "endpoint": "<gql_endpoint>",
2091
+ },
2092
+ },
2093
+ "name": "<api_name>",
2094
+ },
2095
+ ],
2096
+ },
2097
+ "meshId": "dummy_mesh_id",
2098
+ },
2099
+ "sdkList": [
2100
+ "dummy_service",
2101
+ ],
2102
+ }
2103
+ `);
2104
+ });
2105
+
2106
+ test('should return error if secrets file is valid but public key for encryption is empty', async () => {
2107
+ parseSpy.mockResolvedValueOnce({
2108
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
2109
+ flags: {
2110
+ ignoreCache: mockIgnoreCacheFlag,
2111
+ autoConfirmAction: Promise.resolve(true),
2112
+ secrets: 'src/commands/__fixtures__/secrets_valid.yaml',
2113
+ },
2114
+ });
2115
+ getPublicEncryptionKey.mockResolvedValue('');
2116
+
2117
+ crypto.randomBytes.mockReturnValueOnce(mockAesKey).mockReturnValueOnce(mockIv);
2118
+ crypto.createCipheriv.mockReturnValueOnce(mockCipher);
2119
+
2120
+ const runResult = CreateCommand.run();
2121
+ await expect(runResult).rejects.toEqual(
2122
+ new Error('Unable to import secrets. Please check the file and try again.'),
2123
+ );
2124
+ expect(logSpy.mock.calls).toMatchInlineSnapshot(`
2125
+ [
2126
+ [
2127
+ "Unable to encrypt secerts. Invalid Public Key.",
2128
+ ],
2129
+ ]
2130
+ `);
2131
+ });
1834
2132
  });
@@ -18,6 +18,7 @@ const {
18
18
  promptConfirm,
19
19
  setUpTenantFiles,
20
20
  initSdk,
21
+ writeSecretsFile,
21
22
  } = require('../../../helpers');
22
23
  const { getMeshId, getMeshArtifact } = require('../../../lib/devConsole');
23
24
  require('@adobe-apimesh/mesh-builder');
@@ -35,12 +36,16 @@ jest.mock('../../../helpers', () => ({
35
36
  importFiles: jest.fn().mockResolvedValue(),
36
37
  promptConfirm: jest.fn().mockResolvedValue(true),
37
38
  setUpTenantFiles: jest.fn().mockResolvedValue(),
39
+ writeSecretsFile: jest.fn().mockResolvedValue(),
38
40
  }));
39
41
 
40
42
  jest.mock('../../../lib/devConsole', () => ({
41
43
  getMeshId: jest.fn().mockResolvedValue('mockMeshId'),
42
44
  getMeshArtifact: jest.fn().mockResolvedValue(),
43
45
  }));
46
+ jest.mock('chalk', () => ({
47
+ red: jest.fn(text => text), // Return the input text without any color formatting
48
+ }));
44
49
 
45
50
  jest.mock('@adobe-apimesh/mesh-builder', () => {
46
51
  return {
@@ -55,12 +60,15 @@ jest.mock('@adobe-apimesh/mesh-builder', () => {
55
60
  let logSpy = null;
56
61
  let errorLogSpy = null;
57
62
  let parseSpy = null;
63
+ let platformSpy = null;
58
64
 
59
65
  const originalEnv = {
60
66
  API_MESH_TIER: 'NON-TI',
61
67
  };
62
68
 
63
69
  const defaultPort = 5000;
70
+ const os = require('os');
71
+
64
72
  describe('run command tests', () => {
65
73
  beforeEach(() => {
66
74
  global.requestId = 'dummy_request_id';
@@ -68,10 +76,14 @@ describe('run command tests', () => {
68
76
  logSpy = jest.spyOn(RunCommand.prototype, 'log');
69
77
  errorLogSpy = jest.spyOn(RunCommand.prototype, 'error');
70
78
  parseSpy = jest.spyOn(RunCommand.prototype, 'parse');
79
+ platformSpy = jest.spyOn(os, 'platform');
71
80
  process.env = {
72
81
  ...originalEnv,
73
82
  };
74
83
  });
84
+ afterEach(() => {
85
+ platformSpy.mockRestore();
86
+ });
75
87
 
76
88
  test('snapshot run command description', () => {
77
89
  expect(RunCommand.description).toMatchInlineSnapshot(
@@ -121,6 +133,15 @@ describe('run command tests', () => {
121
133
  "parse": [Function],
122
134
  "type": "option",
123
135
  },
136
+ "secrets": {
137
+ "char": "s",
138
+ "default": false,
139
+ "description": "Path to secrets file",
140
+ "input": [],
141
+ "multiple": false,
142
+ "parse": [Function],
143
+ "type": "option",
144
+ },
124
145
  "select": {
125
146
  "allowNo": false,
126
147
  "default": false,
@@ -836,4 +857,109 @@ describe('run command tests', () => {
836
857
  );
837
858
  expect(setUpTenantFiles).toHaveBeenCalled();
838
859
  });
860
+
861
+ test('should return error for run command if mesh has placeholders and the provided secrets file is invalid', async () => {
862
+ parseSpy.mockResolvedValueOnce({
863
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
864
+ flags: {
865
+ secrets: 'src/commands/__fixtures__/secrets_invalid.yaml',
866
+ },
867
+ });
868
+
869
+ const runResult = RunCommand.run();
870
+
871
+ await expect(runResult).rejects.toEqual(
872
+ new Error('Unable to import secrets. Please check the file and try again.'),
873
+ );
874
+ });
875
+
876
+ test('should return error for run command if mesh has placeholders and the provided secrets file is not yaml or yml', async () => {
877
+ parseSpy.mockResolvedValueOnce({
878
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
879
+ flags: {
880
+ secrets: 'src/commands/__fixtures__/.secrets_file.env',
881
+ },
882
+ });
883
+
884
+ const runResult = RunCommand.run();
885
+
886
+ await expect(runResult).rejects.toEqual(
887
+ new Error('Unable to import secrets. Please check the file and try again.'),
888
+ );
889
+
890
+ expect(logSpy.mock.calls).toMatchInlineSnapshot(`
891
+ [
892
+ [
893
+ "Invalid file format. Please provide a YAML file (.yaml or .yml).",
894
+ ],
895
+ ]
896
+ `);
897
+ });
898
+
899
+ test('should successfully run the mesh if provided secrets file is valid', async () => {
900
+ parseSpy.mockResolvedValueOnce({
901
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
902
+ flags: {
903
+ secrets: 'src/commands/__fixtures__/secrets_valid.yaml',
904
+ debug: false,
905
+ },
906
+ });
907
+
908
+ await RunCommand.run();
909
+ expect(writeSecretsFile).toHaveBeenCalled();
910
+ expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
911
+ });
912
+
913
+ test('should return error if ran with secrets against windows platform with batch variables', async () => {
914
+ platformSpy.mockReturnValue('win32');
915
+ parseSpy.mockResolvedValueOnce({
916
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
917
+ flags: {
918
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
919
+ },
920
+ });
921
+
922
+ const runResult = RunCommand.run();
923
+ await expect(runResult).rejects.toEqual(
924
+ new Error('Unable to import secrets. Please check the file and try again.'),
925
+ );
926
+
927
+ expect(logSpy.mock.calls).toMatchInlineSnapshot(`
928
+ [
929
+ [
930
+ "Batch variables are not supported in YAML files on Windows.",
931
+ ],
932
+ ]
933
+ `);
934
+ });
935
+
936
+ test('should pass if ran with secrets against linux platform with batch variables', async () => {
937
+ platformSpy.mockReturnValue('linux');
938
+ parseSpy.mockResolvedValueOnce({
939
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
940
+ flags: {
941
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
942
+ debug: false,
943
+ },
944
+ });
945
+
946
+ await RunCommand.run();
947
+ expect(writeSecretsFile).toHaveBeenCalled();
948
+ expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
949
+ });
950
+
951
+ test('should pass if ran with secrets against darwin(macOS) platform with batch variables', async () => {
952
+ platformSpy.mockReturnValue('darwin');
953
+ parseSpy.mockResolvedValueOnce({
954
+ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' },
955
+ flags: {
956
+ secrets: 'src/commands/__fixtures__/secrets_with_batch_variables.yaml',
957
+ debug: false,
958
+ },
959
+ });
960
+
961
+ await RunCommand.run();
962
+ expect(writeSecretsFile).toHaveBeenCalled();
963
+ expect(startGraphqlServer).toHaveBeenCalledWith(expect.anything(), defaultPort, false);
964
+ });
839
965
  });
@@ -19,11 +19,15 @@ const {
19
19
  jsonFlag,
20
20
  getFilesInMeshConfig,
21
21
  envFileFlag,
22
+ secretsFlag,
22
23
  checkPlaceholders,
23
24
  readFileContents,
24
25
  validateAndInterpolateMesh,
26
+ interpolateSecrets,
27
+ validateSecretsFile,
28
+ encryptSecrets,
25
29
  } = require('../../utils');
26
- const { createMesh, getTenantFeatures } = require('../../lib/devConsole');
30
+ const { createMesh, getTenantFeatures, getPublicEncryptionKey } = require('../../lib/devConsole');
27
31
  const { buildEdgeMeshUrl, buildMeshUrl } = require('../../urlBuilder');
28
32
 
29
33
  class CreateCommand extends Command {
@@ -33,6 +37,7 @@ class CreateCommand extends Command {
33
37
  autoConfirmAction: autoConfirmActionFlag,
34
38
  json: jsonFlag,
35
39
  env: envFileFlag,
40
+ secrets: secretsFlag,
36
41
  };
37
42
 
38
43
  static enableJsonFlag = true;
@@ -53,6 +58,7 @@ class CreateCommand extends Command {
53
58
  const ignoreCache = await flags.ignoreCache;
54
59
  const autoConfirmAction = await flags.autoConfirmAction;
55
60
  const envFilePath = await flags.env;
61
+ const secretsFilePath = await flags.secrets;
56
62
  const {
57
63
  imsOrgId,
58
64
  imsOrgCode,
@@ -103,6 +109,20 @@ class CreateCommand extends Command {
103
109
  }
104
110
  }
105
111
 
112
+ // if secrets is present, include that in data.secrets
113
+ if (secretsFilePath) {
114
+ try {
115
+ await validateSecretsFile(secretsFilePath);
116
+ const secretsData = await interpolateSecrets(secretsFilePath, this);
117
+ const publicKey = await getPublicEncryptionKey(imsOrgCode);
118
+ const encryptedSecrets = await encryptSecrets(publicKey, secretsData);
119
+ data.secrets = encryptedSecrets;
120
+ } catch (err) {
121
+ this.log(err.message);
122
+ this.error('Unable to import secrets. Please check the file and try again.');
123
+ }
124
+ }
125
+
106
126
  let shouldContinue = true;
107
127
 
108
128
  if (!autoConfirmAction) {
@@ -15,11 +15,14 @@ const {
15
15
  debugFlag,
16
16
  selectFlag,
17
17
  envFileFlag,
18
+ secretsFlag,
18
19
  autoConfirmActionFlag,
19
20
  readFileContents,
20
21
  validateAndInterpolateMesh,
21
22
  checkPlaceholders,
22
23
  getFilesInMeshConfig,
24
+ validateSecretsFile,
25
+ interpolateSecrets,
23
26
  } = require('../../utils');
24
27
  const meshBuilder = require('@adobe-apimesh/mesh-builder');
25
28
  const fs = require('fs');
@@ -31,6 +34,7 @@ const {
31
34
  startGraphqlServer,
32
35
  importFiles,
33
36
  setUpTenantFiles,
37
+ writeSecretsFile,
34
38
  } = require('../../helpers');
35
39
  const logger = require('../../classes/logger');
36
40
  const { getMeshId, getMeshArtifact } = require('../../lib/devConsole');
@@ -55,6 +59,7 @@ class RunCommand extends Command {
55
59
  env: envFileFlag,
56
60
  autoConfirmAction: autoConfirmActionFlag,
57
61
  select: selectFlag,
62
+ secrets: secretsFlag,
58
63
  };
59
64
 
60
65
  static enableJsonFlag = true;
@@ -67,6 +72,7 @@ class RunCommand extends Command {
67
72
  logger.info(`RequestId: ${global.requestId}`);
68
73
 
69
74
  const { args, flags } = await this.parse(RunCommand);
75
+ const secretsFilePath = await flags.secrets;
70
76
 
71
77
  //Initialize the meshId based on
72
78
  let meshId = null;
@@ -165,6 +171,17 @@ class RunCommand extends Command {
165
171
  }
166
172
 
167
173
  let portNo;
174
+ //secrets management
175
+ if (secretsFilePath) {
176
+ try {
177
+ await validateSecretsFile(secretsFilePath);
178
+ const stringifiedSecrets = await interpolateSecrets(secretsFilePath, this);
179
+ await writeSecretsFile(stringifiedSecrets, meshId);
180
+ } catch (error) {
181
+ this.log(error.message);
182
+ this.error('Unable to import secrets. Please check the file and try again.');
183
+ }
184
+ }
168
185
 
169
186
  //To set the port number using the environment file
170
187
  if (process.env.PORT !== undefined) {
@@ -17,12 +17,16 @@ const {
17
17
  ignoreCacheFlag,
18
18
  autoConfirmActionFlag,
19
19
  envFileFlag,
20
+ secretsFlag,
20
21
  checkPlaceholders,
21
22
  readFileContents,
22
23
  validateAndInterpolateMesh,
23
24
  getFilesInMeshConfig,
25
+ interpolateSecrets,
26
+ validateSecretsFile,
27
+ encryptSecrets,
24
28
  } = require('../../utils');
25
- const { getMeshId, updateMesh } = require('../../lib/devConsole');
29
+ const { getMeshId, updateMesh, getPublicEncryptionKey } = require('../../lib/devConsole');
26
30
 
27
31
  class UpdateCommand extends Command {
28
32
  static args = [{ name: 'file' }];
@@ -30,6 +34,7 @@ class UpdateCommand extends Command {
30
34
  ignoreCache: ignoreCacheFlag,
31
35
  autoConfirmAction: autoConfirmActionFlag,
32
36
  env: envFileFlag,
37
+ secrets: secretsFlag,
33
38
  };
34
39
 
35
40
  async run() {
@@ -48,12 +53,19 @@ class UpdateCommand extends Command {
48
53
  const ignoreCache = await flags.ignoreCache;
49
54
  const autoConfirmAction = await flags.autoConfirmAction;
50
55
  const envFilePath = await flags.env;
51
-
52
- const { imsOrgId, projectId, workspaceId, orgName, projectName, workspaceName } = await initSdk(
53
- {
54
- ignoreCache,
55
- },
56
- );
56
+ const secretsFilePath = await flags.secrets;
57
+
58
+ const {
59
+ imsOrgId,
60
+ imsOrgCode,
61
+ projectId,
62
+ workspaceId,
63
+ orgName,
64
+ projectName,
65
+ workspaceName,
66
+ } = await initSdk({
67
+ ignoreCache,
68
+ });
57
69
 
58
70
  //Input the mesh data from the input file
59
71
  let inputMeshData = await readFileContents(args.file, this, 'mesh');
@@ -103,6 +115,20 @@ class UpdateCommand extends Command {
103
115
  }
104
116
  }
105
117
 
118
+ // if secrets is present, include that in data.secrets
119
+ if (secretsFilePath) {
120
+ try {
121
+ await validateSecretsFile(secretsFilePath);
122
+ const secretsData = await interpolateSecrets(secretsFilePath, this);
123
+ const publicKey = await getPublicEncryptionKey(imsOrgCode);
124
+ const encryptedSecrets = await encryptSecrets(publicKey, secretsData);
125
+ data.secrets = encryptedSecrets;
126
+ } catch (err) {
127
+ this.log(err.message);
128
+ this.error('Unable to import secrets. Please check the file and try again.');
129
+ }
130
+ }
131
+
106
132
  if (meshId) {
107
133
  let shouldContinue = true;
108
134
 
package/src/helpers.js CHANGED
@@ -860,6 +860,26 @@ async function setUpTenantFiles(meshId) {
860
860
  }
861
861
  }
862
862
 
863
+ /**
864
+ * This function is to create secrets.yaml in mesh-artifacts for respective meshId. Used for local development run command
865
+ *
866
+ * @secretsData secretsData
867
+ * @meshId meshId
868
+ */
869
+ async function writeSecretsFile(secretsData, meshId) {
870
+ if (!fs.existsSync(path.resolve(process.cwd(), 'mesh-artifact', meshId))) {
871
+ throw new Error(`Unexpected Error: issue creating secrets file.`);
872
+ }
873
+ try {
874
+ const secretsFileName = 'secrets.yaml';
875
+ const folderPath = path.join(process.cwd(), 'mesh-artifact', meshId);
876
+ const filePath = path.join(folderPath, secretsFileName);
877
+ fs.writeFileSync(filePath, secretsData);
878
+ } catch (error) {
879
+ throw new Error(error.message);
880
+ }
881
+ }
882
+
863
883
  module.exports = {
864
884
  objToString,
865
885
  promptInput,
@@ -876,4 +896,5 @@ module.exports = {
876
896
  updateFilesArray,
877
897
  startGraphqlServer,
878
898
  setUpTenantFiles,
899
+ writeSecretsFile,
879
900
  };
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const util = require('util');
11
11
  const exec = util.promisify(require('child_process').exec);
12
12
  const contentDisposition = require('content-disposition');
13
+ const chalk = require('chalk');
13
14
 
14
15
  const { DEV_CONSOLE_TRANSPORTER_API_KEY, SMS_BASE_URL } = CONSTANTS;
15
16
 
@@ -968,6 +969,52 @@ const getMeshDeployments = async (organizationCode, projectId, workspaceId, mesh
968
969
  }
969
970
  };
970
971
 
972
+ /**
973
+ * Gets the public key to encrypt secrets.
974
+ *
975
+ * This request bypasses the Dev Console and is sent directly to the Schema Management Service.
976
+ * As a result, we provide the publicKey used for secrets encryption.
977
+ * The near-term goal is to stop using Dev Console as a proxy for all routes.
978
+ * @param organizationCode
979
+ * @returns string
980
+ */
981
+ const getPublicEncryptionKey = async organizationCode => {
982
+ const { accessToken, apiKey } = await getDevConsoleConfig();
983
+ const config = {
984
+ method: 'get',
985
+ url: `${SMS_BASE_URL}/organizations/${organizationCode}/getPublicKey?API_KEY=${apiKey}`,
986
+ headers: {
987
+ 'Authorization': `Bearer ${accessToken}`,
988
+ 'x-request-id': global.requestId,
989
+ },
990
+ };
991
+ logger.info(
992
+ 'Initiating GET %s',
993
+ `${SMS_BASE_URL}/organizations/${organizationCode}/getPublicKey?API_KEY=${apiKey}`,
994
+ );
995
+ try {
996
+ const response = await axios(config);
997
+
998
+ logger.info('Response from GET %s', response.status);
999
+ if (response.status == 200) {
1000
+ let publicKey = '';
1001
+ logger.info(`Public key for encryption: ${objToString(response, ['data'])}`);
1002
+ if (response.data.publicKey) {
1003
+ publicKey = response.data.publicKey.replace(/\\n/g, '\n'); //correcting public key format
1004
+ }
1005
+ return publicKey;
1006
+ } else {
1007
+ let errorMessage = `Failed to load encryption keys. Please contact support.`;
1008
+ logger.error(`${errorMessage}. Received ${response.status}, expected 200`);
1009
+ throw new Error(chalk.red(errorMessage));
1010
+ }
1011
+ } catch (error) {
1012
+ let errorMessage = `Something went wrong while encrypting secrets. Please try again.`;
1013
+ logger.error(errorMessage);
1014
+ throw new Error(chalk.red(errorMessage));
1015
+ }
1016
+ };
1017
+
971
1018
  module.exports = {
972
1019
  getApiKeyCredential,
973
1020
  describeMesh,
@@ -984,4 +1031,5 @@ module.exports = {
984
1031
  getMeshArtifact,
985
1032
  getTenantFeatures,
986
1033
  getMeshDeployments,
1034
+ getPublicEncryptionKey,
987
1035
  };
package/src/server.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  removeRequestHeaders,
14
14
  prepSourceResponseHeaders,
15
15
  processResponseHeaders,
16
+ readSecretsFile,
16
17
  } = require('./serverUtils');
17
18
 
18
19
  let yogaServer = null;
@@ -64,6 +65,8 @@ const getYogaServer = async () => {
64
65
  const tenantMesh = await getBuiltMesh();
65
66
  const corsOptions = getCORSOptions();
66
67
 
68
+ const secrets = readSecretsFile(meshId);
69
+
67
70
  logger.info('Creating graphQL server');
68
71
 
69
72
  meshConfig = readMeshConfig(meshId);
@@ -73,6 +76,10 @@ const getYogaServer = async () => {
73
76
  graphqlEndpoint: `/graphql`,
74
77
  graphiql: true,
75
78
  cors: corsOptions,
79
+ context: initialContext => ({
80
+ ...initialContext,
81
+ secrets,
82
+ }),
76
83
  });
77
84
 
78
85
  return yogaServer;
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const LRUCache = require('lru-cache');
4
4
  const logger = require('./classes/logger');
5
+ const YAML = require('yaml');
5
6
 
6
7
  const headersCache = new LRUCache({
7
8
  max: parseInt(process.env.CACHE_OPT_MAX || '500', 10),
@@ -315,9 +316,29 @@ function ccDirectivesToString(directives) {
315
316
  return chStr.toString();
316
317
  }
317
318
 
319
+ /**
320
+ * Returns secrets content from artifacts
321
+ * @param meshId
322
+ * @returns
323
+ */
324
+ function readSecretsFile(meshId) {
325
+ let secrets = {};
326
+ try {
327
+ const filePath = path.resolve(process.cwd(), 'mesh-artifact', `${meshId}`, 'secrets.yaml');
328
+ if (fs.existsSync(filePath)) {
329
+ secrets = YAML.parse(fs.readFileSync(filePath, 'utf8'));
330
+ }
331
+ } catch (error) {
332
+ logger.error('Unexpected error: unable to locate secrets file in mesh artifacts.');
333
+ throw new Error(error.message);
334
+ }
335
+ return secrets;
336
+ }
337
+
318
338
  module.exports = {
319
339
  readMeshConfig,
320
340
  removeRequestHeaders,
321
341
  prepSourceResponseHeaders,
322
342
  processResponseHeaders,
343
+ readSecretsFile,
323
344
  };
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',
@@ -391,6 +402,136 @@ async function validateAndInterpolateMesh(inputMeshData, envFilePath, command) {
391
402
  }
392
403
  }
393
404
 
405
+ /**
406
+ * Validate secrets file
407
+ *
408
+ * @param secretsFile Validates that secrets file extension is in yaml
409
+ */
410
+ async function validateSecretsFile(secretsFile) {
411
+ try {
412
+ const validExtensions = ['.yaml', '.yml'];
413
+ const fileExtension = secretsFile.split('.').pop().toLowerCase();
414
+ if (!validExtensions.includes('.' + fileExtension)) {
415
+ throw new Error(
416
+ chalk.red('Invalid file format. Please provide a YAML file (.yaml or .yml).'),
417
+ );
418
+ }
419
+ } catch (error) {
420
+ logger.error(error.message);
421
+ throw new Error(error.message);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Read the secrets file, checks validation and interpolate mesh
427
+ *
428
+ * @param secretsFilePath Secrets file path
429
+ * @param command
430
+ */
431
+ async function interpolateSecrets(secretsFilePath, command) {
432
+ try {
433
+ const secretsContent = await readFileContents(secretsFilePath, command, 'secrets');
434
+
435
+ // Check if environment variables are used in the file content
436
+ if (os.platform() === 'win32' && /\$({)?[a-zA-Z_][a-zA-Z0-9_]*}?/.test(secretsContent)) {
437
+ throw new Error(chalk.red('Batch variables are not supported in YAML files on Windows.'));
438
+ }
439
+ const secrets = await parseSecrets(secretsContent);
440
+ return secrets;
441
+ } catch (err) {
442
+ logger.error(err.message);
443
+ throw new Error(err.message);
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Parse secrets YAML content.
449
+ *
450
+ * @param secretsFilePath Secrets file path
451
+ */
452
+ async function parseSecrets(secretsContent) {
453
+ try {
454
+ const envParserConfig = {
455
+ outputFile: null,
456
+ options: {
457
+ all: false,
458
+ diff: false,
459
+ protect: false,
460
+ syntax: 'dollar-both',
461
+ },
462
+ cli: false,
463
+ };
464
+ const compiledSecretsFileContent = parseEnv(secretsContent, envParserConfig);
465
+ const parsedSecrets = YAML.parse(compiledSecretsFileContent);
466
+ //check if secrets file is empty
467
+ if (!parsedSecrets) {
468
+ throw new Error(chalk.red('Invalid YAML file contents. Please verify and try again.'));
469
+ }
470
+ //check if parsedSecrets is string and not in k:v pair
471
+ if (typeof parsedSecrets === 'string') {
472
+ throw new Error(chalk.red('Please provide a valid YAML in key:value format.'));
473
+ }
474
+ const secretsYamlString = YAML.stringify(parsedSecrets);
475
+ return secretsYamlString; //TODO: here we will encrypt secrets and return.
476
+ } catch (err) {
477
+ throw new Error(chalk.red(getSecretsYamlParseError(err)));
478
+ }
479
+ }
480
+
481
+ /**
482
+ * This function returns user friendly errors that occurs while YAML.parse
483
+ *
484
+ * @param error errors from YAML.parse
485
+ */
486
+ function getSecretsYamlParseError(error) {
487
+ if (error.code === 'BAD_INDENT') {
488
+ return 'Invalid YAML - Bad Indentation: ' + error.message;
489
+ } else if (error.code === 'DUPLICATE_KEY') {
490
+ return 'Invalid YAML - Found Duplicate Keys: ' + error.message;
491
+ } else {
492
+ return 'Unexpected Error: ' + error.message;
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Performs hybrid encryption of secrets(AES + RSA)
498
+ *
499
+ * @param publicKey Public key for (AES + RSA) encryption
500
+ * @param secrets Secrets Data that needs encryption
501
+ */
502
+ async function encryptSecrets(publicKey, secrets) {
503
+ if (!publicKey || typeof publicKey !== 'string' || !publicKey.trim()) {
504
+ throw new Error(chalk.red('Unable to encrypt secerts. Invalid Public Key.'));
505
+ }
506
+ try {
507
+ // Generate a random AES key and IV
508
+ const aesKey = crypto.randomBytes(32); // 256-bit key for AES-256
509
+ const iv = crypto.randomBytes(16); // Initialization vector
510
+ // Encrypt the secrets using AES-256-CBC
511
+ const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
512
+ let encryptedData = cipher.update(secrets, 'utf8', 'base64');
513
+ encryptedData += cipher.final('base64');
514
+ // Encrypt the AES key using RSA with OAEP padding
515
+ const encryptedAesKey = crypto.publicEncrypt(
516
+ {
517
+ key: publicKey,
518
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
519
+ },
520
+ aesKey,
521
+ );
522
+ // Package the encrypted AES key, IV, and encrypted data
523
+ const encryptedPackage = {
524
+ iv: iv.toString('base64'),
525
+ key: encryptedAesKey.toString('base64'),
526
+ data: encryptedData,
527
+ };
528
+ return JSON.stringify(encryptedPackage);
529
+ } catch (error) {
530
+ logger.error('Unable to encrypt secrets. Please try again. :', error.message);
531
+ throw new Error(`Unable to encrypt secerts. ${error.message}`);
532
+ }
533
+ }
534
+
394
535
  module.exports = {
395
536
  ignoreCacheFlag,
396
537
  autoConfirmActionFlag,
@@ -405,4 +546,8 @@ module.exports = {
405
546
  portNoFlag,
406
547
  debugFlag,
407
548
  selectFlag,
549
+ secretsFlag,
550
+ interpolateSecrets,
551
+ validateSecretsFile,
552
+ encryptSecrets,
408
553
  };