@aifabrix/builder 2.31.1 → 2.32.2

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Login Credentials Handling
3
+ *
4
+ * Handles credentials-based authentication flow
5
+ *
6
+ * @fileoverview Credentials login handling for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const inquirer = require('inquirer');
12
+ const chalk = require('chalk');
13
+ const { getToken } = require('../api/auth.api');
14
+ const { formatApiError } = require('../utils/api-error-handler');
15
+ const { loadClientCredentials } = require('../utils/token-manager');
16
+ const logger = require('../utils/logger');
17
+
18
+ /**
19
+ * Tries to load credentials from secrets file
20
+ * @async
21
+ * @function tryLoadCredentialsFromSecrets
22
+ * @param {string} appName - Application name
23
+ * @returns {Promise<Object|null>} Credentials or null
24
+ */
25
+ async function tryLoadCredentialsFromSecrets(appName) {
26
+ if (!appName) {
27
+ return null;
28
+ }
29
+ const credentials = await loadClientCredentials(appName);
30
+ if (!credentials) {
31
+ logger.log(chalk.yellow(`⚠️ Credentials not found in secrets.local.yaml for app '${appName}'`));
32
+ logger.log(chalk.gray(` Looking for: '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`));
33
+ logger.log(chalk.gray(' Prompting for credentials...\n'));
34
+ }
35
+ return credentials;
36
+ }
37
+
38
+ /**
39
+ * Prompt for credentials if not provided
40
+ * @async
41
+ * @param {string} [clientId] - Existing client ID
42
+ * @param {string} [clientSecret] - Existing client secret
43
+ * @returns {Promise<{clientId: string, clientSecret: string}>} Credentials
44
+ */
45
+ async function promptForCredentials(clientId, clientSecret) {
46
+ if (clientId && clientSecret) {
47
+ return { clientId: clientId.trim(), clientSecret: clientSecret.trim() };
48
+ }
49
+
50
+ const credentials = await inquirer.prompt([
51
+ {
52
+ type: 'input',
53
+ name: 'clientId',
54
+ message: 'Client ID:',
55
+ default: clientId || '',
56
+ validate: (input) => {
57
+ const value = input.trim();
58
+ if (!value || value.length === 0) {
59
+ return 'Client ID is required';
60
+ }
61
+ return true;
62
+ }
63
+ },
64
+ {
65
+ type: 'password',
66
+ name: 'clientSecret',
67
+ message: 'Client Secret:',
68
+ default: clientSecret || '',
69
+ mask: '*',
70
+ validate: (input) => {
71
+ const value = input.trim();
72
+ if (!value || value.length === 0) {
73
+ return 'Client Secret is required';
74
+ }
75
+ return true;
76
+ }
77
+ }
78
+ ]);
79
+
80
+ return {
81
+ clientId: credentials.clientId.trim(),
82
+ clientSecret: credentials.clientSecret.trim()
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Gets credentials from secrets or prompts user
88
+ * @async
89
+ * @function getCredentialsForLogin
90
+ * @param {string} appName - Application name
91
+ * @param {string} clientId - Optional client ID
92
+ * @param {string} clientSecret - Optional client secret
93
+ * @returns {Promise<Object>} Credentials object
94
+ */
95
+ async function getCredentialsForLogin(appName, clientId, clientSecret) {
96
+ let credentials = null;
97
+
98
+ // Try to load from secrets.local.yaml if appName provided and credentials not provided
99
+ if (appName && !clientId && !clientSecret) {
100
+ credentials = await tryLoadCredentialsFromSecrets(appName);
101
+ }
102
+
103
+ // If still no credentials, prompt for them
104
+ if (!credentials) {
105
+ credentials = await promptForCredentials(clientId, clientSecret);
106
+ }
107
+
108
+ return credentials;
109
+ }
110
+
111
+ /**
112
+ * Handles login error response
113
+ * @function handleLoginError
114
+ * @param {Object} response - API response
115
+ * @param {string} appName - Application name
116
+ */
117
+ function handleLoginError(response, appName) {
118
+ const formattedError = response.formattedError || formatApiError(response);
119
+ logger.error(formattedError);
120
+
121
+ // Provide additional context for login failures
122
+ if (response.status === 401) {
123
+ logger.log(chalk.gray('\n💡 Tip: Verify your client credentials are correct.'));
124
+ logger.log(chalk.gray(' Check secrets.local.yaml for:'));
125
+ logger.log(chalk.gray(` - ${appName}-client-idKeyVault`));
126
+ logger.log(chalk.gray(` - ${appName}-client-secretKeyVault`));
127
+ }
128
+
129
+ process.exit(1);
130
+ }
131
+
132
+ /**
133
+ * Extracts token data from API response
134
+ * @function extractTokenData
135
+ * @param {Object} response - API response
136
+ * @returns {Object} Token data object
137
+ */
138
+ function extractTokenData(response) {
139
+ // OpenAPI spec response: { success: boolean, token: string, expiresIn: number, expiresAt: string, ... }
140
+ // Handle both flat and nested response structures (some APIs wrap in data field)
141
+ // If response.data exists, use it; otherwise use response directly
142
+ const apiResponse = response.data || response;
143
+ const responseData = apiResponse.data || apiResponse;
144
+
145
+ if (!responseData || !responseData.token) {
146
+ logger.error(chalk.red('❌ Invalid response: missing token'));
147
+ if (responseData) {
148
+ logger.error(chalk.gray(`Response structure: ${JSON.stringify(responseData, null, 2)}`));
149
+ }
150
+ process.exit(1);
151
+ }
152
+
153
+ return responseData;
154
+ }
155
+
156
+ /**
157
+ * Calculates token expiration timestamp
158
+ * @function calculateTokenExpiration
159
+ * @param {Object} responseData - Response data with expiration info
160
+ * @returns {string} ISO timestamp
161
+ */
162
+ function calculateTokenExpiration(responseData) {
163
+ // Calculate expiration (use expiresAt if provided, otherwise calculate from expiresIn, default to 24 hours)
164
+ if (responseData.expiresAt) {
165
+ return responseData.expiresAt;
166
+ }
167
+ if (responseData.expiresIn) {
168
+ return new Date(Date.now() + responseData.expiresIn * 1000).toISOString();
169
+ }
170
+ return new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
171
+ }
172
+
173
+ /**
174
+ * Handle credentials-based login
175
+ * Uses OpenAPI /api/v1/auth/token endpoint with x-client-id and x-client-secret headers
176
+ * Reads credentials from secrets.local.yaml using pattern <app-name>-client-idKeyVault
177
+ * @async
178
+ * @param {string} controllerUrl - Controller URL
179
+ * @param {string} appName - Application name
180
+ * @param {string} [clientId] - Client ID from options (optional, overrides secrets.local.yaml)
181
+ * @param {string} [clientSecret] - Client Secret from options (optional, overrides secrets.local.yaml)
182
+ * @returns {Promise<{token: string, expiresAt: string}>} Authentication token and expiration
183
+ */
184
+ async function handleCredentialsLogin(controllerUrl, appName, clientId, clientSecret) {
185
+ const credentials = await getCredentialsForLogin(appName, clientId, clientSecret);
186
+
187
+ // Use centralized API client for token generation
188
+ const response = await getToken(credentials.clientId, credentials.clientSecret, controllerUrl);
189
+
190
+ if (!response.success) {
191
+ handleLoginError(response, appName);
192
+ }
193
+
194
+ const responseData = extractTokenData(response);
195
+ const expiresAt = calculateTokenExpiration(responseData);
196
+
197
+ return {
198
+ token: responseData.token,
199
+ expiresAt: expiresAt
200
+ };
201
+ }
202
+
203
+ module.exports = {
204
+ handleCredentialsLogin,
205
+ getCredentialsForLogin,
206
+ promptForCredentials,
207
+ tryLoadCredentialsFromSecrets
208
+ };
209
+
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Login Device Code Flow Handling
3
+ *
4
+ * Handles device code flow authentication
5
+ *
6
+ * @fileoverview Device code login handling for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const inquirer = require('inquirer');
12
+ const chalk = require('chalk');
13
+ const ora = require('ora');
14
+ const { setCurrentEnvironment, saveDeviceToken } = require('../core/config');
15
+ const { initiateDeviceCodeFlow } = require('../api/auth.api');
16
+ const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
17
+ const logger = require('../utils/logger');
18
+
19
+ /**
20
+ * Validate environment key format
21
+ * @param {string} envKey - Environment key to validate
22
+ * @throws {Error} If environment key format is invalid
23
+ */
24
+ function validateEnvironmentKey(envKey) {
25
+ if (!/^[a-z0-9-_]+$/i.test(envKey)) {
26
+ throw new Error('Environment key must contain only letters, numbers, hyphens, and underscores');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get and validate environment key
32
+ * @async
33
+ * @param {string} [environment] - Environment key from options
34
+ * @returns {Promise<string>} Validated environment key
35
+ */
36
+ async function getEnvironmentKey(environment) {
37
+ if (environment) {
38
+ const envKey = environment.trim();
39
+ validateEnvironmentKey(envKey);
40
+ return envKey;
41
+ }
42
+
43
+ const envPrompt = await inquirer.prompt([{
44
+ type: 'input',
45
+ name: 'environment',
46
+ message: 'Environment key (e.g., miso, dev, tst, pro):',
47
+ validate: (input) => {
48
+ if (!input || input.trim().length === 0) {
49
+ return 'Environment key is required';
50
+ }
51
+ validateEnvironmentKey(input.trim());
52
+ return true;
53
+ }
54
+ }]);
55
+
56
+ return envPrompt.environment.trim();
57
+ }
58
+
59
+ /**
60
+ * Save device token configuration (root level, controller-specific)
61
+ * @async
62
+ * @param {string} controllerUrl - Controller URL (used as key)
63
+ * @param {string} token - Authentication token
64
+ * @param {string} refreshToken - Refresh token for token renewal
65
+ * @param {string} expiresAt - Token expiration time
66
+ */
67
+ async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt) {
68
+ await saveDeviceToken(controllerUrl, token, refreshToken, expiresAt);
69
+ }
70
+
71
+ /**
72
+ * Poll for device code token and save configuration
73
+ * @async
74
+ * @param {string} controllerUrl - Controller URL
75
+ * @param {string} deviceCode - Device code
76
+ * @param {number} interval - Polling interval
77
+ * @param {number} expiresIn - Expiration time
78
+ * @param {string} envKey - Environment key
79
+ * @returns {Promise<{token: string, environment: string}>} Token and environment
80
+ */
81
+ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, envKey) {
82
+ const spinner = ora({
83
+ text: 'Waiting for approval',
84
+ spinner: 'dots'
85
+ }).start();
86
+
87
+ let pollCount = 0;
88
+ const pollCallback = () => {
89
+ pollCount++;
90
+ spinner.text = `Waiting for approval (attempt ${pollCount})...`;
91
+ };
92
+
93
+ try {
94
+ const tokenResponse = await pollDeviceCodeToken(
95
+ controllerUrl,
96
+ deviceCode,
97
+ interval,
98
+ expiresIn,
99
+ pollCallback
100
+ );
101
+
102
+ spinner.succeed('Authentication approved!');
103
+
104
+ const token = tokenResponse.access_token;
105
+ const refreshToken = tokenResponse.refresh_token;
106
+ const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
107
+
108
+ // Save device token at root level (controller-specific, not environment-specific)
109
+ await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
110
+
111
+ // Still set current environment if provided (for other purposes)
112
+ if (envKey) {
113
+ await setCurrentEnvironment(envKey);
114
+ }
115
+
116
+ logger.log(chalk.green('\n✅ Successfully logged in!'));
117
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
118
+ if (envKey) {
119
+ logger.log(chalk.gray(`Environment: ${envKey}`));
120
+ }
121
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
122
+
123
+ return { token, environment: envKey };
124
+
125
+ } catch (pollError) {
126
+ spinner.fail('Authentication failed');
127
+ throw pollError;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Build scope string from options
133
+ * @param {boolean} [offline] - Whether to request offline_access
134
+ * @param {string} [customScope] - Custom scope string
135
+ * @returns {string} Scope string
136
+ */
137
+ function buildScope(offline, customScope) {
138
+ const defaultScope = 'openid profile email';
139
+
140
+ if (customScope) {
141
+ // If custom scope provided, use it and optionally add offline_access
142
+ if (offline && !customScope.includes('offline_access')) {
143
+ return `${customScope} offline_access`;
144
+ }
145
+ return customScope;
146
+ }
147
+
148
+ // Default scope with optional offline_access
149
+ if (offline) {
150
+ return `${defaultScope} offline_access`;
151
+ }
152
+
153
+ return defaultScope;
154
+ }
155
+
156
+ /**
157
+ * Validate device code API response
158
+ * @param {Object} deviceCodeApiResponse - API response
159
+ * @throws {Error} If response is invalid
160
+ */
161
+ function validateDeviceCodeResponse(deviceCodeApiResponse) {
162
+ if (!deviceCodeApiResponse) {
163
+ throw new Error('Device code flow initiation returned no response');
164
+ }
165
+
166
+ if (!deviceCodeApiResponse.success) {
167
+ const errorMessage = deviceCodeApiResponse.formattedError ||
168
+ deviceCodeApiResponse.error ||
169
+ 'Device code flow initiation failed';
170
+ const error = new Error(errorMessage);
171
+ if (deviceCodeApiResponse.formattedError) {
172
+ error.formattedError = deviceCodeApiResponse.formattedError;
173
+ }
174
+ throw error;
175
+ }
176
+
177
+ if (!deviceCodeApiResponse.data) {
178
+ throw new Error('Device code flow initiation returned no data');
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Convert API response to device code format
184
+ * @param {Object} apiResponse - API response data
185
+ * @returns {Object} Device code response in snake_case format
186
+ */
187
+ function convertDeviceCodeResponse(apiResponse) {
188
+ const deviceCodeData = apiResponse.data || apiResponse;
189
+ return {
190
+ device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
191
+ user_code: deviceCodeData.userCode || deviceCodeData.user_code,
192
+ verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
193
+ expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
194
+ interval: deviceCodeData.interval || 5
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Handle device code flow login
200
+ * @async
201
+ * @param {string} controllerUrl - Controller URL
202
+ * @param {string} [environment] - Environment key from options
203
+ * @param {boolean} [offline] - Whether to request offline_access scope
204
+ * @param {string} [scope] - Custom scope string
205
+ * @returns {Promise<{token: string, environment: string}>} Token and environment
206
+ */
207
+ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
208
+ const envKey = await getEnvironmentKey(environment);
209
+ const requestScope = buildScope(offline, scope);
210
+
211
+ logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
212
+ if (offline) {
213
+ logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
214
+ }
215
+
216
+ try {
217
+ // Use centralized API client for device code flow initiation
218
+ const deviceCodeApiResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
219
+
220
+ // Validate response structure
221
+ validateDeviceCodeResponse(deviceCodeApiResponse);
222
+
223
+ // Convert API response to device code format
224
+ const apiResponse = deviceCodeApiResponse.data;
225
+ const deviceCodeResponse = convertDeviceCodeResponse(apiResponse);
226
+
227
+ displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
228
+
229
+ return await pollAndSaveDeviceCodeToken(
230
+ controllerUrl,
231
+ deviceCodeResponse.device_code,
232
+ deviceCodeResponse.interval,
233
+ deviceCodeResponse.expires_in,
234
+ envKey
235
+ );
236
+
237
+ } catch (deviceError) {
238
+ // Display formatted error if available (includes detailed validation info)
239
+ if (deviceError.formattedError) {
240
+ logger.error(chalk.red('\n❌ Device code flow failed:'));
241
+ logger.log(deviceError.formattedError);
242
+ } else {
243
+ logger.error(chalk.red(`\n❌ Device code flow failed: ${deviceError.message}`));
244
+ }
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ module.exports = {
250
+ handleDeviceCodeLogin,
251
+ getEnvironmentKey,
252
+ validateEnvironmentKey
253
+ };
254
+