@aifabrix/builder 2.6.3 → 2.8.0

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.
Files changed (43) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/bin/aifabrix.js +4 -0
  3. package/lib/app-config.js +10 -0
  4. package/lib/app-deploy.js +18 -0
  5. package/lib/app-dockerfile.js +15 -0
  6. package/lib/app-prompts.js +172 -9
  7. package/lib/app-push.js +15 -0
  8. package/lib/app-register.js +14 -0
  9. package/lib/app-run.js +25 -0
  10. package/lib/app.js +30 -13
  11. package/lib/audit-logger.js +9 -4
  12. package/lib/build.js +8 -0
  13. package/lib/cli.js +99 -2
  14. package/lib/commands/datasource.js +94 -0
  15. package/lib/commands/login.js +40 -3
  16. package/lib/config.js +121 -114
  17. package/lib/datasource-deploy.js +182 -0
  18. package/lib/datasource-diff.js +73 -0
  19. package/lib/datasource-list.js +138 -0
  20. package/lib/datasource-validate.js +63 -0
  21. package/lib/diff.js +266 -0
  22. package/lib/environment-deploy.js +305 -0
  23. package/lib/external-system-deploy.js +262 -0
  24. package/lib/external-system-generator.js +187 -0
  25. package/lib/schema/application-schema.json +869 -698
  26. package/lib/schema/external-datasource.schema.json +512 -0
  27. package/lib/schema/external-system.schema.json +262 -0
  28. package/lib/schema/infrastructure-schema.json +1 -1
  29. package/lib/secrets.js +20 -1
  30. package/lib/templates.js +32 -1
  31. package/lib/utils/device-code.js +10 -2
  32. package/lib/utils/env-copy.js +24 -0
  33. package/lib/utils/env-endpoints.js +50 -11
  34. package/lib/utils/schema-loader.js +220 -0
  35. package/lib/utils/schema-resolver.js +174 -0
  36. package/lib/utils/secrets-helpers.js +65 -17
  37. package/lib/utils/token-encryption.js +68 -0
  38. package/lib/validate.js +299 -0
  39. package/lib/validator.js +47 -3
  40. package/package.json +1 -1
  41. package/tatus +181 -0
  42. package/templates/external-system/external-datasource.json.hbs +55 -0
  43. package/templates/external-system/external-system.json.hbs +37 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AI Fabrix Builder - Datasource Commands
