@aifabrix/builder 2.10.1 → 2.20.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 +194 -0
  2. package/README.md +12 -0
  3. package/integration/hubspot/README.md +2 -2
  4. package/integration/hubspot/hubspot-deploy.json +12 -4
  5. package/lib/api/applications.api.js +164 -0
  6. package/lib/api/auth.api.js +304 -0
  7. package/lib/api/datasources-core.api.js +87 -0
  8. package/lib/api/datasources-extended.api.js +117 -0
  9. package/lib/api/datasources.api.js +13 -0
  10. package/lib/api/deployments.api.js +126 -0
  11. package/lib/api/environments.api.js +245 -0
  12. package/lib/api/external-systems.api.js +251 -0
  13. package/lib/api/index.js +221 -0
  14. package/lib/api/pipeline.api.js +234 -0
  15. package/lib/api/types/applications.types.js +136 -0
  16. package/lib/api/types/auth.types.js +218 -0
  17. package/lib/api/types/datasources.types.js +272 -0
  18. package/lib/api/types/deployments.types.js +184 -0
  19. package/lib/api/types/environments.types.js +197 -0
  20. package/lib/api/types/external-systems.types.js +244 -0
  21. package/lib/api/types/pipeline.types.js +125 -0
  22. package/lib/app-list.js +5 -7
  23. package/lib/app-register.js +70 -403
  24. package/lib/app-rotate-secret.js +4 -10
  25. package/lib/commands/login.js +19 -12
  26. package/lib/datasource-deploy.js +7 -30
  27. package/lib/datasource-list.js +9 -6
  28. package/lib/deployer.js +103 -135
  29. package/lib/environment-deploy.js +15 -26
  30. package/lib/external-system-deploy.js +12 -39
  31. package/lib/external-system-download.js +5 -13
  32. package/lib/external-system-test.js +9 -12
  33. package/lib/utils/api-error-handler.js +11 -453
  34. package/lib/utils/app-register-api.js +66 -0
  35. package/lib/utils/app-register-auth.js +72 -0
  36. package/lib/utils/app-register-config.js +205 -0
  37. package/lib/utils/app-register-display.js +69 -0
  38. package/lib/utils/app-register-validator.js +143 -0
  39. package/lib/utils/deployment-errors.js +88 -6
  40. package/lib/utils/device-code.js +1 -1
  41. package/lib/utils/error-formatters/error-parser.js +150 -0
  42. package/lib/utils/error-formatters/http-status-errors.js +189 -0
  43. package/lib/utils/error-formatters/network-errors.js +46 -0
  44. package/lib/utils/error-formatters/permission-errors.js +94 -0
  45. package/lib/utils/error-formatters/validation-errors.js +133 -0
  46. package/package.json +1 -1
  47. package/templates/applications/README.md.hbs +1 -1
@@ -8,376 +8,98 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const fs = require('fs').promises;
12
- const path = require('path');
13
11
  const chalk = require('chalk');
14
- const yaml = require('js-yaml');
15
- const { getConfig } = require('./config');
16
- const { authenticatedApiCall } = require('./utils/api');
17
- const { formatApiError } = require('./utils/api-error-handler');
18
12
  const logger = require('./utils/logger');
19
13
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
20
14
  const { updateEnvTemplate } = require('./utils/env-template');
21
- const { getOrRefreshDeviceToken } = require('./utils/token-manager');
22
- const { detectAppType } = require('./utils/paths');
23
15
  const { generateEnvFile } = require('./secrets');
