@extrahorizon/exh-cli 1.8.2 → 1.9.0-dev-85-4c62b0a

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Extra Horizon CLI changelog
2
2
 
3
+ ### v1.9.0
4
+ * Introduced the `executionCredentials` field in the task configuration:
5
+ * Specify the permissions your task needs
6
+ * The CLI automatically creates a user, role, and credentials for your task
7
+ * These credentials are injected as environment variables into the task automatically
8
+
3
9
  ### v1.8.2
4
10
  * Updated the ExH SDK to `8.6.0` to fix a security warning from `axios`
5
11
 
@@ -5,6 +5,7 @@ const fs = require("fs/promises");
5
5
  const chalk = require("chalk");
6
6
  const constants_1 = require("../../constants");
7
7
  const util_1 = require("../../helpers/util");
8
+ const authRepository = require("../../repositories/auth");
8
9
  const functionRepository = require("../../repositories/functions");
9
10
  const taskConfig_1 = require("./taskConfig");
10
11
  const util_2 = require("./util");
@@ -96,13 +97,31 @@ async function syncSingleTask(sdk, config) {
96
97
  if (config.memoryLimit) {
97
98
  request.memoryLimit = config.memoryLimit;
98
99
  }
99
- if (config.environment) {
100
- request.environmentVariables =
101
- Object.entries(config.environment).reduce((prev, curr) => ({ ...prev, [curr[0]]: { value: curr[1] } }), {});
102
- }
103
100
  if (config.retryPolicy) {
104
101
  request.retryPolicy = config.retryPolicy;
105
102
  }
103
+ if (config.executionCredentials) {
104
+ const credentials = await (0, util_2.syncFunctionUser)(sdk, {
105
+ taskName: config.name,
106
+ targetEmail: config.executionCredentials.email,
107
+ targetPermissions: config.executionCredentials.permissions,
108
+ });
109
+ config.environment = {
110
+ ...config.environment,
111
+ API_HOST: authRepository.getHost(sdk),
112
+ API_OAUTH_CONSUMER_KEY: process.env.API_OAUTH_CONSUMER_KEY,
113
+ API_OAUTH_CONSUMER_SECRET: process.env.API_OAUTH_CONSUMER_SECRET,
114
+ API_OAUTH_TOKEN: credentials.token,
115
+ API_OAUTH_TOKEN_SECRET: credentials.tokenSecret,
116
+ };
117
+ }
118
+ if (config.environment) {
119
+ const environmentVariables = {};
120
+ for (const [key, value] of Object.entries(config.environment)) {
121
+ environmentVariables[key] = { value };
122
+ }
123
+ request.environmentVariables = environmentVariables;
124
+ }
106
125
  if (myFunction === undefined) {
107
126
  await functionRepository.create(sdk, request);
108
127
  console.log(chalk.green('Successfully created task', config.name));
@@ -17,6 +17,10 @@ export interface TaskConfig {
17
17
  enabled: boolean;
18
18
  errorsToRetry: string[];
19
19
  };
20
+ executionCredentials?: {
21
+ email?: string;
22
+ permissions: string[];
23
+ };
20
24
  }
21
25
  export declare function assertExecutionPermission(mode: string): asserts mode is permissionModes | undefined;
22
26
  export declare function validateConfig(config: TaskConfig): Promise<boolean>;
@@ -26,6 +26,10 @@ const taskConfigSchema = Joi.object({
26
26
  }),
27
27
  environment: Joi.object().pattern(/.*/, Joi.string()),
28
28
  executionPermission: Joi.string().valid(...Object.values(permissionModes)),
29
+ executionCredentials: Joi.object({
30
+ email: Joi.string(),
31
+ permissions: Joi.array().items(Joi.string()).required(),
32
+ }),
29
33
  });
