@extrahorizon/exh-cli 1.8.2 → 1.9.0-dev-86-1fadbca

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
 
@@ -96,13 +96,31 @@ async function syncSingleTask(sdk, config) {
96
96
  if (config.memoryLimit) {
97
97
  request.memoryLimit = config.memoryLimit;
98
98
  }
99
- if (config.environment) {
100
- request.environmentVariables =
101
- Object.entries(config.environment).reduce((prev, curr) => ({ ...prev, [curr[0]]: { value: curr[1] } }), {});
102
- }
103
99
  if (config.retryPolicy) {
104
100
  request.retryPolicy = config.retryPolicy;
105
101
  }
102
+ if (config.executionCredentials) {
103
+ const credentials = await (0, util_2.syncFunctionUser)(sdk, {
104
+ taskName: config.name,
105
+ targetEmail: config.executionCredentials.email,
106
+ targetPermissions: config.executionCredentials.permissions,
107
+ });
108
+ config.environment = {
109
+ ...config.environment,
110
+ API_HOST: process.env.API_HOST,
111
+ API_OAUTH_CONSUMER_KEY: process.env.API_OAUTH_CONSUMER_KEY,
112
+ API_OAUTH_CONSUMER_SECRET: process.env.API_OAUTH_CONSUMER_SECRET,
113
+ API_OAUTH_TOKEN: credentials.token,
114
+ API_OAUTH_TOKEN_SECRET: credentials.tokenSecret,
115
+ };
116
+ }
117
+ if (config.environment) {
118
+ const environmentVariables = {};
119
+ for (const [key, value] of Object.entries(config.environment)) {
120
+ environmentVariables[key] = { value };
121
+ }
122
+ request.environmentVariables = environmentVariables;
123
+ }
106
124
  if (myFunction === undefined) {
107
125
  await functionRepository.create(sdk, request);
108
126
  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,14 @@
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
6
  const archiver = require("archiver");
7
+ const chalk = require("chalk");
7
8
  const uuid_1 = require("uuid");
9
+ const authRepository = require("../../repositories/auth");
10
+ const functionRepository = require("../../repositories/functions");
11
+ const userRepository = require("../../repositories/user");
8
12
  async function zipFileFromDirectory(path) {
9
13
  return new Promise((res, rej) => {
10
14
  const tmpPath = `${(0, os_1.tmpdir)()}/${(0, uuid_1.v4)()}`;
@@ -25,3 +29,104 @@ async function zipFileFromDirectory(path) {
25
29
  });
26
30
  }
27
31
  exports.zipFileFromDirectory = zipFileFromDirectory;
32
+ async function syncFunctionUser(sdk, data) {
33
+ const { taskName, targetEmail, targetPermissions } = data;
34
+ const email = targetEmail || `exh.tasks+${taskName}@extrahorizon.com`;
35
+ validateEmail(email);
36
+ const password = `0Oo-${(0, uuid_1.v4)()}`;
37
+ const roleName = `exh.tasks.${taskName}`;
38
+ const role = await syncRoleWithPermissions(sdk, taskName, roleName, targetPermissions);
39
+ let user = await userRepository.findUserByEmail(sdk, email);
40
+ console.group(chalk.white(`🔄 Syncing user: ${email}`));
41
+ if (!user) {
42
+ console.log(chalk.white('⚙️ Creating the user...'));
43
+ user = await userRepository.createUser(sdk, {
44
+ firstName: `${taskName}`,
45
+ lastName: 'exh.tasks',
46
+ email,
47
+ password,
48
+ phoneNumber: '0000000000',
49
+ language: 'EN',
50
+ });
51
+ await assignRoleToUser(sdk, user.id, role.id);
52
+ const oAuth1Tokens = await createOAuth1Tokens(sdk, email, password);
53
+ console.groupEnd();
54
+ console.log(chalk.green('✅ Successfully synced user'));
55
+ console.log('');
56
+ return oAuth1Tokens;
57
+ }
58
+ const currentFunction = await functionRepository.findByName(sdk, taskName);
59
+ const hasExistingCredentials = (currentFunction?.environmentVariables?.API_HOST?.value &&
60
+ currentFunction?.environmentVariables?.API_OAUTH_TOKEN_SECRET?.value &&
61
+ currentFunction?.environmentVariables?.API_OAUTH_TOKEN?.value &&
62
+ currentFunction?.environmentVariables?.API_OAUTH_CONSUMER_KEY?.value &&
63
+ currentFunction?.environmentVariables?.API_OAUTH_CONSUMER_SECRET?.value);
64
+ if (!hasExistingCredentials) {
65
+ throw new Error('❌ No credentials were found for the existing user');
66
+ }
67
+ console.log(chalk.white('⚙️ Reusing existing user credentials...'));
68
+ const userRole = user.roles.find(({ name }) => name === roleName);
69
+ if (!userRole) {
70
+ await assignRoleToUser(sdk, user.id, role.id);
71
+ }
72
+ console.groupEnd();
73
+ console.log(chalk.green('✅ Successfully synced user'));
74
+ console.log('');
75
+ return {
76
+ token: currentFunction.environmentVariables.API_OAUTH_TOKEN.value,
77
+ tokenSecret: currentFunction.environmentVariables.API_OAUTH_TOKEN_SECRET.value,
78
+ };
79
+ }
80
+ exports.syncFunctionUser = syncFunctionUser;
81
+ async function syncRoleWithPermissions(sdk, taskName, roleName, targetPermissions) {
82
+ console.group(chalk.white(`🔄 Syncing role: ${roleName}`));
83
+ if (targetPermissions.length === 0) {
84
+ console.log(chalk.yellow('⚠️ The executionCredentials.permissions field has no permissions defined'));
85
+ }
86
+ let role = await userRepository.findGlobalRoleByName(sdk, roleName);
87
+ if (!role) {
88
+ console.log(chalk.white('⚙️ Creating the role...'));
89
+ const roleDescription = `A role created by the CLI for the execution of the task ${taskName}`;
90
+ role = await userRepository.createGlobalRole(sdk, roleName, roleDescription);
91
+ if (targetPermissions.length !== 0) {
92
+ await userRepository.addPermissionsToGlobalRole(sdk, roleName, targetPermissions);
93
+ }
94
+ console.log(chalk.white(`🔐 Permissions added: [${targetPermissions.join(', ')}]`));
95
+ console.groupEnd();
96
+ console.log(chalk.green('✅ Successfully synced role'));
97
+ console.log('');
98
+ return role;
99
+ }
100
+ console.log(chalk.white('⚙️ Updating the role...'));
101
+ const currentPermissions = role.permissions?.flatMap(permission => permission.name) || [];
102
+ const permissionsToAdd = targetPermissions.filter(targetPermission => !currentPermissions.includes(targetPermission));
103
+ const permissionsToRemove = currentPermissions.filter(currentPermission => !targetPermissions.includes(currentPermission));
104
+ if (permissionsToAdd.length > 0) {
105
+ await userRepository.addPermissionsToGlobalRole(sdk, roleName, permissionsToAdd);
106
+ console.log(chalk.white(`🔐 Permissions added: [${permissionsToAdd.join(',')}]`));
107
+ }
108
+ if (permissionsToRemove.length > 0) {
109
+ await userRepository.removePermissionsFromGlobalRole(sdk, roleName, permissionsToRemove);
110
+ console.log(chalk.white(`🔐 Permissions removed: [${permissionsToRemove.join(',')}]`));
111
+ }
112
+ console.groupEnd();
113
+ console.log(chalk.green('✅ Successfully synced role'));
114
+ console.log('');
115
+ return role;
116
+ }
117
+ async function assignRoleToUser(sdk, userId, roleId) {
118
+ console.log(chalk.white('⚙️ Assigning the role to the user...'));
119
+ await userRepository.addGlobalRoleToUser(sdk, userId, roleId);
120
+ }
121
+ async function createOAuth1Tokens(sdk, email, password) {
122
+ console.log(chalk.white('⚙️ Creating credentials...'));
123
+ const response = await authRepository.createOAuth1Tokens(sdk, email, password);
124
+ const { token, tokenSecret } = response;
125
+ return { token, tokenSecret };
126
+ }
127
+ function validateEmail(email) {
128
+ const emailRegex = /.+@.+\..+/;
129
+ if (email.length < 3 || email.length > 256 || !emailRegex.test(email)) {
130
+ throw new Error('Invalid email address');
131
+ }
132
+ }
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,11 @@ 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
+ .catch(e => e);
12
+ if (response.status === 404) {
13
+ return undefined;
14
+ }
11
15
  return response.data;
12
16
  }
13
17
  exports.findByName = findByName;
@@ -0,0 +1,9 @@
1
+ import { OAuth1Client, RegisterUserData } from '@extrahorizon/javascript-sdk';
2
+ export declare function findUserByEmail(sdk: OAuth1Client, email: string): Promise<import("@extrahorizon/javascript-sdk").UserData>;
3
+ export declare function isEmailAvailable(sdk: OAuth1Client, email: string): Promise<boolean>;
4
+ export declare function createUser(sdk: OAuth1Client, data: RegisterUserData): Promise<import("@extrahorizon/javascript-sdk").UserData>;
5
+ export declare function findGlobalRoleByName(sdk: OAuth1Client, name: string): Promise<import("@extrahorizon/javascript-sdk").Role>;
6
+ export declare function createGlobalRole(sdk: OAuth1Client, name: string, description: string): Promise<import("@extrahorizon/javascript-sdk").Role>;
7
+ export declare function addPermissionsToGlobalRole(sdk: OAuth1Client, name: string, permissions: string[]): Promise<import("@extrahorizon/javascript-sdk").AffectedRecords>;
8
+ export declare function removePermissionsFromGlobalRole(sdk: OAuth1Client, name: string, permissions: string[]): Promise<import("@extrahorizon/javascript-sdk").AffectedRecords>;
9
+ export declare function addGlobalRoleToUser(sdk: OAuth1Client, userId: string, roleId: string): Promise<import("@extrahorizon/javascript-sdk").AffectedRecords>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addGlobalRoleToUser = exports.removePermissionsFromGlobalRole = exports.addPermissionsToGlobalRole = exports.createGlobalRole = exports.findGlobalRoleByName = exports.createUser = exports.isEmailAvailable = 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;
10
+ async function isEmailAvailable(sdk, email) {
11
+ const { emailAvailable } = await sdk.users.isEmailAvailable(email);
12
+ return emailAvailable;
13
+ }
14
+ exports.isEmailAvailable = isEmailAvailable;
15
+ async function createUser(sdk, data) {
16
+ return await sdk.users.createAccount(data);
17
+ }
18
+ exports.createUser = createUser;
19
+ async function findGlobalRoleByName(sdk, name) {
20
+ return await sdk.users.globalRoles.findByName(name);
21
+ }
22
+ exports.findGlobalRoleByName = findGlobalRoleByName;
23
+ async function createGlobalRole(sdk, name, description) {
24
+ return await sdk.users.globalRoles.create({ name, description });
25
+ }
26
+ exports.createGlobalRole = createGlobalRole;
27
+ async function addPermissionsToGlobalRole(sdk, name, permissions) {
28
+ return await sdk.users.globalRoles.addPermissions((0, javascript_sdk_1.rqlBuilder)().eq('name', name).build(), { permissions });
29
+ }
30
+ exports.addPermissionsToGlobalRole = addPermissionsToGlobalRole;
31
+ async function removePermissionsFromGlobalRole(sdk, name, permissions) {
32
+ return await sdk.users.globalRoles.removePermissions((0, javascript_sdk_1.rqlBuilder)().eq('name', name).build(), { permissions });
33
+ }
34
+ exports.removePermissionsFromGlobalRole = removePermissionsFromGlobalRole;
35
+ async function addGlobalRoleToUser(sdk, userId, roleId) {
36
+ return await sdk.users.globalRoles.addToUsers((0, javascript_sdk_1.rqlBuilder)().eq('id', userId).build(), { roles: [roleId] });
37
+ }
38
+ exports.addGlobalRoleToUser = addGlobalRoleToUser;
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-86-1fadbca",
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": {