@aifabrix/builder 2.7.0 → 2.9.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 (47) hide show
  1. package/.cursor/rules/project-rules.mdc +680 -0
  2. package/integration/hubspot/README.md +136 -0
  3. package/integration/hubspot/env.template +9 -0
  4. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  5. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  6. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  7. package/integration/hubspot/hubspot-deploy.json +91 -0
  8. package/integration/hubspot/variables.yaml +17 -0
  9. package/lib/app-config.js +13 -2
  10. package/lib/app-deploy.js +9 -3
  11. package/lib/app-dockerfile.js +14 -1
  12. package/lib/app-prompts.js +177 -13
  13. package/lib/app-push.js +16 -1
  14. package/lib/app-register.js +37 -5
  15. package/lib/app-rotate-secret.js +10 -0
  16. package/lib/app-run.js +19 -0
  17. package/lib/app.js +70 -25
  18. package/lib/audit-logger.js +9 -4
  19. package/lib/build.js +25 -13
  20. package/lib/cli.js +109 -2
  21. package/lib/commands/login.js +40 -3
  22. package/lib/config.js +121 -114
  23. package/lib/datasource-deploy.js +14 -20
  24. package/lib/environment-deploy.js +305 -0
  25. package/lib/external-system-deploy.js +345 -0
  26. package/lib/external-system-download.js +431 -0
  27. package/lib/external-system-generator.js +190 -0
  28. package/lib/external-system-test.js +446 -0
  29. package/lib/generator-builders.js +323 -0
  30. package/lib/generator.js +200 -292
  31. package/lib/schema/application-schema.json +830 -800
  32. package/lib/schema/external-datasource.schema.json +868 -46
  33. package/lib/schema/external-system.schema.json +98 -80
  34. package/lib/schema/infrastructure-schema.json +1 -1
  35. package/lib/templates.js +32 -1
  36. package/lib/utils/cli-utils.js +4 -4
  37. package/lib/utils/device-code.js +10 -2
  38. package/lib/utils/external-system-display.js +159 -0
  39. package/lib/utils/external-system-validators.js +245 -0
  40. package/lib/utils/paths.js +151 -1
  41. package/lib/utils/schema-resolver.js +7 -2
  42. package/lib/utils/token-encryption.js +68 -0
  43. package/lib/validator.js +52 -5
  44. package/package.json +1 -1
  45. package/tatus +181 -0
  46. package/templates/external-system/external-datasource.json.hbs +55 -0
  47. package/templates/external-system/external-system.json.hbs +37 -0