30
34
  function assertExecutionPermission(mode) {
31
35
  if (mode !== undefined && !Object.values(permissionModes).includes(mode)) {
@@ -1 +1,10 @@
1
+ import { OAuth1Client } from '@extrahorizon/javascript-sdk';
1
2
  export declare function zipFileFromDirectory(path: string): Promise<string>;
3
+ export declare function syncFunctionUser(sdk: OAuth1Client, data: {
4
+ taskName: string;
5
+ targetEmail?: string;
6
+ targetPermissions: string[];
7
+ }): Promise<{
8
+ token: any;
9
+ tokenSecret: any;
10
+ }>;
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.zipFileFromDirectory = void 0;
3
+ exports.syncFunctionUser = exports.zipFileFromDirectory = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const os_1 = require("os");
6
+ const javascript_sdk_1 = require("@extrahorizon/javascript-sdk");
6
7
  const archiver = require("archiver");
8
+ const chalk = require("chalk");
7
9
  const uuid_1 = require("uuid");
10
+ const authRepository = require("../../repositories/auth");
11
+ const functionRepository = require("../../repositories/functions");
12
+ const userRepository = require("../../repositories/user");
8
13
  async function zipFileFromDirectory(path) {
9
14
  return new Promise((res, rej) => {
10
15
  const tmpPath = `${(0, os_1.tmpdir)()}/${(0, uuid_1.v4)()}`;
@@ -25,3 +30,97 @@ async function zipFileFromDirectory(path) {
25
30
  });
26
31
  }
27
32
  exports.zipFileFromDirectory = zipFileFromDirectory;
33
+ async function syncFunctionUser(sdk, data) {
34
+ const { taskName, targetEmail, targetPermissions } = data;
35
+ const email = targetEmail || `exh.tasks+${taskName}@extrahorizon.com`;
36
+ validateEmail(email);
37
+ const password = `0Oo-${(0, uuid_1.v4)()}`;
38
+ const roleName = `exh.tasks.${taskName}`;
39
+ const role = await syncRoleWithPermissions(sdk, taskName, roleName, targetPermissions);
40
+ let user = await userRepository.findUserByEmail(sdk, email);
41
+ if (!user) {
42
+ console.log(chalk.white('⚙️ Creating a user for the task'));
43
+ const { emailAvailable } = await sdk.users.isEmailAvailable(email);
44
+ if (!emailAvailable) {
45
+ throw new Error('❌ The user could not be created as the email address is already in use');
46
+ }
47
+ user = await sdk.users.createAccount({
48
+ firstName: `${taskName}`,
49
+ lastName: 'exh.tasks',
50
+ email,
51
+ password,
52
+ phoneNumber: '0000000000',
53
+ language: 'EN',
54
+ });
55
+ console.log(chalk.green('✅ Successfully created a user for task'));
56
+ await assignRoleToUser(sdk, user.id, role.id);
57
+ return await createOAuth1Tokens(sdk, email, password);
58
+ }
59
+ console.log(chalk.white('⚙️ Checking for the existing users credentials'));
60
+ const currentFunction = await functionRepository.findByName(sdk, taskName);
61
+ const hasExistingCredentials = (currentFunction?.environmentVariables?.API_HOST?.value &&
62
+ currentFunction?.environmentVariables?.API_OAUTH_TOKEN_SECRET?.value &&
63
+ currentFunction?.environmentVariables?.API_OAUTH_TOKEN?.value &&
64
+ currentFunction?.environmentVariables?.API_OAUTH_CONSUMER_KEY?.value &&
65
+ currentFunction?.environmentVariables?.API_OAUTH_CONSUMER_SECRET?.value);
66
+ if (!hasExistingCredentials) {
67
+ throw new Error('❌ No credentials were found for the existing user');
68
+ }
69
+ const userRole = user.roles.find(({ name }) => name === roleName);
70
+ if (!userRole) {
71
+ await assignRoleToUser(sdk, user.id, role.id);
72
+ }
73
+ console.log(chalk.green('✅ Using existing credentials for the user'));
74
+ return {
75
+ token: currentFunction.environmentVariables.API_OAUTH_TOKEN.value,
76
+ tokenSecret: currentFunction.environmentVariables.API_OAUTH_TOKEN_SECRET.value,
77
+ };
78
+ }
79
+ exports.syncFunctionUser = syncFunctionUser;
80
+ async function syncRoleWithPermissions(sdk, taskName, roleName, targetPermissions) {
81
+ console.log(chalk.white('⚙️ Checking if the role exists'));
82
+ let role = await sdk.users.globalRoles.findByName(roleName);
83
+ if (!role) {
84
+ console.log(chalk.white('⚙️ Role does not exist, creating a new role'));
85
+ role = await sdk.users.globalRoles.create({
86
+ name: roleName,
87
+ description: `A role created by the CLI for the execution of the task ${taskName}`,
88
+ });
89
+ console.log(chalk.white('⚙️ Assigning permissions to the role'));
90
+ await sdk.users.globalRoles.addPermissions((0, javascript_sdk_1.rqlBuilder)().eq('name', roleName).build(), { permissions: targetPermissions });
91
+ console.log(chalk.green('✅ Successfully assigned permissions to the role'));
92
+ return role;
93
+ }
94
+ const currentPermissions = role.permissions?.flatMap(permission => permission.name) || [];
95
+ const permissionsToAdd = targetPermissions.filter(targetPermission => !currentPermissions.includes(targetPermission));
96
+ const permissionsToRemove = currentPermissions.filter(currentPermission => !targetPermissions.includes(currentPermission));
97
+ if (permissionsToAdd.length > 0) {
98
+ console.log(chalk.white('⚙️ Adding missing permissions to the role'));
99
+ await sdk.users.globalRoles.addPermissions((0, javascript_sdk_1.rqlBuilder)().eq('name', roleName).build(), { permissions: permissionsToAdd });
100
+ console.log(chalk.green('✅ Successfully added missing permissions to the role'));
101
+ }
102
+ if (permissionsToRemove.length > 0) {
103
+ console.log(chalk.white('⚙️ Removing excess permissions from the role'));
104
+ await sdk.users.globalRoles.removePermissions((0, javascript_sdk_1.rqlBuilder)().eq('name', roleName).build(), { permissions: permissionsToRemove });
105
+ console.log(chalk.green('✅ Successfully removed excess permissions from the role'));
106
+ }
107
+ return role;
108
+ }
109
+ async function assignRoleToUser(sdk, userId, roleId) {
110
+ console.log(chalk.white('⚙️ Assigning the role to the user'));
111
+ await sdk.users.globalRoles.addToUsers((0, javascript_sdk_1.rqlBuilder)().eq('id', userId).build(), { roles: [roleId] });
112
+ console.log(chalk.green('✅ Successfully assigned the role to the user'));
113
+ }
114
+ function validateEmail(email) {
115
+ const emailRegex = /.+@.+\..+/;
116
+ if (email.length < 3 || email.length > 256 || !emailRegex.test(email)) {
117
+ throw new Error('Invalid email address');
118
+ }
119
+ }
120
+ async function createOAuth1Tokens(sdk, email, password) {
121
+ console.log(chalk.white('⚙️ Creating OAuth1 tokens for the user', email));
122
+ const response = await authRepository.createOAuth1Tokens(sdk, email, password);
123
+ const { token, tokenSecret } = response.data;
124
+ console.log(chalk.green('✅ Successfully created OAuth1 tokens for the user', email));
125
+ return { token, tokenSecret };
126
+ }
package/build/exh.js CHANGED
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sdkAuth = exports.sdkInitOnly = void 0;
4
- const fs = require("fs");
5
4
  const javascript_sdk_1 = require("@extrahorizon/javascript-sdk");
6
- const constants_1 = require("./constants");
5
+ const util_1 = require("./helpers/util");
7
6
  let sdk = null;
8
7
  function sdkInitOnly(apiHost, consumerKey, consumerSecret) {
9
8
  sdk = (0, javascript_sdk_1.createOAuth1Client)({
@@ -15,46 +14,16 @@ function sdkInitOnly(apiHost, consumerKey, consumerSecret) {
15
14
  }
16
15
  exports.sdkInitOnly = sdkInitOnly;
17
16
  async function sdkAuth() {
18
- let credentials = {};
19
- let haveCredFile = false;
20
- const needed = ['API_HOST', 'API_OAUTH_CONSUMER_KEY', 'API_OAUTH_CONSUMER_SECRET', 'API_OAUTH_TOKEN', 'API_OAUTH_TOKEN_SECRET'];
21
- const error = missing => {
22
- let message = 'Failed to retrieve all credentials. ';
23
- if (!haveCredFile) {
24
- message += 'Couldn\'t open ~/.exh/credentials. ';
25
- }
26
- if (missing.length) {
27
- message += `Missing properties: ${missing.join(',')}`;
28
- }
29
- throw new Error(message);
30
- };
31
- try {
32
- const credentialsFile = fs.readFileSync(constants_1.EXH_CONFIG_FILE, 'utf-8');
33
- haveCredFile = true;
34
- credentials = credentialsFile
35
- .split(/\r?\n/).map(l => l.split(/=/)).filter(i => i.length === 2)
36
- .reduce((r, v) => { r[v[0].trim()] = v[1].trim(); return r; }, {});
37
- for (const k of Object.keys(credentials)) {
38
- if (!process.env[k]) {
39
- process.env[k] = credentials[k];
40
- }
41
- }
42
- }
43
- catch (err) { }
44
- needed.forEach(v => { credentials[v] = process.env[v] ?? credentials[v]; });
45
- const missingProperties = needed.filter(p => credentials[p] === undefined);
46
- if (missingProperties.length) {
47
- error(missingProperties);
48
- }
17
+ (0, util_1.loadAndAssertCredentials)();
49
18
  sdk = (0, javascript_sdk_1.createOAuth1Client)({
50
- consumerKey: credentials.API_OAUTH_CONSUMER_KEY,
51
- consumerSecret: credentials.API_OAUTH_CONSUMER_SECRET,
52
- host: credentials.API_HOST,
19
+ host: process.env.API_HOST,
20
+ consumerKey: process.env.API_OAUTH_CONSUMER_KEY,
21
+ consumerSecret: process.env.API_OAUTH_CONSUMER_SECRET,
53
22
  });
54
23
  try {
55
24
  await sdk.auth.authenticate({
56
- token: credentials.API_OAUTH_TOKEN,
57
- tokenSecret: credentials.API_OAUTH_TOKEN_SECRET,
25
+ token: process.env.API_OAUTH_TOKEN,
26
+ tokenSecret: process.env.API_OAUTH_TOKEN_SECRET,
58
27
  });
59
28
  }
60
29
  catch (err) {
@@ -1,3 +1,4 @@
1
1
  import * as yargs from 'yargs';
2
2
  export declare function epilogue(y: yargs.Argv): yargs.Argv;
3
3
  export declare function asyncExec(cmd: string): Promise<string>;
4
+ export declare function loadAndAssertCredentials(): void;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.asyncExec = exports.epilogue = void 0;
3
+ exports.loadAndAssertCredentials = exports.asyncExec = exports.epilogue = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ const fs = require("fs");
5
6
  const chalk = require("chalk");
7
+ const constants_1 = require("../constants");
6
8
  const error_1 = require("./error");
7
9
  function epilogue(y) {
8
10
  return y.epilogue('Visit https://docs.extrahorizon.com/extrahorizon-cli/ for more information.').fail((msg, err, argv) => {
@@ -34,3 +36,33 @@ async function asyncExec(cmd) {
34
36
  });
35
37
  }
36
38
  exports.asyncExec = asyncExec;
39
+ function loadAndAssertCredentials() {
40
+ const credentials = {};
41
+ let credentialsFile;
42
+ let errorMessage = '';
43
+ try {
44
+ credentialsFile = fs.readFileSync(constants_1.EXH_CONFIG_FILE, 'utf-8');
45
+ }
46
+ catch (e) {
47
+ errorMessage += 'Couldn\'t open ~/.exh/credentials. ';
48
+ }
49
+ const credentialFileLines = credentialsFile.split(/\r?\n/);
50
+ for (const credentialFileLine of credentialFileLines) {
51
+ const [key, value] = credentialFileLine.split('=');
52
+ if (key && value) {
53
+ credentials[key.trim()] = value.trim();
54
+ }
55
+ }
56
+ const requiredEnvVariables = ['API_HOST', 'API_OAUTH_CONSUMER_KEY', 'API_OAUTH_CONSUMER_SECRET', 'API_OAUTH_TOKEN', 'API_OAUTH_TOKEN_SECRET'];
57
+ for (const key of requiredEnvVariables) {
58
+ if (credentials[key] && !process.env[key]) {
59
+ process.env[key] = credentials[key];
60
+ }
61
+ }
62
+ const missingEnvironmentVariables = requiredEnvVariables.filter(key => !process.env[key]);
63
+ if (missingEnvironmentVariables.length > 0) {
64
+ errorMessage += `Missing environment variables: ${missingEnvironmentVariables.join(',')}`;
65
+ throw new Error(`Failed to retrieve all credentials. ${errorMessage}`);
66
+ }
67
+ }
68
+ exports.loadAndAssertCredentials = loadAndAssertCredentials;
@@ -1,3 +1,4 @@
1
1
  import { OAuth1Client } from '@extrahorizon/javascript-sdk';
2
2
  export declare function getHost(sdk: OAuth1Client): string;
3
+ export declare function createOAuth1Tokens(sdk: OAuth1Client, email: string, password: string): Promise<any>;
3
4
  export declare function fetchMe(sdk: OAuth1Client): Promise<import("@extrahorizon/javascript-sdk").UserData>;
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchMe = exports.getHost = void 0;
3
+ exports.fetchMe = exports.createOAuth1Tokens = exports.getHost = void 0;
4
4
  function getHost(sdk) {
5
5
  return sdk?.raw?.defaults?.baseURL;
6
6
  }
7
7
  exports.getHost = getHost;
8
+ async function createOAuth1Tokens(sdk, email, password) {
9
+ const response = await sdk.raw.post('/auth/v2/oauth1/tokens', { email, password });
10
+ return response.data;
11
+ }
12
+ exports.createOAuth1Tokens = createOAuth1Tokens;
8
13
  async function fetchMe(sdk) {
9
14
  return await sdk?.users.me();
10
15
  }
@@ -7,7 +7,7 @@ async function find(sdk) {
7
7
  }
8
8
  exports.find = find;
9
9
  async function findByName(sdk, name) {
10
- const response = await sdk.raw.get(`/tasks/v1/functions/${name}`);
10
+ const response = await sdk.raw.get(`/tasks/v1/functions/${name}`, { customResponseKeys: ['*'] });
11
11
  return response.data;
12
12
  }
13
13
  exports.findByName = findByName;
@@ -0,0 +1,2 @@
1
+ import { OAuth1Client } from '@extrahorizon/javascript-sdk';
2
+ export declare function findUserByEmail(sdk: OAuth1Client, email: string): Promise<import("@extrahorizon/javascript-sdk").UserData>;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findUserByEmail = void 0;
4
+ const javascript_sdk_1 = require("@extrahorizon/javascript-sdk");
5
+ async function findUserByEmail(sdk, email) {
6
+ const rql = (0, javascript_sdk_1.rqlBuilder)().eq('email', email).build();
7
+ return await sdk.users.findFirst({ rql });
8
+ }
9
+ exports.findUserByEmail = findUserByEmail;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extrahorizon/exh-cli",
3
- "version": "1.8.2",
3
+ "version": "1.9.0-dev-85-4c62b0a",
4
4
  "main": "build/index.js",
5
5
  "exports": "./build/index.js",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  "clean": "rimraf build",
16
16
  "build": "yarn clean && tsc",
17
17
  "test": "jest",
18
+ "test:ci": "jest --silent",
18
19
  "lint": "eslint src tests --ext .ts"
19
20
  },
20
21
  "bin": {