@aifabrix/builder 2.31.1 → 2.32.1
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.
- package/README.md +9 -9
- package/integration/hubspot/README.md +2 -2
- package/integration/hubspot/hubspot-deploy-company.json +17 -14
- package/integration/hubspot/hubspot-deploy-contact.json +19 -16
- package/integration/hubspot/hubspot-deploy-deal.json +21 -18
- package/lib/api/types/datasources.types.js +31 -5
- package/lib/api/types/wizard.types.js +142 -0
- package/lib/api/wizard.api.js +177 -0
- package/lib/{app-config.js → app/config.js} +4 -4
- package/lib/{app-deploy.js → app/deploy.js} +8 -8
- package/lib/app/display.js +90 -0
- package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
- package/lib/{app-down.js → app/down.js} +4 -4
- package/lib/app/helpers.js +218 -0
- package/lib/app/index.js +298 -0
- package/lib/{app-list.js → app/list.js} +6 -6
- package/lib/{app-push.js → app/push.js} +4 -4
- package/lib/{app-readme.js → app/readme.js} +34 -13
- package/lib/{app-register.js → app/register.js} +9 -9
- package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
- package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
- package/lib/{app-run.js → app/run.js} +6 -6
- package/lib/{build.js → build/index.js} +59 -32
- package/lib/build/package.json +7 -0
- package/lib/cli.js +245 -179
- package/lib/commands/app.js +3 -3
- package/lib/commands/datasource.js +4 -4
- package/lib/commands/login-credentials.js +209 -0
- package/lib/commands/login-device.js +254 -0
- package/lib/commands/login.js +67 -378
- package/lib/commands/logout.js +1 -1
- package/lib/commands/secrets-set.js +1 -1
- package/lib/commands/secure.js +2 -2
- package/lib/commands/wizard.js +498 -0
- package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
- package/lib/{config.js → core/config.js} +28 -26
- package/lib/{diff.js → core/diff.js} +157 -72
- package/lib/{secrets.js → core/secrets.js} +86 -49
- package/lib/{templates.js → core/templates-env.js} +14 -222
- package/lib/core/templates.js +279 -0
- package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
- package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
- package/lib/datasource/list.js +223 -0
- package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
- package/lib/{deployer.js → deployment/deployer.js} +48 -18
- package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
- package/lib/{push.js → deployment/push.js} +1 -1
- package/lib/external-system/deploy-helpers.js +145 -0
- package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
- package/lib/external-system/download-helpers.js +114 -0
- package/lib/{external-system-download.js → external-system/download.js} +92 -135
- package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
- package/lib/external-system/test-auth.js +40 -0
- package/lib/external-system/test-execution.js +84 -0
- package/lib/external-system/test-helpers.js +109 -0
- package/lib/{external-system-test.js → external-system/test.js} +174 -192
- package/lib/{generator-builders.js → generator/builders.js} +87 -10
- package/lib/{generator-external.js → generator/external.js} +115 -52
- package/lib/{github-generator.js → generator/github.js} +116 -15
- package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
- package/lib/{generator.js → generator/index.js} +49 -22
- package/lib/{generator-split.js → generator/split.js} +108 -55
- package/lib/generator/wizard-prompts.js +357 -0
- package/lib/generator/wizard.js +490 -0
- package/lib/{infra.js → infrastructure/index.js} +49 -22
- package/lib/schema/external-datasource.schema.json +145 -133
- package/lib/schema/external-system.schema.json +42 -0
- package/lib/utils/api.js +9 -5
- package/lib/utils/app-register-api.js +60 -32
- package/lib/utils/app-register-auth.js +172 -47
- package/lib/utils/app-register-config.js +130 -59
- package/lib/utils/app-run-containers.js +29 -8
- package/lib/utils/build-helpers.js +1 -1
- package/lib/utils/cli-utils.js +78 -30
- package/lib/utils/compose-generator.js +145 -65
- package/lib/utils/config-paths.js +2 -0
- package/lib/utils/deployment-errors.js +1 -1
- package/lib/utils/device-code.js +99 -41
- package/lib/utils/env-config-loader.js +1 -1
- package/lib/utils/env-copy.js +21 -18
- package/lib/utils/env-endpoints.js +115 -67
- package/lib/utils/env-map.js +13 -14
- package/lib/utils/env-ports.js +45 -25
- package/lib/utils/env-template.js +84 -42
- package/lib/utils/error-formatter.js +26 -9
- package/lib/utils/error-formatters/error-parser.js +90 -4
- package/lib/utils/error-formatters/http-status-errors.js +54 -17
- package/lib/utils/error-formatters/network-errors.js +103 -26
- package/lib/utils/external-system-display.js +184 -90
- package/lib/utils/external-system-validators.js +164 -42
- package/lib/utils/file-upload.js +109 -0
- package/lib/utils/health-check.js +199 -83
- package/lib/utils/infra-containers.js +1 -1
- package/lib/utils/infra-status.js +66 -15
- package/lib/utils/local-secrets.js +45 -25
- package/lib/utils/paths.js +45 -33
- package/lib/utils/schema-loader.js +42 -25
- package/lib/utils/schema-resolver.js +123 -74
- package/lib/utils/secrets-encryption.js +62 -25
- package/lib/utils/secrets-helpers.js +126 -63
- package/lib/utils/secrets-path.js +1 -1
- package/lib/utils/secrets-url.js +1 -1
- package/lib/utils/token-manager-refresh.js +181 -0
- package/lib/utils/token-manager.js +76 -123
- package/lib/utils/variable-transformer.js +154 -77
- package/lib/utils/yaml-preserve.js +41 -47
- package/lib/{template-validator.js → validation/template.js} +54 -23
- package/lib/{validate.js → validation/validate.js} +205 -125
- package/lib/{validator.js → validation/validator.js} +58 -39
- package/package.json +31 -2
- package/templates/external-system/deploy.ps1.hbs +34 -0
- package/templates/external-system/deploy.sh.hbs +34 -0
- package/templates/external-system/external-datasource.json.hbs +31 -12
- package/lib/app.js +0 -467
- package/lib/datasource-list.js +0 -141
- /package/lib/{app-prompts.js → app/prompts.js} +0 -0
- /package/lib/{env-reader.js → core/env-reader.js} +0 -0
- /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
|
+
|