package/lib/cli.js CHANGED
@@ -38,6 +38,8 @@ function setupCommands(program) {
38
38
  .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
39
39
  .option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
40
40
  .option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
41
+ .option('--offline', 'Request offline token (adds offline_access scope, device flow only)')
42
+ .option('--scope <scopes>', 'Custom OAuth2 scope string (device flow only, default: "openid profile email")')
41
43
  .action(async(options) => {
42
44
  try {
43
45
  await handleLogin(options);
@@ -103,12 +105,18 @@ function setupCommands(program) {
103
105
  .option('-a, --authentication', 'Requires authentication/RBAC')
104
106
  .option('-l, --language <lang>', 'Runtime language (typescript/python)')
105
107
  .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
108
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
106
109
  .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
107
110
  .option('-g, --github', 'Generate GitHub Actions workflows')
108
111
  .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
109
112
  .option('--main-branch <branch>', 'Main branch name for workflows', 'main')
110
113
  .action(async(appName, options) => {
111
114
  try {
115
+ // Validate type if provided
116
+ const validTypes = ['webapp', 'api', 'service', 'functionapp', 'external'];
117
+ if (options.type && !validTypes.includes(options.type)) {
118
+ throw new Error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
119
+ }
112
120
  await app.createApp(appName, options);
113
121
  } catch (error) {
114
122
  handleCommandError(error, 'create');
@@ -158,6 +166,46 @@ function setupCommands(program) {
158
166
  }
159
167
  });
160
168
 
169
+ // Environment deployment command
170
+ const environment = program
171
+ .command('environment')
172
+ .description('Manage environments');
173
+
174
+ const deployEnvHandler = async(envKey, options) => {
175
+ try {
176
+ const environmentDeploy = require('./environment-deploy');
177
+ await environmentDeploy.deployEnvironment(envKey, options);
178
+ } catch (error) {
179
+ handleCommandError(error, 'environment deploy');
180
+ process.exit(1);
181
+ }
182
+ };
183
+
184
+ environment
185
+ .command('deploy <env>')
186
+ .description('Deploy/setup environment in Miso Controller')
187
+ .option('-c, --controller <url>', 'Controller URL (required)')
188
+ .option('--config <file>', 'Environment configuration file')
189
+ .option('--skip-validation', 'Skip environment validation')
190
+ .option('--poll', 'Poll for deployment status', true)
191
+ .option('--no-poll', 'Do not poll for status')
192
+ .action(deployEnvHandler);
193
+
194
+ // Alias: env deploy (register as separate command since Commander.js doesn't support multi-word aliases)
195
+ const env = program
196
+ .command('env')
197
+ .description('Environment management (alias for environment)');
198
+
199
+ env
200
+ .command('deploy <env>')
201
+ .description('Deploy/setup environment in Miso Controller')
202
+ .option('-c, --controller <url>', 'Controller URL (required)')
203
+ .option('--config <file>', 'Environment configuration file')
204
+ .option('--skip-validation', 'Skip environment validation')
205
+ .option('--poll', 'Poll for deployment status', true)
206
+ .option('--no-poll', 'Do not poll for status')
207
+ .action(deployEnvHandler);
208
+
161
209
  program.command('deploy <app>')
162
210
  .description('Deploy to Azure via Miso Controller')
163
211
  .option('-c, --controller <url>', 'Controller URL')
@@ -293,12 +341,13 @@ function setupCommands(program) {
293
341
  });
294
342
 
295
343
  program.command('json <app>')
296
- .description('Generate deployment JSON')
344
+ .description('Generate deployment JSON (aifabrix-deploy.json for normal apps, application-schema.json for external systems)')
297
345
  .action(async(appName) => {
298
346
  try {
299
347
  const result = await generator.generateDeployJsonWithValidation(appName);
300
348
  if (result.success) {
301
- logger.log(`āœ“ Generated deployment JSON: ${result.path}`);
349
+ const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
350
+ logger.log(`āœ“ Generated ${fileName}: ${result.path}`);
302
351
 
303
352
  if (result.validation.warnings && result.validation.warnings.length > 0) {
304
353
  logger.log('\nāš ļø Warnings:');
@@ -515,6 +564,64 @@ function setupCommands(program) {
515
564
  process.exit(1);
516
565
  }
517
566
  });
567
+
568
+ // External system download command
569
+ program.command('download <system-key>')
570
+ .description('Download external system from dataplane to local development structure')
571
+ .option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
572
+ .option('-c, --controller <url>', 'Controller URL')
573
+ .option('--dry-run', 'Show what would be downloaded without actually downloading')
574
+ .action(async(systemKey, options) => {
575
+ try {
576
+ const download = require('./external-system-download');
577
+ await download.downloadExternalSystem(systemKey, options);
578
+ } catch (error) {
579
+ handleCommandError(error, 'download');
580
+ process.exit(1);
581
+ }
582
+ });
583
+
584
+ // Unit test command (local validation)
585
+ program.command('test <app>')
586
+ .description('Run unit tests for external system (local validation, no API calls)')
587
+ .option('-d, --datasource <key>', 'Test specific datasource only')
588
+ .option('-v, --verbose', 'Show detailed validation output')
589
+ .action(async(appName, options) => {
590
+ try {
591
+ const test = require('./external-system-test');
592
+ const results = await test.testExternalSystem(appName, options);
593
+ test.displayTestResults(results, options.verbose);
594
+ if (!results.valid) {
595
+ process.exit(1);
596
+ }
597
+ } catch (error) {
598
+ handleCommandError(error, 'test');
599
+ process.exit(1);
600
+ }
601
+ });
602
+
603
+ // Integration test command (via dataplane)
604
+ program.command('test-integration <app>')
605
+ .description('Run integration tests via dataplane pipeline API')
606
+ .option('-d, --datasource <key>', 'Test specific datasource only')
607
+ .option('-p, --payload <file>', 'Path to custom test payload file')
608
+ .option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
609
+ .option('-c, --controller <url>', 'Controller URL')
610
+ .option('-v, --verbose', 'Show detailed test output')
611
+ .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
612
+ .action(async(appName, options) => {
613
+ try {
614
+ const test = require('./external-system-test');
615
+ const results = await test.testExternalSystemIntegration(appName, options);
616
+ test.displayIntegrationTestResults(results, options.verbose);
617
+ if (!results.success) {
618
+ process.exit(1);
619
+ }
620
+ } catch (error) {
621
+ handleCommandError(error, 'test-integration');
622
+ process.exit(1);
623
+ }
624
+ });
518
625
  }
519
626
 
520
627
  module.exports = {
@@ -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,
@@ -129,39 +129,33 @@ async function deployDatasource(appKey, filePath, options) {
129
129
  const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
130
130
  logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
131
131
 
132
- // Deploy to dataplane
133
- logger.log(chalk.blue('\nšŸš€ Deploying to dataplane...'));
134
- const deployEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`;
132
+ // Publish to dataplane (using publish endpoint)
133
+ logger.log(chalk.blue('\nšŸš€ Publishing datasource to dataplane...'));
134
+ const publishEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/publish`;
135
135
 
136
- // Prepare deployment request
137
- const deployRequest = {
138
- datasource: datasourceConfig
139
- };
140
-
141
- // Make API call to dataplane
142
- // This is a placeholder - actual API structure may vary
143
- let deployResponse;
136
+ // Prepare publish request - send datasource configuration directly
137
+ let publishResponse;
144
138
  if (authConfig.type === 'bearer' && authConfig.token) {
145
- deployResponse = await authenticatedApiCall(
146
- deployEndpoint,
139
+ publishResponse = await authenticatedApiCall(
140
+ publishEndpoint,
147
141
  {
148
142
  method: 'POST',
149
- body: JSON.stringify(deployRequest)
143
+ body: JSON.stringify(datasourceConfig)
150
144
  },
151
145
  authConfig.token
152
146
  );
153
147
  } else {
154
- throw new Error('Bearer token authentication required for dataplane deployment');
148
+ throw new Error('Bearer token authentication required for dataplane publish');
155
149
  }
156
150
 
157
- if (!deployResponse.success) {
158
- const formattedError = deployResponse.formattedError || formatApiError(deployResponse);
159
- logger.error(chalk.red('āŒ Deployment failed:'));
151
+ if (!publishResponse.success) {
152
+ const formattedError = publishResponse.formattedError || formatApiError(publishResponse);
153
+ logger.error(chalk.red('āŒ Publish failed:'));
160
154
  logger.error(formattedError);
161
- throw new Error(`Dataplane deployment failed: ${formattedError}`);
155
+ throw new Error(`Dataplane publish failed: ${formattedError}`);
162
156
  }
163
157
 
164
- logger.log(chalk.green('\nāœ“ Datasource deployed successfully!'));
158
+ logger.log(chalk.green('\nāœ“ Datasource published successfully!'));
165
159
  logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
166
160
  logger.log(chalk.blue(`System: ${systemKey}`));
167
161
  logger.log(chalk.blue(`Environment: ${options.environment}`));