24
- const applicationSchema = require('./schema/application-schema.json');
25
-
26
- // Import createApp to auto-generate config if missing
27
- let createApp;
28
- try {
29
- createApp = require('./app').createApp;
30
- } catch {
31
- createApp = null;
32
- }
33
-
34
- // Extract valid enum values from application schema
35
- const validTypes = applicationSchema.properties.type.enum || [];
36
- const validRegistryModes = applicationSchema.properties.registryMode.enum || [];
37
- const portConstraints = {
38
- minimum: applicationSchema.properties.port?.minimum || 1,
39
- maximum: applicationSchema.properties.port?.maximum || 65535
40
- };
41
-
42
- /**
43
- * Validation schema for application registration
44
- * Validates according to application-schema.json
45
- */
46
- const registerApplicationSchema = {
47
- environmentId: (val) => {
48
- if (!val || val.length < 1) {
49
- throw new Error('Invalid environment ID format');
50
- }
51
- return val;
52
- },
53
- key: (val) => {
54
- if (!val || val.length < 1) {
55
- throw new Error('Application key is required');
56
- }
57
- const keyPattern = applicationSchema.properties.key.pattern;
58
- const keyMaxLength = applicationSchema.properties.key.maxLength || 50;
59
- if (val.length > keyMaxLength) {
60
- throw new Error(`Application key must be at most ${keyMaxLength} characters`);
61
- }
62
- if (keyPattern && !new RegExp(keyPattern).test(val)) {
63
- throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
64
- }
65
- return val;
66
- },
67
- displayName: (val) => {
68
- if (!val || val.length < 1) {
69
- throw new Error('Display name is required');
70
- }
71
- const displayNameMaxLength = applicationSchema.properties.displayName.maxLength || 100;
72
- if (val.length > displayNameMaxLength) {
73
- throw new Error(`Display name must be at most ${displayNameMaxLength} characters`);
74
- }
75
- return val;
76
- },
77
- description: (val) => val || undefined,
78
- configuration: (val) => {
79
- if (!val || !val.type || !validTypes.includes(val.type)) {
80
- throw new Error(`Configuration type must be one of: ${validTypes.join(', ')}`);
81
- }
82
- if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
83
- throw new Error(`Registry mode must be one of: ${validRegistryModes.join(', ')}`);
84
- }
85
- // Port validation: skip for external type (external systems don't need ports)
86
- // For other types, port is required and must be valid
87
- if (val.type !== 'external') {
88
- if (val.port === undefined || val.port === null) {
89
- throw new Error('Port is required for non-external application types');
90
- }
91
- if (!Number.isInteger(val.port) || val.port < portConstraints.minimum || val.port > portConstraints.maximum) {
92
- throw new Error(`Port must be an integer between ${portConstraints.minimum} and ${portConstraints.maximum}`);
93
- }
94
- }
95
- return val;
96
- }
97
- };
16
+ const { registerApplicationSchema, validateAppRegistrationData } = require('./utils/app-register-validator');
17
+ const {
18
+ loadVariablesYaml,
19
+ createMinimalAppIfNeeded,
20
+ extractAppConfiguration
21
+ } = require('./utils/app-register-config');
22
+ const { checkAuthentication } = require('./utils/app-register-auth');
23
+ const { callRegisterApi } = require('./utils/app-register-api');
24
+ const { displayRegistrationResults, getEnvironmentPrefix } = require('./utils/app-register-display');
98
25
 
99
26
  /**
100
- * Load variables.yaml file for an application
101
- * @async
102
- * @param {string} appKey - Application key
103
- * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
104
- */
105
- async function loadVariablesYaml(appKey) {
106
- // Detect app type and get correct path (integration or builder)
107
- const { appPath } = await detectAppType(appKey);
108
- const variablesPath = path.join(appPath, 'variables.yaml');
109
-
110
- try {
111
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
112
- return { variables: yaml.load(variablesContent), created: false };
113
- } catch (error) {
114
- if (error.code === 'ENOENT') {
115
- logger.log(chalk.yellow(`āš ļø variables.yaml not found for ${appKey}`));
116
- logger.log(chalk.yellow('šŸ“ Creating minimal configuration...\n'));
117
- return { variables: null, created: true };
118
- }
119
- throw new Error(`Failed to read variables.yaml: ${error.message}`);
120
- }
121
- }
122
-
123
- /**
124
- * Create minimal application configuration if needed
125
- * @async
126
- * @param {string} appKey - Application key
27
+ * Build registration data payload from app configuration
28
+ * @param {Object} appConfig - Application configuration
127
29
  * @param {Object} options - Registration options
128
- * @returns {Promise<Object>} Variables after creation
30
+ * @returns {Object} Registration data payload
129
31
  */
