@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
@@ -0,0 +1,205 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Configuration Utilities
3
+ *
4
+ * Configuration extraction and loading for application registration
5
+ *
6
+ * @fileoverview Configuration utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const yaml = require('js-yaml');
15
+ const logger = require('./logger');
16
+ const { detectAppType } = require('./paths');
17
+
18
+ // Import createApp to auto-generate config if missing
19
+ let createApp;
20
+ try {
21
+ createApp = require('../app').createApp;
22
+ } catch {
23
+ createApp = null;
24
+ }
25
+
26
+ /**
27
+ * Load variables.yaml file for an application
28
+ * @async
29
+ * @param {string} appKey - Application key
30
+ * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
31
+ */
32
+ async function loadVariablesYaml(appKey) {
33
+ // Detect app type and get correct path (integration or builder)
34
+ const { appPath } = await detectAppType(appKey);
35
+ const variablesPath = path.join(appPath, 'variables.yaml');
36
+
37
+ try {
38
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
39
+ return { variables: yaml.load(variablesContent), created: false };
40
+ } catch (error) {
41
+ if (error.code === 'ENOENT') {
42
+ logger.log(chalk.yellow(`⚠️ variables.yaml not found for ${appKey}`));
43
+ logger.log(chalk.yellow('📝 Creating minimal configuration...\n'));
44
+ return { variables: null, created: true };
45
+ }
46
+ throw new Error(`Failed to read variables.yaml: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Create minimal application configuration if needed
52
+ * @async
53
+ * @param {string} appKey - Application key
54
+ * @param {Object} options - Registration options
55
+ * @returns {Promise<Object>} Variables after creation
56
+ */
57
+ async function createMinimalAppIfNeeded(appKey, options) {
58
+ if (!createApp) {
59
+ throw new Error('Cannot auto-create application: createApp function not available');
60
+ }
61
+
62
+ await createApp(appKey, {
63
+ port: options.port,
64
+ language: 'typescript',
65
+ database: false,
66
+ redis: false,
67
+ storage: false,
68
+ authentication: false
69
+ });
70
+
71
+ // Detect app type and get correct path (integration or builder)
72
+ const { appPath } = await detectAppType(appKey);
73
+ const variablesPath = path.join(appPath, 'variables.yaml');
74
+ const variablesContent = await fs.readFile(variablesPath, 'utf-8');
75
+ return yaml.load(variablesContent);
76
+ }
77
+
78
+ /**
79
+ * Builds image reference string from variables
80
+ * Format: repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)
81
+ * @param {Object} variables - Variables from YAML file
82
+ * @param {string} appKey - Application key (fallback)
83
+ * @returns {string} Image reference string
84
+ */
85
+ function buildImageReference(variables, appKey) {
86
+ const imageName = variables.image?.name || variables.app?.key || appKey;
87
+ const registry = variables.image?.registry;
88
+ const tag = variables.image?.tag || 'latest';
89
+ return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
90
+ }
91
+
92
+ /**
93
+ * Extract URL from external system JSON file for registration
94
+ * @async
95
+ * @param {string} appKey - Application key
96
+ * @param {Object} externalIntegration - External integration config from variables.yaml
97
+ * @returns {Promise<{url: string, apiKey?: string}>} URL and optional API key
98
+ */
99
+ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
100
+ if (!externalIntegration || !externalIntegration.systems || externalIntegration.systems.length === 0) {
101
+ throw new Error('externalIntegration.systems is required for external type applications');
102
+ }
103
+
104
+ // Detect app type and get correct path (integration or builder)
105
+ const { appPath } = await detectAppType(appKey);
106
+ const schemaBasePath = externalIntegration.schemaBasePath || './';
107
+ const systemFileName = externalIntegration.systems[0];
108
+
109
+ // Resolve system file path (handle both relative and absolute paths)
110
+ const systemFilePath = path.isAbsolute(schemaBasePath)
111
+ ? path.join(schemaBasePath, systemFileName)
112
+ : path.join(appPath, schemaBasePath, systemFileName);
113
+
114
+ try {
115
+ const systemContent = await fs.readFile(systemFilePath, 'utf-8');
116
+ const systemJson = JSON.parse(systemContent);
117
+
118
+ // Extract URL from environment.baseUrl
119
+ const url = systemJson.environment?.baseUrl;
120
+ if (!url) {
121
+ throw new Error(`Missing environment.baseUrl in ${systemFileName}`);
122
+ }
123
+
124
+ // Extract optional API key from authentication if present
125
+ let apiKey;
126
+ if (systemJson.authentication?.apikey?.key) {
127
+ // If it's a kv:// reference, we can't resolve it here, so leave it undefined
128
+ // The API will handle kv:// references
129
+ const keyValue = systemJson.authentication.apikey.key;
130
+ if (!keyValue.startsWith('kv://') && !keyValue.startsWith('{{')) {
131
+ apiKey = keyValue;
132
+ }
133
+ }
134
+
135
+ return { url, apiKey };
136
+ } catch (error) {
137
+ if (error.code === 'ENOENT') {
138
+ throw new Error(`External system file not found: ${systemFilePath}`);
139
+ }
140
+ throw new Error(`Failed to read external system file: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Extract application configuration from variables.yaml
146
+ * @async
147
+ * @param {Object} variables - Variables from YAML file
148
+ * @param {string} appKey - Application key
149
+ * @param {Object} options - Registration options
150
+ * @returns {Promise<Object>} Extracted configuration
151
+ */
152
+ async function extractAppConfiguration(variables, appKey, options) {
153
+ const appKeyFromFile = variables.app?.key || appKey;
154
+ const displayName = variables.app?.name || options.name || appKey;
155
+ const description = variables.app?.description || '';
156
+
157
+ // Handle external type
158
+ if (variables.app?.type === 'external') {
159
+ // Extract URL from external system JSON file
160
+ const { url, apiKey } = await extractExternalIntegrationUrl(appKey, variables.externalIntegration);
161
+
162
+ // Build simplified externalIntegration object for registration API
163
+ const externalIntegration = { url };
164
+ if (apiKey) {
165
+ externalIntegration.apiKey = apiKey;
166
+ }
167
+
168
+ return {
169
+ appKey: appKeyFromFile,
170
+ displayName,
171
+ description,
172
+ appType: 'external',
173
+ externalIntegration,
174
+ port: null, // External systems don't need ports
175
+ image: null, // External systems don't need images
176
+ language: null // External systems don't need language
177
+ };
178
+ }
179
+
180
+ const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
181
+ const registryMode = variables.image?.registryMode || 'external';
182
+ const port = variables.build?.port || options.port || 3000;
183
+ const language = variables.build?.language || 'typescript';
184
+ const image = buildImageReference(variables, appKeyFromFile);
185
+
186
+ return {
187
+ appKey: appKeyFromFile,
188
+ displayName,
189
+ description,
190
+ appType,
191
+ registryMode,
192
+ port,
193
+ image,
194
+ language
195
+ };
196
+ }
197
+
198
+ module.exports = {
199
+ loadVariablesYaml,
200
+ createMinimalAppIfNeeded,
201
+ buildImageReference,
202
+ extractAppConfiguration,
203
+ extractExternalIntegrationUrl
204
+ };
205
+
@@ -0,0 +1,69 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Display Utilities
3
+ *
4
+ * Display and formatting utilities for application registration results
5
+ *
6
+ * @fileoverview Display utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ /**
15
+ * Get environment prefix for GitHub Secrets
16
+ * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro', 'miso')
17
+ * @returns {string} Uppercase prefix (e.g., 'DEV', 'TST', 'PRO', 'MISO')
18
+ */
19
+ function getEnvironmentPrefix(environment) {
20
+ if (!environment) {
21
+ return 'DEV';
22
+ }
23
+ // Convert to uppercase and handle common variations
24
+ const env = environment.toLowerCase();
25
+ if (env === 'dev' || env === 'development') {
26
+ return 'DEV';
27
+ }
28
+ if (env === 'tst' || env === 'test' || env === 'staging') {
29
+ return 'TST';
30
+ }
31
+ if (env === 'pro' || env === 'prod' || env === 'production') {
32
+ return 'PRO';
33
+ }
34
+ // For other environments (e.g., 'miso'), uppercase the entire string
35
+ // Use full string if 4 characters or less, otherwise use first 4 characters
36
+ const upper = environment.toUpperCase();
37
+ return upper.length <= 4 ? upper : upper.substring(0, 4);
38
+ }
39
+
40
+ /**
41
+ * Display registration success and credentials
42
+ * @param {Object} data - Registration response data
43
+ * @param {string} apiUrl - API URL
44
+ * @param {string} environment - Environment key
45
+ */
46
+ function displayRegistrationResults(data, apiUrl, environment) {
47
+ logger.log(chalk.green('✅ Application registered successfully!\n'));
48
+ logger.log(chalk.bold('📋 Application Details:'));
49
+ logger.log(` ID: ${data.application.id}`);
50
+ logger.log(` Key: ${data.application.key}`);
51
+ logger.log(` Display Name: ${data.application.displayName}\n`);
52
+
53
+ logger.log(chalk.bold.yellow('🔑 CREDENTIALS (save these immediately):'));
54
+ logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
55
+ logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
56
+
57
+ logger.log(chalk.red('⚠️ IMPORTANT: Client Secret will not be shown again!\n'));
58
+
59
+ const envPrefix = getEnvironmentPrefix(environment);
60
+ logger.log(chalk.bold('📝 Add to GitHub Secrets:'));
61
+ logger.log(chalk.cyan(' Repository level:'));
62
+ logger.log(chalk.cyan(` MISO_CONTROLLER_URL = ${apiUrl}`));
63
+ logger.log(chalk.cyan(`\n Environment level (${environment}):`));
64
+ logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTID = ${data.credentials.clientId}`));
65
+ logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTSECRET = ${data.credentials.clientSecret}\n`));
66
+ }
67
+
68
+ module.exports = { displayRegistrationResults, getEnvironmentPrefix };
69
+
@@ -0,0 +1,143 @@
1
+ /**
2
+ * AI Fabrix Builder - App Register Validation Utilities
3
+ *
4
+ * Validation logic for application registration
5
+ *
6
+ * @fileoverview Validation utilities for app registration
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const path = require('path');
12
+ const chalk = require('chalk');
13
+ const logger = require('./logger');
14
+ const { detectAppType } = require('./paths');
15
+ const applicationSchema = require('../schema/application-schema.json');
16
+
17
+ // Extract valid enum values from application schema
18
+ const validTypes = applicationSchema.properties.type.enum || [];
19
+ const validRegistryModes = applicationSchema.properties.registryMode.enum || [];
20
+ const portConstraints = {
21
+ minimum: applicationSchema.properties.port?.minimum || 1,
22
+ maximum: applicationSchema.properties.port?.maximum || 65535
23
+ };
24
+ const imagePattern = applicationSchema.properties.image?.pattern || /^[a-zA-Z0-9._/-]+:[a-zA-Z0-9._-]+$/;
25
+
26
+ /**
27
+ * Validation schema for application registration
28
+ * Validates according to application-schema.json
29
+ */
30
+ const registerApplicationSchema = {
31
+ environmentId: (val) => {
32
+ if (!val || val.length < 1) {
33
+ throw new Error('Invalid environment ID format');
34
+ }
35
+ return val;
36
+ },
37
+ key: (val) => {
38
+ if (!val || val.length < 1) {
39
+ throw new Error('Application key is required');
40
+ }
41
+ const keyPattern = applicationSchema.properties.key.pattern;
42
+ const keyMaxLength = applicationSchema.properties.key.maxLength || 50;
43
+ if (val.length > keyMaxLength) {
44
+ throw new Error(`Application key must be at most ${keyMaxLength} characters`);
45
+ }
46
+ if (keyPattern && !new RegExp(keyPattern).test(val)) {
47
+ throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
48
+ }
49
+ return val;
50
+ },
51
+ displayName: (val) => {
52
+ if (!val || val.length < 1) {
53
+ throw new Error('Display name is required');
54
+ }
55
+ const displayNameMaxLength = applicationSchema.properties.displayName.maxLength || 100;
56
+ if (val.length > displayNameMaxLength) {
57
+ throw new Error(`Display name must be at most ${displayNameMaxLength} characters`);
58
+ }
59
+ return val;
60
+ },
61
+ description: (val) => val || undefined,
62
+ image: (val, appType) => {
63
+ // Image is required for non-external types
64
+ if (appType !== 'external') {
65
+ if (!val || typeof val !== 'string') {
66
+ throw new Error('Image is required for non-external application types');
67
+ }
68
+ // Validate image format: repository:tag
69
+ const pattern = typeof imagePattern === 'string' ? new RegExp(imagePattern) : imagePattern;
70
+ if (!pattern.test(val)) {
71
+ throw new Error('Image must be in format repository:tag (e.g., aifabrix/miso-controller:latest or myregistry.azurecr.io/miso-controller:v1.0.0)');
72
+ }
73
+ }
74
+ return val || undefined;
75
+ },
76
+ configuration: (val) => {
77
+ if (!val || !val.type || !validTypes.includes(val.type)) {
78
+ throw new Error(`Configuration type must be one of: ${validTypes.join(', ')}`);
79
+ }
80
+
81
+ // For external type: require externalIntegration, skip registryMode/port validation
82
+ if (val.type === 'external') {
83
+ if (!val.externalIntegration) {
84
+ throw new Error('externalIntegration is required for external application type');
85
+ }
86
+ // External type should not have registryMode, port, or image
87
+ return val;
88
+ }
89
+
90
+ // For non-external types: require registryMode, port, image
91
+ if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
92
+ throw new Error(`Registry mode must be one of: ${validRegistryModes.join(', ')}`);
93
+ }
94
+ if (val.port === undefined || val.port === null) {
95
+ throw new Error('Port is required for non-external application types');
96
+ }
97
+ if (!Number.isInteger(val.port) || val.port < portConstraints.minimum || val.port > portConstraints.maximum) {
98
+ throw new Error(`Port must be an integer between ${portConstraints.minimum} and ${portConstraints.maximum}`);
99
+ }
100
+ return val;
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Validate application registration data
106
+ * @async
107
+ * @param {Object} config - Application configuration
108
+ * @param {string} originalAppKey - Original app key for error messages
109
+ * @throws {Error} If validation fails
110
+ */
111
+ async function validateAppRegistrationData(config, originalAppKey) {
112
+ const missingFields = [];
113
+ if (!config.appKey) missingFields.push('app.key');
114
+ if (!config.displayName) missingFields.push('app.name');
115
+
116
+ if (missingFields.length > 0) {
117
+ logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
118
+ missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
119
+ // Detect app type to show correct path
120
+ const { appPath } = await detectAppType(originalAppKey);
121
+ const relativePath = path.relative(process.cwd(), appPath);
122
+ logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
123
+ process.exit(1);
124
+ }
125
+
126
+ try {
127
+ registerApplicationSchema.key(config.appKey);
128
+ registerApplicationSchema.displayName(config.displayName);
129
+ registerApplicationSchema.image(config.image, config.appType);
130
+ registerApplicationSchema.configuration({
131
+ type: config.appType,
132
+ registryMode: config.registryMode,
133
+ port: config.port,
134
+ externalIntegration: config.externalIntegration
135
+ });
136
+ } catch (error) {
137
+ logger.error(chalk.red(`❌ Invalid configuration: ${error.message}`));
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ module.exports = { registerApplicationSchema, validateAppRegistrationData };
143
+
@@ -18,8 +18,17 @@ const { parseErrorResponse } = require('./api-error-handler');
18
18
  * @returns {Object} Structured error information
19
19
  */
20
20
  function handleDeploymentError(error) {
21
+ if (!error) {
22
+ return {
23
+ message: 'Unknown error',
24
+ code: 'UNKNOWN',
25
+ timeout: false,
26
+ status: undefined,
27
+ data: undefined
28
+ };
29
+ }
21
30
  const safeError = {
22
- message: error.message,
31
+ message: error.message || 'Unknown error',
23
32
  code: error.code || 'UNKNOWN',
24
33
  timeout: error.code === 'ECONNABORTED',
25
34
  status: error.status || error.response?.status,
@@ -42,6 +51,38 @@ function handleDeploymentError(error) {
42
51
  * @throws {Error} User-friendly error message
43
52
  */
44
53
  async function handleDeploymentErrors(error, appName, url, alreadyLogged = false) {
54
+ // For validation errors (like URL validation), just re-throw them directly
55
+ // They already have user-friendly messages
56
+ // Handle both Error objects and strings
57
+ let errorMessage = '';
58
+ if (error instanceof Error) {
59
+ errorMessage = error.message || '';
60
+ } else if (typeof error === 'string') {
61
+ errorMessage = error;
62
+ } else if (error && typeof error === 'object' && error.message) {
63
+ errorMessage = error.message;
64
+ }
65
+
66
+ if (errorMessage && (
67
+ errorMessage.includes('Controller URL must use HTTPS') ||
68
+ errorMessage.includes('Invalid environment key') ||
69
+ errorMessage.includes('Environment key is required') ||
70
+ errorMessage.includes('Authentication configuration is required') ||
71
+ errorMessage.includes('Invalid controller URL format') ||
72
+ errorMessage.includes('Controller URL is required')
73
+ )) {
74
+ // If error is a string, convert to Error object
75
+ if (typeof error === 'string') {
76
+ throw new Error(error);
77
+ }
78
+ // If it's already an Error object, re-throw it directly
79
+ if (error instanceof Error) {
80
+ throw error;
81
+ }
82
+ // Otherwise, create a new Error with the message
83
+ throw new Error(errorMessage);
84
+ }
85
+
45
86
  // Log to audit log if not already logged
46
87
  if (!alreadyLogged) {
47
88
  try {
@@ -63,7 +104,17 @@ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false
63
104
 
64
105
  // Ensure errorData is not undefined before parsing
65
106
  // If errorData is undefined, use the error message instead
66
- const errorResponse = errorData !== undefined ? errorData : safeError.message;
107
+ let errorResponse = errorData !== undefined ? errorData : safeError.message;
108
+
109
+ // Ensure errorResponse is a string or object, not an Error object
110
+ if (errorResponse instanceof Error) {
111
+ errorResponse = errorResponse.message || 'Unknown error occurred';
112
+ }
113
+
114
+ // Ensure errorResponse is not null or undefined
115
+ if (errorResponse === null || errorResponse === undefined) {
116
+ errorResponse = safeError.message || 'Unknown error occurred';
117
+ }
67
118
 
68
119
  // Determine if this is a network error
69
120
  const isNetworkError = safeError.code === 'ECONNREFUSED' ||
@@ -72,13 +123,44 @@ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false
72
123
  safeError.timeout;
73
124
 
74
125
  // Parse error using error handler
75
- const parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetworkError);
126
+ let parsedError;
127
+ try {
128
+ parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetworkError);
129
+ // Ensure parsedError is a valid object
130
+ if (!parsedError || typeof parsedError !== 'object') {
131
+ throw new Error('parseErrorResponse returned invalid result');
132
+ }
133
+ } catch (parseErr) {
134
+ // If parsing fails, use the safe error message
135
+ parsedError = {
136
+ message: safeError.message || 'Unknown error occurred',
137
+ formatted: safeError.message || 'Unknown error occurred',
138
+ data: undefined
139
+ };
140
+ }
141
+
142
+ // Ensure parsedError is always a valid object with required properties
143
+ if (!parsedError || typeof parsedError !== 'object') {
144
+ parsedError = {
145
+ message: safeError.message || 'Unknown error occurred',
146
+ formatted: safeError.message || 'Unknown error occurred',
147
+ data: undefined
148
+ };
149
+ }
150
+
151
+ // Ensure we have a message - handle case where parsedError.message might be undefined
152
+ const finalErrorMessage = (parsedError && parsedError.message) ? parsedError.message : (safeError.message || 'Unknown error occurred');
153
+
154
+ // Validate finalErrorMessage is a string
155
+ if (typeof finalErrorMessage !== 'string') {
156
+ throw new Error(`Invalid error message type: ${typeof finalErrorMessage}. Error: ${JSON.stringify(error)}`);
157
+ }
76
158
 
77
159
  // Throw clean error message (without emoji) - CLI will format it
78
- const formattedError = new Error(parsedError.message);
79
- formattedError.formatted = parsedError.formatted;
160
+ const formattedError = new Error(finalErrorMessage);
161
+ formattedError.formatted = parsedError?.formatted || finalErrorMessage;
80
162
  formattedError.status = safeError.status;
81
- formattedError.data = parsedError.data;
163
+ formattedError.data = parsedError?.data;
82
164
  formattedError._logged = true; // Mark as logged to prevent double-logging
83
165
  throw formattedError;
84
166
  }
@@ -388,7 +388,7 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
388
388
  }
389
389
 
390
390
  const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
391
- const response = await makeApiCall(url, {
391
+ const response = await getMakeApiCall()(url, {
392
392
  method: 'POST',
393
393
  headers: {
394
394
  'Content-Type': 'application/json'