3
+ *
4
+ * Handles datasource validation, listing, comparison, and deployment
5
+ * Commands: datasource validate, datasource list, datasource diff, datasource deploy
6
+ *
7
+ * @fileoverview Datasource management commands for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const { validateDatasourceFile } = require('../datasource-validate');
15
+ const { listDatasources } = require('../datasource-list');
16
+ const { compareDatasources } = require('../datasource-diff');
17
+ const { deployDatasource } = require('../datasource-deploy');
18
+
19
+ /**
20
+ * Setup datasource management commands
21
+ * @param {Command} program - Commander program instance
22
+ */
23
+ function setupDatasourceCommands(program) {
24
+ const datasource = program
25
+ .command('datasource')
26
+ .description('Manage external data sources');
27
+
28
+ // Validate command
29
+ datasource
30
+ .command('validate <file>')
31
+ .description('Validate external datasource JSON file')
32
+ .action(async(file) => {
33
+ try {
34
+ const result = await validateDatasourceFile(file);
35
+ if (result.valid) {
36
+ logger.log(chalk.green(`\n✓ Datasource file is valid: ${file}`));
37
+ } else {
38
+ logger.log(chalk.red(`\n✗ Datasource file has errors: ${file}`));
39
+ result.errors.forEach(error => {
40
+ logger.log(chalk.red(` • ${error}`));
41
+ });
42
+ process.exit(1);
43
+ }
44
+ } catch (error) {
45
+ logger.error(chalk.red('❌ Validation failed:'), error.message);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ // List command
51
+ datasource
52
+ .command('list')
53
+ .description('List datasources from environment')
54
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
55
+ .action(async(options) => {
56
+ try {
57
+ await listDatasources(options);
58
+ } catch (error) {
59
+ logger.error(chalk.red('❌ Failed to list datasources:'), error.message);
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ // Diff command
65
+ datasource
66
+ .command('diff <file1> <file2>')
67
+ .description('Compare two datasource configuration files (for dataplane)')
68
+ .action(async(file1, file2) => {
69
+ try {
70
+ await compareDatasources(file1, file2);
71
+ } catch (error) {
72
+ logger.error(chalk.red('❌ Diff failed:'), error.message);
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+ // Deploy command
78
+ datasource
79
+ .command('deploy <myapp> <file>')
80
+ .description('Deploy datasource to dataplane')
81
+ .requiredOption('--controller <url>', 'Controller URL')
82
+ .requiredOption('-e, --environment <env>', 'Environment (miso, dev, tst, pro)')
83
+ .action(async(myapp, file, options) => {
84
+ try {
85
+ await deployDatasource(myapp, file, options);
86
+ } catch (error) {
87
+ logger.error(chalk.red('❌ Deployment failed:'), error.message);
88
+ process.exit(1);
89
+ }
90
+ });
91
+ }
92
+
93
+ module.exports = { setupDatasourceCommands };
94
+
@@ -302,20 +302,51 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
302
302
  }
303
303
  }
304
304
 
305
+ /**
306
+ * Build scope string from options
307
+ * @param {boolean} [offline] - Whether to request offline_access
308
+ * @param {string} [customScope] - Custom scope string
309
+ * @returns {string} Scope string
310
+ */
311
+ function buildScope(offline, customScope) {
312
+ const defaultScope = 'openid profile email';
313
+
314
+ if (customScope) {
315
+ // If custom scope provided, use it and optionally add offline_access
316
+ if (offline && !customScope.includes('offline_access')) {
317
+ return `${customScope} offline_access`;
318
+ }
319
+ return customScope;
320
+ }
321
+
322
+ // Default scope with optional offline_access
323
+ if (offline) {
324
+ return `${defaultScope} offline_access`;
325
+ }
326
+
327
+ return defaultScope;
328
+ }
329
+
305
330
  /**
306
331
  * Handle device code flow login
307
332
  * @async
308
333
  * @param {string} controllerUrl - Controller URL
309
334
  * @param {string} [environment] - Environment key from options
335
+ * @param {boolean} [offline] - Whether to request offline_access scope
336
+ * @param {string} [scope] - Custom scope string
310
337
  * @returns {Promise<{token: string, environment: string}>} Token and environment
311
338
  */
312
- async function handleDeviceCodeLogin(controllerUrl, environment) {
339
+ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
313
340
  const envKey = await getEnvironmentKey(environment);
341
+ const requestScope = buildScope(offline, scope);
314
342
 
315
343
  logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
344
+ if (offline) {
345
+ logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
346
+ }
316
347
 
317
348
  try {
318
- const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey);
349
+ const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
319
350
 
320
351
  displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
321
352
 
@@ -369,6 +400,12 @@ async function handleLogin(options) {
369
400
  let token;
370
401
  let expiresAt;
371
402
 
403
+ // Validate scope options - only applicable to device flow
404
+ if (method === 'credentials' && (options.offline || options.scope)) {
405
+ logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
406
+ logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
407
+ }
408
+
372
409
  if (method === 'credentials') {
373
410
  if (!options.app) {
374
411
  logger.error(chalk.red('❌ --app is required for credentials login method'));
@@ -379,7 +416,7 @@ async function handleLogin(options) {
379
416
  expiresAt = loginResult.expiresAt;
380
417
  await saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, options.app);
381
418
  } else if (method === 'device') {
382
- const result = await handleDeviceCodeLogin(controllerUrl, options.environment);
419
+ const result = await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
383
420
  token = result.token;
384
421
  environment = result.environment;
385
422
  return; // Early return for device flow (already saved config)
package/lib/config.js CHANGED
@@ -12,6 +12,7 @@ const fs = require('fs').promises;
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const os = require('os');
15
+ const { encryptToken, decryptToken, isTokenEncrypted } = require('./utils/token-encryption');
15
16
  // Avoid importing paths here to prevent circular dependency.
16
17
  // Config location is always under OS home at ~/.aifabrix/config.yaml
17
18
 
@@ -26,11 +27,6 @@ const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
26
27
  // Cache for developer ID - loaded when getConfig() is first called
27
28
  let cachedDeveloperId = null;
28
29
 
29
- /**
30
- * Get stored configuration
31
- * Loads developer ID and caches it as a property for easy access
32
- * @returns {Promise<Object>} Configuration object with new structure
33
- */
34
30
  async function getConfig() {
35
31
  try {
36
32
  const configContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
@@ -156,61 +152,37 @@ async function getDeveloperId() {
156
152
  */
157
153
  async function setDeveloperId(developerId) {
158
154
  const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
155
+ const errorMsg = 'Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)';
159
156
  let devIdString;
160
157
  if (typeof developerId === 'number') {
161
- if (!Number.isFinite(developerId) || developerId < 0) {
162
- throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
163
- }
158
+ if (!Number.isFinite(developerId) || developerId < 0) throw new Error(errorMsg);
164
159
  devIdString = String(developerId);
165
160
  } else if (typeof developerId === 'string') {
166
- if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
167
- throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
168
- }
161
+ if (!DEV_ID_DIGITS_REGEX.test(developerId)) throw new Error(errorMsg);
169
162
  devIdString = developerId;
170
163
  } else {
171
- throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
164
+ throw new Error(errorMsg);
172
165
  }
173
- // Clear cache first to ensure we get fresh data from file
174
166
  cachedDeveloperId = null;
175
- // Read file directly to avoid any caching issues
176
167
  const config = await getConfig();
177
- // Update developer ID
178
168
  config['developer-id'] = devIdString;
179
- // Update cache before saving
180
169
  cachedDeveloperId = devIdString;
181
- // Save the entire config object to ensure all fields are preserved
182
170
  await saveConfig(config);
183
- // Verify the file was saved correctly by reading it back
184
- // This ensures the file system has written the data
185
- // Add a small delay to ensure file system has flushed the write
186
171
  await new Promise(resolve => setTimeout(resolve, 100));
187
- // Read file again with fresh file handle to avoid OS caching
188
172
  const savedContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
189
173
  const savedConfig = yaml.load(savedContent);
190
- // YAML may parse numbers as numbers, so convert to string for comparison
191
174
  const savedDevIdString = String(savedConfig['developer-id']);
192
175
  if (savedDevIdString !== devIdString) {
193
176
  throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
194
177
  }
195
- // Clear the cache to force reload from file on next getDeveloperId() call
196
- // This ensures we get the value that was actually saved to disk
197
178
  cachedDeveloperId = null;
198
179
  }
199
180
 
200
- /**
201
- * Get current environment from root-level config
202
- * @returns {Promise<string>} Current environment (defaults to 'dev')
203
- */
204
181
  async function getCurrentEnvironment() {
205
182
  const config = await getConfig();
206
183
  return config.environment || 'dev';
207
184
  }
208
185
 
209
- /**
210
- * Set current environment in root-level config
211
- * @param {string} environment - Environment to set (e.g., 'miso', 'dev', 'tst', 'pro')
212
- * @returns {Promise<void>}
213
- */
214
186
  async function setCurrentEnvironment(environment) {
215
187
  if (!environment || typeof environment !== 'string') {
216
188
  throw new Error('Environment must be a non-empty string');
@@ -220,31 +192,45 @@ async function setCurrentEnvironment(environment) {
220
192
  await saveConfig(config);
221
193
  }
222
194
 
223
- /**
224
- * Check if token is expired
225
- * @param {string} expiresAt - ISO timestamp string
226
- * @returns {boolean} True if token is expired
227
- */
228
195
  function isTokenExpired(expiresAt) {
229
196
  if (!expiresAt) return true;
230
197
  const expirationTime = new Date(expiresAt).getTime();
231
198
  const now = Date.now();
232
- return now >= (expirationTime - 5 * 60 * 1000); // 5 minute buffer
199
+ return now >= (expirationTime - 5 * 60 * 1000);
233
200
  }
234
201
 
235
- /**
236
- * Check if token should be refreshed proactively (within 15 minutes of expiry)
237
- * Helps keep Keycloak sessions alive by refreshing before SSO Session Idle timeout (30 minutes)
238
- * @param {string} expiresAt - ISO timestamp string
239
- * @returns {boolean} True if token should be refreshed proactively
240
- */
241
202
  function shouldRefreshToken(expiresAt) {
242
203
  if (!expiresAt) return true;
243
204
  const expirationTime = new Date(expiresAt).getTime();
244
205
  const now = Date.now();
245
- return now >= (expirationTime - 15 * 60 * 1000); // 15 minutes buffer
206
+ return now >= (expirationTime - 15 * 60 * 1000);
207
+ }
208
+ async function encryptTokenValue(value) {
209
+ if (!value || typeof value !== 'string') return value;
210
+ try {
211
+ const encryptionKey = await getSecretsEncryptionKey();
212
+ if (!encryptionKey) return value;
213
+ if (isTokenEncrypted(value)) return value;
214
+ const encrypted = encryptToken(value, encryptionKey);
215
+ // Ensure we never return undefined for valid inputs
216
+ return encrypted !== undefined && encrypted !== null ? encrypted : value;
217
+ } catch (error) {
218
+ return value;
219
+ }
220
+ }
221
+ async function decryptTokenValue(value) {
222
+ if (!value || typeof value !== 'string') return value;
223
+ try {
224
+ const encryptionKey = await getSecretsEncryptionKey();
225
+ if (!encryptionKey) return value;
226
+ if (!isTokenEncrypted(value)) return value;
227
+ const decrypted = decryptToken(value, encryptionKey);
228
+ // Ensure we never return undefined for valid inputs
229
+ return decrypted !== undefined && decrypted !== null ? decrypted : value;
230
+ } catch (error) {
231
+ return value;
232
+ }
246
233
  }
247
-
248
234
  /**
249
235
  * Get device token for controller
250
236
  * @param {string} controllerUrl - Controller URL
@@ -254,10 +240,37 @@ async function getDeviceToken(controllerUrl) {
254
240
  const config = await getConfig();
255
241
  if (!config.device || !config.device[controllerUrl]) return null;
256
242
  const deviceToken = config.device[controllerUrl];
243
+
244
+ // Migration: If tokens are plain text and encryption key exists, encrypt them first
245
+ const encryptionKey = await getSecretsEncryptionKey();
246
+ if (encryptionKey) {
247
+ let needsSave = false;
248
+
249
+ if (deviceToken.token && !isTokenEncrypted(deviceToken.token)) {
250
+ // Token is plain text, encrypt it
251
+ deviceToken.token = await encryptTokenValue(deviceToken.token);
252
+ needsSave = true;
253
+ }
254
+
255
+ if (deviceToken.refreshToken && !isTokenEncrypted(deviceToken.refreshToken)) {
256
+ // Refresh token is plain text, encrypt it
257
+ deviceToken.refreshToken = await encryptTokenValue(deviceToken.refreshToken);
258
+ needsSave = true;
259
+ }
260
+
261
+ if (needsSave) {
262
+ // Save encrypted tokens back to config
263
+ await saveConfig(config);
264
+ }
265
+ }
266
+ // Decrypt tokens if encrypted (for return value)
267
+ const token = deviceToken.token ? await decryptTokenValue(deviceToken.token) : undefined;
268
+ const refreshToken = deviceToken.refreshToken ? await decryptTokenValue(deviceToken.refreshToken) : null;
269
+
257
270
  return {
258
271
  controller: controllerUrl,
259
- token: deviceToken.token,
260
- refreshToken: deviceToken.refreshToken,
272
+ token: token,
273
+ refreshToken: refreshToken,
261
274
  expiresAt: deviceToken.expiresAt
262
275
  };
263
276
  }
@@ -272,7 +285,26 @@ async function getClientToken(environment, appName) {
272
285
  const config = await getConfig();
273
286
  if (!config.environments || !config.environments[environment]) return null;
274
287
  if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
275
- return config.environments[environment].clients[appName];
288
+
289
+ const clientToken = config.environments[environment].clients[appName];
290
+
291
+ // Migration: If token is plain text and encryption key exists, encrypt it first
292
+ const encryptionKey = await getSecretsEncryptionKey();
293
+ if (encryptionKey && clientToken.token && !isTokenEncrypted(clientToken.token)) {
294
+ // Token is plain text, encrypt it
295
+ clientToken.token = await encryptTokenValue(clientToken.token);
296
+ // Save encrypted token back to config
297
+ await saveConfig(config);
298
+ }
299
+
300
+ // Decrypt token if encrypted (for return value)
301
+ const token = await decryptTokenValue(clientToken.token);
302
+
303
+ return {
304
+ controller: clientToken.controller,
305
+ token: token,
306
+ expiresAt: clientToken.expiresAt
307
+ };
276
308
  }
277
309
 
278
310
  /**
@@ -286,7 +318,16 @@ async function getClientToken(environment, appName) {
286
318
  async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
287
319
  const config = await getConfig();
288
320
  if (!config.device) config.device = {};
289
- config.device[controllerUrl] = { token, refreshToken, expiresAt };
321
+
322
+ // Encrypt tokens before saving
323
+ const encryptedToken = await encryptTokenValue(token);
324
+ const encryptedRefreshToken = refreshToken ? await encryptTokenValue(refreshToken) : null;
325
+
326
+ config.device[controllerUrl] = {
327
+ token: encryptedToken,
328
+ refreshToken: encryptedRefreshToken,
329
+ expiresAt
330
+ };
290
331
  await saveConfig(config);
291
332
  }
292
333
 
@@ -304,7 +345,15 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
304
345
  if (!config.environments) config.environments = {};
305
346
  if (!config.environments[environment]) config.environments[environment] = { clients: {} };
306
347
  if (!config.environments[environment].clients) config.environments[environment].clients = {};
307
- config.environments[environment].clients[appName] = { controller: controllerUrl, token, expiresAt };
348
+
349
+ // Encrypt token before saving
350
+ const encryptedToken = await encryptTokenValue(token);
351
+
352
+ config.environments[environment].clients[appName] = {
353
+ controller: controllerUrl,
354
+ token: encryptedToken,
355
+ expiresAt
356
+ };
308
357
  await saveConfig(config);
309
358
  }
310
359
 
@@ -349,100 +398,56 @@ async function setSecretsEncryptionKey(key) {
349
398
  await saveConfig(config);
350
399
  }
351
400
 
352
- /**
353
- * Get general secrets path from configuration
354
- * Returns aifabrix-secrets path from config.yaml if configured
355
- * @returns {Promise<string|null>} Secrets path or null if not set
356
- */
357
401
  async function getSecretsPath() {
358
402
  const config = await getConfig();
359
- // Backward compatibility: prefer new key, fallback to legacy
360
403
  return config['aifabrix-secrets'] || config['secrets-path'] || null;
361
404
  }
362
405
 
363
- /**
364
- * Set general secrets path in configuration
365
- * @param {string} secretsPath - Path to general secrets file
366
- * @returns {Promise<void>}
367
- */
368
406
  async function setSecretsPath(secretsPath) {
369
407
  if (!secretsPath || typeof secretsPath !== 'string') {
370
408
  throw new Error('Secrets path is required and must be a string');
371
409
  }
372
-
373
410
  const config = await getConfig();
374
- // Store under new canonical key
375
411
  config['aifabrix-secrets'] = secretsPath;
376
412
  await saveConfig(config);
377
413
  }
378
414
 
379
- /**
380
- * Get aifabrix-home override from configuration
381
- * @returns {Promise<string|null>} Home override path or null if not set
382
- */
383
- async function getAifabrixHomeOverride() {
415
+ async function getPathConfig(key) {
384
416
  const config = await getConfig();
385
- return config['aifabrix-home'] || null;
417
+ return config[key] || null;
386
418
  }
387
419
 
388
- /**
389
- * Set aifabrix-home override in configuration
390
- * @param {string} homePath - Base directory path for AI Fabrix files
391
- * @returns {Promise<void>}
392
- */
393
- async function setAifabrixHomeOverride(homePath) {
394
- if (!homePath || typeof homePath !== 'string') {
395
- throw new Error('Home path is required and must be a string');
420
+ async function setPathConfig(key, value, errorMsg) {
421
+ if (!value || typeof value !== 'string') {
422
+ throw new Error(errorMsg);
396
423
  }
397
424
  const config = await getConfig();
398
- config['aifabrix-home'] = homePath;
425
+ config[key] = value;
399
426
  await saveConfig(config);
400
427
  }
401
428
 
402
- /**
403
- * Get aifabrix-secrets path from configuration (canonical)
404
- * @returns {Promise<string|null>} Secrets path or null if not set
405
- */
429
+ async function getAifabrixHomeOverride() {
430
+ return getPathConfig('aifabrix-home');
431
+ }
432
+
433
+ async function setAifabrixHomeOverride(homePath) {
434
+ await setPathConfig('aifabrix-home', homePath, 'Home path is required and must be a string');
435
+ }
436
+
406
437
  async function getAifabrixSecretsPath() {
407
- const config = await getConfig();
408
- return config['aifabrix-secrets'] || null;
438
+ return getPathConfig('aifabrix-secrets');
409
439
  }
410
440
 
411
- /**
412
- * Set aifabrix-secrets path in configuration (canonical)
413
- * @param {string} secretsPath - Path to default secrets file
414
- * @returns {Promise<void>}
415
- */
416
441
  async function setAifabrixSecretsPath(secretsPath) {
417
- if (!secretsPath || typeof secretsPath !== 'string') {
418
- throw new Error('Secrets path is required and must be a string');
419
- }
420
- const config = await getConfig();
421
- config['aifabrix-secrets'] = secretsPath;
422
- await saveConfig(config);
442
+ await setPathConfig('aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
423
443
  }
424
444
 
425
- /**
426
- * Get aifabrix-env-config path from configuration
427
- * @returns {Promise<string|null>} Env config path or null if not set
428
- */
429
445
  async function getAifabrixEnvConfigPath() {
430
- const config = await getConfig();
431
- return config['aifabrix-env-config'] || null;
446
+ return getPathConfig('aifabrix-env-config');
432
447
  }
433
448
 
434
- /**
435
- * Set aifabrix-env-config path in configuration
436
- * @param {string} envConfigPath - Path to user env-config file
437
- * @returns {Promise<void>}
438
- */
439
449
  async function setAifabrixEnvConfigPath(envConfigPath) {
440
- if (!envConfigPath || typeof envConfigPath !== 'string') {
441
- throw new Error('Env config path is required and must be a string');
442
- }
443
- const config = await getConfig();
444
- config['aifabrix-env-config'] = envConfigPath;
445
- await saveConfig(config);
450
+ await setPathConfig('aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
446
451
  }
447
452
 
448
453
  // Create exports object
@@ -461,6 +466,8 @@ const exportsObj = {
461
466
  getClientToken,
462
467
  saveDeviceToken,
463
468
  saveClientToken,
469
+ encryptTokenValue,
470
+ decryptTokenValue,
464
471
  getSecretsEncryptionKey,
465
472
  setSecretsEncryptionKey,
466
473
  getSecretsPath,