130
- async function createMinimalAppIfNeeded(appKey, options) {
131
- if (!createApp) {
132
- throw new Error('Cannot auto-create application: createApp function not available');
133
- }
134
-
135
- await createApp(appKey, {
136
- port: options.port,
137
- language: 'typescript',
138
- database: false,
139
- redis: false,
140
- storage: false,
141
- authentication: false
142
- });
143
-
144
- // Detect app type and get correct path (integration or builder)
145
- const { appPath } = await detectAppType(appKey);
146
- const variablesPath = path.join(appPath, 'variables.yaml');
147
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
148
- return yaml.load(variablesContent);
149
- }
150
-
151
- /**
152
- * Extract application configuration from variables.yaml
153
- * @param {Object} variables - Variables from YAML file
154
- * @param {string} appKey - Application key
155
- * @param {Object} options - Registration options
156
- * @returns {Object} Extracted configuration
157
- */
158
- function extractAppConfiguration(variables, appKey, options) {
159
- const appKeyFromFile = variables.app?.key || appKey;
160
- const displayName = variables.app?.name || options.name || appKey;
161
- const description = variables.app?.description || '';
162
-
163
- // Handle external type
164
- if (variables.app?.type === 'external') {
165
- return {
166
- appKey: appKeyFromFile,
167
- displayName,
168
- description,
169
- appType: 'external',
170
- registryMode: 'external',
171
- port: null, // External systems don't need ports
172
- language: null // External systems don't need language
173
- };
174
- }
175
-
176
- const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
177
- const registryMode = 'external';
178
- const port = variables.build?.port || options.port || 3000;
179
- const language = variables.build?.language || 'typescript';
180
-
181
- return {
182
- appKey: appKeyFromFile,
183
- displayName,
184
- description,
185
- appType,
186
- registryMode,
187
- port,
188
- language
32
+ function buildRegistrationData(appConfig, options) {
33
+ const registrationData = {
34
+ key: appConfig.appKey,
35
+ displayName: appConfig.displayName,
36
+ type: appConfig.appType
189
37
  };
190
- }
191
-
192
- /**
193
- * Validate application registration data
194
- * @async
195
- * @param {Object} config - Application configuration
196
- * @param {string} originalAppKey - Original app key for error messages
197
- * @throws {Error} If validation fails
198
- */
199
- async function validateAppRegistrationData(config, originalAppKey) {
200
- const missingFields = [];
201
- if (!config.appKey) missingFields.push('app.key');
202
- if (!config.displayName) missingFields.push('app.name');
203
-
204
- if (missingFields.length > 0) {
205
- logger.error(chalk.red('āŒ Missing required fields in variables.yaml:'));
206
- missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
207
- // Detect app type to show correct path
208
- const { appPath } = await detectAppType(originalAppKey);
209
- const relativePath = path.relative(process.cwd(), appPath);
210
- logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
211
- process.exit(1);
212
- }
213
38
 
214
- try {
215
- registerApplicationSchema.key(config.appKey);
216
- registerApplicationSchema.displayName(config.displayName);
217
- registerApplicationSchema.configuration({
218
- type: config.appType,
219
- registryMode: config.registryMode,
220
- port: config.port
221
- });
222
- } catch (error) {
223
- logger.error(chalk.red(`āŒ Invalid configuration: ${error.message}`));
224
- process.exit(1);
39
+ // Add optional fields only if they have values
40
+ if (appConfig.description || options.description) {
41
+ registrationData.description = appConfig.description || options.description;
225
42
  }
226
- }
227
-
228
- /**
229
- * Check if user is authenticated and get token
230
- * @async
231
- * @param {string} [controllerUrl] - Optional controller URL from variables.yaml
232
- * @param {string} [environment] - Optional environment key
233
- * @returns {Promise<{apiUrl: string, token: string}>} Configuration with API URL and token
234
- */
235
- async function checkAuthentication(controllerUrl, environment) {
236
- const config = await getConfig();
237
-
238
- // Try to get controller URL from parameter, config, or device tokens
239
- let finalControllerUrl = controllerUrl;
240
- let token = null;
241
43
 
242
- // If controller URL provided, try to get device token
243
- if (finalControllerUrl) {
244
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
245
- if (deviceToken && deviceToken.token) {
246
- token = deviceToken.token;
247
- finalControllerUrl = deviceToken.controller;
44
+ // Handle external type vs non-external types differently
45
+ if (appConfig.appType === 'external') {
46
+ // For external type: include externalIntegration, exclude registryMode/port/image
47
+ if (appConfig.externalIntegration) {
48
+ registrationData.externalIntegration = appConfig.externalIntegration;
248
49
  }
249
- }
50
+ } else {
51
+ // For non-external types: include registryMode, port, image
52
+ registrationData.registryMode = appConfig.registryMode;
250
53
 
251
- // If no token yet, try to find any device token in config
252
- if (!token && config.device) {
253
- const deviceUrls = Object.keys(config.device);
254
- if (deviceUrls.length > 0) {
255
- // Use first available device token
256
- finalControllerUrl = deviceUrls[0];
257
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
258
- if (deviceToken && deviceToken.token) {
259
- token = deviceToken.token;
260
- finalControllerUrl = deviceToken.controller;
261
- }
54
+ // Port is required for non-external types
55
+ if (appConfig.port) {
56
+ registrationData.port = appConfig.port;
262
57
  }
263
- }
264
-
265
- // If still no token, check for client token (requires environment and app)
266
- if (!token && environment) {
267
- // For app register, we don't have an app yet, so client tokens won't work
268
- // This is expected - device tokens should be used for registration
269
- }
270
58
 
271
- if (!token || !finalControllerUrl) {
272
- logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
273
- logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
274
- process.exit(1);
59
+ // Image is required for non-external types
60
+ if (appConfig.image) {
61
+ registrationData.image = appConfig.image;
62
+ }
275
63
  }
276
64
 
277
- return {
278
- apiUrl: finalControllerUrl,
279
- token: token
280
- };
65
+ return registrationData;
281
66
  }
282
67
 
283
68
  /**
284
- * Call registration API
69
+ * Save credentials to local secrets if localhost
285
70
  * @async
71
+ * @param {Object} responseData - Registration response data
286
72
  * @param {string} apiUrl - API URL
287
- * @param {string} token - Authentication token
288
- * @param {string} environment - Environment ID
289
- * @param {Object} registrationData - Registration data
290
- * @returns {Promise<Object>} API response
291
73
  */
292
- async function callRegisterApi(apiUrl, token, environment, registrationData) {
293
- const response = await authenticatedApiCall(
294
- `${apiUrl}/api/v1/environments/${encodeURIComponent(environment)}/applications/register`,
295
- {
296
- method: 'POST',
297
- body: JSON.stringify(registrationData)
298
- },
299
- token
300
- );
301
-
302
- if (!response.success) {
303
- const formattedError = response.formattedError || formatApiError(response);
304
- logger.error(formattedError);
305
- process.exit(1);
74
+ async function saveLocalCredentials(responseData, apiUrl) {
75
+ if (!isLocalhost(apiUrl)) {
76
+ return;
306
77
  }
307
78
 
308
- // Handle API response structure:
309
- // makeApiCall returns: { success: true, data: <API response> }
310
- // API response can be:
311
- // 1. Direct format: { application: {...}, credentials: {...} }
312
- // 2. Wrapped format: { success: true, data: { application: {...}, credentials: {...} } }
313
- const apiResponse = response.data;
314
- if (apiResponse && apiResponse.data && apiResponse.data.application) {
315
- // Wrapped format: use apiResponse.data
316
- return apiResponse.data;
317
- } else if (apiResponse && apiResponse.application) {
318
- // Direct format: use apiResponse directly
319
- return apiResponse;
320
- }
321
- // Fallback: return apiResponse as-is (shouldn't happen, but handle gracefully)
322
- logger.error(chalk.red('āŒ Invalid response: missing application data'));
323
- logger.error(chalk.gray('\nFull response for debugging:'));
324
- logger.error(chalk.gray(JSON.stringify(response, null, 2)));
325
- process.exit(1);
326
-
327
- }
328
-
329
- /**
330
- * Get environment prefix for GitHub Secrets
331
- * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro', 'miso')
332
- * @returns {string} Uppercase prefix (e.g., 'DEV', 'TST', 'PRO', 'MISO')
333
- */
334
- function getEnvironmentPrefix(environment) {
335
- if (!environment) {
336
- return 'DEV';
337
- }
338
- // Convert to uppercase and handle common variations
339
- const env = environment.toLowerCase();
340
- if (env === 'dev' || env === 'development') {
341
- return 'DEV';
342
- }
343
- if (env === 'tst' || env === 'test' || env === 'staging') {
344
- return 'TST';
345
- }
346
- if (env === 'pro' || env === 'prod' || env === 'production') {
347
- return 'PRO';
348
- }
349
- // For other environments (e.g., 'miso'), uppercase the entire string
350
- // Use full string if 4 characters or less, otherwise use first 4 characters
351
- const upper = environment.toUpperCase();
352
- return upper.length <= 4 ? upper : upper.substring(0, 4);
353
- }
79
+ const registeredAppKey = responseData.application.key;
80
+ const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
81
+ const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
354
82
 
355
- /**
356
- * Display registration success and credentials
357
- * @param {Object} data - Registration response data
358
- * @param {string} apiUrl - API URL
359
- * @param {string} environment - Environment key
360
- */
361
- function displayRegistrationResults(data, apiUrl, environment) {
362
- logger.log(chalk.green('āœ… Application registered successfully!\n'));
363
- logger.log(chalk.bold('šŸ“‹ Application Details:'));
364
- logger.log(` ID: ${data.application.id}`);
365
- logger.log(` Key: ${data.application.key}`);
366
- logger.log(` Display Name: ${data.application.displayName}\n`);
83
+ try {
84
+ await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
85
+ await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
367
86
 
368
- logger.log(chalk.bold.yellow('šŸ”‘ CREDENTIALS (save these immediately):'));
369
- logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
370
- logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
87
+ // Update env.template
88
+ await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, apiUrl);
371
89
 
372
- logger.log(chalk.red('āš ļø IMPORTANT: Client Secret will not be shown again!\n'));
90
+ // Regenerate .env file with updated credentials
91
+ try {
92
+ await generateEnvFile(registeredAppKey, null, 'local');
93
+ logger.log(chalk.green('āœ“ .env file updated with new credentials'));
94
+ } catch (error) {
95
+ logger.warn(chalk.yellow(`āš ļø Could not regenerate .env file: ${error.message}`));
96
+ }
373
97
 
374
- const envPrefix = getEnvironmentPrefix(environment);
375
- logger.log(chalk.bold('šŸ“ Add to GitHub Secrets:'));
376
- logger.log(chalk.cyan(' Repository level:'));
377
- logger.log(chalk.cyan(` MISO_CONTROLLER_URL = ${apiUrl}`));
378
- logger.log(chalk.cyan(`\n Environment level (${environment}):`));
379
- logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTID = ${data.credentials.clientId}`));
380
- logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTSECRET = ${data.credentials.clientSecret}\n`));
98
+ logger.log(chalk.green('\nāœ“ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
99
+ logger.log(chalk.green('āœ“ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
100
+ } catch (error) {
101
+ logger.warn(chalk.yellow(`āš ļø Could not save credentials locally: ${error.message}`));
102
+ }
381
103
  }
382
104
 
383
105
  /**
@@ -396,49 +118,21 @@ async function registerApplication(appKey, options) {
396
118
 
397
119
  // Load variables.yaml
398
120
  const { variables, created } = await loadVariablesYaml(appKey);
399
- let finalVariables = variables;
400
-
401
- // Create minimal app if needed
402
- if (created) {
403
- finalVariables = await createMinimalAppIfNeeded(appKey, options);
404
- }
121
+ const finalVariables = created
122
+ ? await createMinimalAppIfNeeded(appKey, options)
123
+ : variables;
405
124
 
406
- // Extract configuration
407
- const appConfig = extractAppConfiguration(finalVariables, appKey, options);
408
-
409
- // Validate configuration (pass original appKey for error messages)
125
+ // Extract and validate configuration
126
+ const appConfig = await extractAppConfiguration(finalVariables, appKey, options);
410
127
  await validateAppRegistrationData(appConfig, appKey);
411
128
 
412
- // Get controller URL from variables.yaml if available
129
+ // Authenticate and get API configuration
413
130
  const controllerUrl = finalVariables?.deployment?.controllerUrl;
414
-
415
- // Check authentication (try device token first, supports registration flow)
416
131
  const authConfig = await checkAuthentication(controllerUrl, options.environment);
417
-
418
- // Validate environment
419
132
  const environment = registerApplicationSchema.environmentId(options.environment);
420
133
 
421
- // Prepare registration data to match OpenAPI RegisterApplicationRequest schema
422
- // Schema: { key, displayName, description?, configuration: { type, registryMode, port?, image? } }
423
- const registrationData = {
424
- key: appConfig.appKey,
425
- displayName: appConfig.displayName,
426
- configuration: {
427
- type: appConfig.appType,
428
- registryMode: appConfig.registryMode
429
- }
430
- };
431
-
432
- // Add optional fields only if they have values
433
- if (appConfig.description || options.description) {
434
- registrationData.description = appConfig.description || options.description;
435
- }
436
-
437
- if (appConfig.port) {
438
- registrationData.configuration.port = appConfig.port;
439
- }
440
-
441
134
  // Register application
135
+ const registrationData = buildRegistrationData(appConfig, options);
442
136
  const responseData = await callRegisterApi(
443
137
  authConfig.apiUrl,
444
138
  authConfig.token,
@@ -446,35 +140,8 @@ async function registerApplication(appKey, options) {
446
140
  registrationData
447
141
  );
448
142
 
449
- // Save credentials to local secrets if localhost
450
- if (isLocalhost(authConfig.apiUrl)) {
451
- const registeredAppKey = responseData.application.key;
452
- const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
453
- const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
454
-
455
- try {
456
- await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
457
- await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
458
-
459
- // Update env.template
460
- await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, authConfig.apiUrl);
461
-
462
- // Regenerate .env file with updated credentials
463
- try {
464
- await generateEnvFile(registeredAppKey, null, 'local');
465
- logger.log(chalk.green('āœ“ .env file updated with new credentials'));
466
- } catch (error) {
467
- logger.warn(chalk.yellow(`āš ļø Could not regenerate .env file: ${error.message}`));
468
- }
469
-
470
- logger.log(chalk.green('\nāœ“ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
471
- logger.log(chalk.green('āœ“ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
472
- } catch (error) {
473
- logger.warn(chalk.yellow(`āš ļø Could not save credentials locally: ${error.message}`));
474
- }
475
- }
476
-
477
- // Display results
143
+ // Save credentials and display results
144
+ await saveLocalCredentials(responseData, authConfig.apiUrl);
478
145
  displayRegistrationResults(responseData, authConfig.apiUrl, environment);
479
146
  }
480
147
 
@@ -11,7 +11,7 @@
11
11
  const chalk = require('chalk');
12
12
  const { getConfig } = require('./config');
13
13
  const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
- const { authenticatedApiCall } = require('./utils/api');
14
+ const { rotateApplicationSecret } = require('./api/applications.api');
15
15
  const { formatApiError } = require('./utils/api-error-handler');
16
16
  const logger = require('./utils/logger');
17
17
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
@@ -115,15 +115,9 @@ async function rotateSecret(appKey, options) {
115
115
  // Validate environment
116
116
  validateEnvironment(options.environment);
117
117
 
118
- // OpenAPI spec: POST /api/v1/environments/{envKey}/applications/{appKey}/rotate-secret
119
- // Path parameters: envKey, appKey (no query parameters)
120
- const response = await authenticatedApiCall(
121
- `${controllerUrl}/api/v1/environments/${encodeURIComponent(options.environment)}/applications/${encodeURIComponent(appKey)}/rotate-secret`,
122
- {
123
- method: 'POST'
124
- },
125
- token
126
- );
118
+ // Use centralized API client
119
+ const authConfig = { type: 'bearer', token: token };
120
+ const response = await rotateApplicationSecret(controllerUrl, options.environment, appKey, authConfig);
127
121
 
128
122
  if (!response.success) {
129
123
  const formattedError = response.formattedError || formatApiError(response);
@@ -13,7 +13,8 @@ const inquirer = require('inquirer');
13
13
  const chalk = require('chalk');
14
14
  const ora = require('ora');
15
15
  const { setCurrentEnvironment, saveDeviceToken, saveClientToken } = require('../config');
16
- const { makeApiCall, initiateDeviceCodeFlow, pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
16
+ const { getToken, initiateDeviceCodeFlow } = require('../api/auth.api');
17
+ const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
17
18
  const { formatApiError } = require('../utils/api-error-handler');
18
19
  const { loadClientCredentials } = require('../utils/token-manager');
19
20
  const logger = require('../utils/logger');
@@ -187,16 +188,8 @@ async function handleCredentialsLogin(controllerUrl, appName, clientId, clientSe
187
188
  credentials = await promptForCredentials(clientId, clientSecret);
188
189
  }
189
190
 
190
- // OpenAPI spec: POST /api/v1/auth/token with x-client-id and x-client-secret headers
191
- // Response: { success: boolean, token: string, expiresIn: number, expiresAt: string, timestamp: string }
192
- const response = await makeApiCall(`${controllerUrl}/api/v1/auth/token`, {
193
- method: 'POST',
194
- headers: {
195
- 'Content-Type': 'application/json',
196
- 'x-client-id': credentials.clientId,
197
- 'x-client-secret': credentials.clientSecret
198
- }
199
- });
191
+ // Use centralized API client for token generation
192
+ const response = await getToken(credentials.clientId, credentials.clientSecret, controllerUrl);
200
193
 
201
194
  if (!response.success) {
202
195
  const formattedError = response.formattedError || formatApiError(response);
@@ -346,7 +339,21 @@ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope)
346
339
  }
347
340
 
348
341
  try {
349
- const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
342
+ // Use centralized API client for device code flow initiation
343
+ const deviceCodeApiResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
344
+
345
+ // Handle API response format: { success: boolean, data: DeviceCodeResponse }
346
+ const apiResponse = deviceCodeApiResponse.data;
347
+ const deviceCodeData = apiResponse.data || apiResponse;
348
+
349
+ // Convert camelCase from API to snake_case for compatibility with existing code
350
+ const deviceCodeResponse = {
351
+ device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
352
+ user_code: deviceCodeData.userCode || deviceCodeData.user_code,
353
+ verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
354
+ expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
355
+ interval: deviceCodeData.interval || 5
356
+ };
350
357
 
351
358
  displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
352
359
 
@@ -12,7 +12,8 @@
12
12
  const fs = require('fs');
13
13
  const chalk = require('chalk');
14
14
  const { getDeploymentAuth } = require('./utils/token-manager');
15
- const { authenticatedApiCall } = require('./utils/api');
15
+ const { getEnvironmentApplication } = require('./api/environments.api');
16
+ const { publishDatasourceViaPipeline } = require('./api/pipeline.api');
16
17
  const { formatApiError } = require('./utils/api-error-handler');
17
18
  const logger = require('./utils/logger');
18
19
  const { validateDatasourceFile } = require('./datasource-validate');
@@ -30,18 +31,8 @@ const { validateDatasourceFile } = require('./datasource-validate');
30
31
  * @throws {Error} If dataplane URL cannot be retrieved
31
32
  */
32
33
  async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
33
- // Call controller API to get application details
34
- // Expected: GET /api/v1/environments/{env}/applications/{appKey}
35
- const endpoint = `${controllerUrl}/api/v1/environments/${environment}/applications/${appKey}`;
36
-
37
- let response;
38
- if (authConfig.type === 'bearer' && authConfig.token) {
39
- response = await authenticatedApiCall(endpoint, {}, authConfig.token);
40
- } else {
41
- // For credentials, we'd need to use a different API call method
42
- // For now, use bearer token approach
43
- throw new Error('Bearer token authentication required for getting dataplane URL');
44
- }
34
+ // Call controller API to get application details using centralized API client
35
+ const response = await getEnvironmentApplication(controllerUrl, environment, appKey, authConfig);
45
36
 
46
37
  if (!response.success || !response.data) {
47
38
  const formattedError = response.formattedError || formatApiError(response);
@@ -129,24 +120,10 @@ async function deployDatasource(appKey, filePath, options) {
129
120
  const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
130
121
  logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
131
122
 
132
- // Publish to dataplane (using publish endpoint)
123
+ // Publish to dataplane using pipeline workflow endpoint
133
124
  logger.log(chalk.blue('\nšŸš€ Publishing datasource to dataplane...'));
134
- const publishEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/publish`;
135
-
136
- // Prepare publish request - send datasource configuration directly
137
- let publishResponse;
138
- if (authConfig.type === 'bearer' && authConfig.token) {
139
- publishResponse = await authenticatedApiCall(
140
- publishEndpoint,
141
- {
142
- method: 'POST',
143
- body: JSON.stringify(datasourceConfig)
144
- },
145
- authConfig.token
146
- );
147
- } else {
148
- throw new Error('Bearer token authentication required for dataplane publish');
149
- }
125
+
126
+ const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig);
150
127
 
151
128
  if (!publishResponse.success) {
152
129
  const formattedError = publishResponse.formattedError || formatApiError(publishResponse);