@aifabrix/builder 2.32.1 ā 2.32.3
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/lib/api/index.js +10 -5
- package/lib/api/wizard.api.js +52 -21
- package/lib/app/list.js +62 -28
- package/lib/app/prompts.js +9 -5
- package/lib/app/rotate-secret.js +2 -1
- package/lib/cli.js +33 -4
- package/lib/commands/auth-status.js +262 -0
- package/lib/commands/login-device.js +17 -12
- package/lib/commands/login.js +16 -9
- package/lib/commands/wizard.js +63 -69
- package/lib/datasource/deploy.js +5 -2
- package/lib/external-system/deploy.js +3 -2
- package/lib/external-system/download.js +2 -1
- package/lib/external-system/test-auth.js +2 -1
- package/lib/schema/application-schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +314 -18
- package/lib/schema/external-system.schema.json +2 -2
- package/lib/utils/api.js +20 -7
- package/lib/utils/app-register-display.js +2 -1
- package/lib/utils/cli-utils.js +3 -1
- package/lib/utils/controller-url.js +67 -0
- package/lib/utils/env-map.js +2 -1
- package/lib/utils/error-formatter.js +100 -28
- package/lib/utils/token-manager.js +60 -0
- package/lib/validation/validator.js +2 -1
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +2 -2
- package/templates/external-system/external-system.json.hbs +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Auth Status Command
|
|
3
|
+
*
|
|
4
|
+
* Displays authentication status for the current controller and environment
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Authentication status command implementation
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const config = require('../core/config');
|
|
14
|
+
const { getConfig, getCurrentEnvironment } = config;
|
|
15
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
16
|
+
const { getAuthUser } = require('../api/auth.api');
|
|
17
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format expiration date for display
|
|
21
|
+
* @param {string} expiresAt - ISO 8601 expiration timestamp
|
|
22
|
+
* @returns {string} Formatted expiration string
|
|
23
|
+
*/
|
|
24
|
+
function formatExpiration(expiresAt) {
|
|
25
|
+
if (!expiresAt) {
|
|
26
|
+
return 'Unknown';
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const date = new Date(expiresAt);
|
|
30
|
+
return date.toISOString();
|
|
31
|
+
} catch {
|
|
32
|
+
return expiresAt;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check and validate device token
|
|
38
|
+
* @async
|
|
39
|
+
* @param {string} controllerUrl - Controller URL
|
|
40
|
+
* @returns {Promise<Object|null>} Token validation result or null
|
|
41
|
+
*/
|
|
42
|
+
async function checkDeviceToken(controllerUrl) {
|
|
43
|
+
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
44
|
+
if (!deviceToken || !deviceToken.token) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const authConfig = { type: 'bearer', token: deviceToken.token };
|
|
50
|
+
// Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
|
|
51
|
+
const { getAuthUser } = require('../api/auth.api');
|
|
52
|
+
const response = await getAuthUser(controllerUrl, authConfig);
|
|
53
|
+
|
|
54
|
+
if (response.success && response.data) {
|
|
55
|
+
return {
|
|
56
|
+
type: 'Device Token',
|
|
57
|
+
token: deviceToken.token,
|
|
58
|
+
authenticated: response.data.authenticated !== false,
|
|
59
|
+
user: response.data.user,
|
|
60
|
+
expiresAt: deviceToken.expiresAt
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
type: 'Device Token',
|
|
66
|
+
token: deviceToken.token,
|
|
67
|
+
authenticated: false,
|
|
68
|
+
error: response.error || response.formattedError || 'Token validation failed'
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'Device Token',
|
|
73
|
+
token: deviceToken.token,
|
|
74
|
+
authenticated: false,
|
|
75
|
+
error: error.message || 'Token validation error'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decrypt token if encrypted
|
|
82
|
+
* @async
|
|
83
|
+
* @param {string} token - Token to decrypt
|
|
84
|
+
* @returns {Promise<string>} Decrypted token
|
|
85
|
+
*/
|
|
86
|
+
async function decryptTokenIfNeeded(token) {
|
|
87
|
+
const { decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
|
|
88
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
89
|
+
|
|
90
|
+
if (encryptionKey && isTokenEncrypted(token)) {
|
|
91
|
+
return await decryptToken(token, encryptionKey);
|
|
92
|
+
}
|
|
93
|
+
return token;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate client token and return result
|
|
98
|
+
* @async
|
|
99
|
+
* @param {string} token - Token to validate
|
|
100
|
+
* @param {string} controllerUrl - Controller URL
|
|
101
|
+
* @param {string} environment - Environment key
|
|
102
|
+
* @param {string} appName - Application name
|
|
103
|
+
* @param {string} expiresAt - Token expiration
|
|
104
|
+
* @returns {Promise<Object>} Token validation result
|
|
105
|
+
*/
|
|
106
|
+
async function validateClientToken(token, controllerUrl, environment, appName, expiresAt) {
|
|
107
|
+
try {
|
|
108
|
+
const authConfig = { type: 'bearer', token: token };
|
|
109
|
+
// Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
|
|
110
|
+
const response = await getAuthUser(controllerUrl, authConfig);
|
|
111
|
+
|
|
112
|
+
if (response.success && response.data) {
|
|
113
|
+
return {
|
|
114
|
+
type: 'Client Token',
|
|
115
|
+
token: token,
|
|
116
|
+
authenticated: response.data.authenticated !== false,
|
|
117
|
+
user: response.data.user,
|
|
118
|
+
expiresAt: expiresAt,
|
|
119
|
+
appName: appName
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'Client Token',
|
|
125
|
+
token: token,
|
|
126
|
+
authenticated: false,
|
|
127
|
+
error: response.error || response.formattedError || 'Token validation failed',
|
|
128
|
+
appName: appName
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
type: 'Client Token',
|
|
133
|
+
token: '***',
|
|
134
|
+
authenticated: false,
|
|
135
|
+
error: error.message || 'Token validation error',
|
|
136
|
+
appName: appName
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check and validate client token
|
|
143
|
+
* @async
|
|
144
|
+
* @param {string} controllerUrl - Controller URL
|
|
145
|
+
* @param {string} environment - Environment key
|
|
146
|
+
* @returns {Promise<Object|null>} Token validation result or null
|
|
147
|
+
*/
|
|
148
|
+
async function checkClientToken(controllerUrl, environment) {
|
|
149
|
+
const configData = await getConfig();
|
|
150
|
+
const environments = configData.environments || {};
|
|
151
|
+
const envConfig = environments[environment];
|
|
152
|
+
|
|
153
|
+
if (!envConfig || !envConfig.clients) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [appName, tokenData] of Object.entries(envConfig.clients)) {
|
|
158
|
+
if (tokenData.controller === controllerUrl && tokenData.token) {
|
|
159
|
+
const token = await decryptTokenIfNeeded(tokenData.token);
|
|
160
|
+
return await validateClientToken(token, controllerUrl, environment, appName, tokenData.expiresAt);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Display user information
|
|
169
|
+
* @param {Object} user - User object
|
|
170
|
+
*/
|
|
171
|
+
function displayUserInfo(user) {
|
|
172
|
+
if (!user) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger.log('');
|
|
177
|
+
logger.log(chalk.bold('User Information:'));
|
|
178
|
+
if (user.email) {
|
|
179
|
+
logger.log(` Email: ${chalk.cyan(user.email)}`);
|
|
180
|
+
}
|
|
181
|
+
if (user.username) {
|
|
182
|
+
logger.log(` Username: ${chalk.cyan(user.username)}`);
|
|
183
|
+
}
|
|
184
|
+
if (user.id) {
|
|
185
|
+
logger.log(` ID: ${chalk.gray(user.id)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Display token information
|
|
191
|
+
* @param {Object} tokenInfo - Token information
|
|
192
|
+
*/
|
|
193
|
+
function displayTokenInfo(tokenInfo) {
|
|
194
|
+
const statusIcon = tokenInfo.authenticated ? chalk.green('ā') : chalk.red('ā');
|
|
195
|
+
const statusText = tokenInfo.authenticated ? 'Authenticated' : 'Not authenticated';
|
|
196
|
+
|
|
197
|
+
logger.log(`Status: ${statusIcon} ${statusText}`);
|
|
198
|
+
logger.log(`Token Type: ${chalk.cyan(tokenInfo.type)}`);
|
|
199
|
+
|
|
200
|
+
if (tokenInfo.appName) {
|
|
201
|
+
logger.log(`Application: ${chalk.cyan(tokenInfo.appName)}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (tokenInfo.expiresAt) {
|
|
205
|
+
logger.log(`Expires: ${chalk.gray(formatExpiration(tokenInfo.expiresAt))}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (tokenInfo.error) {
|
|
209
|
+
logger.log(`Error: ${chalk.red(tokenInfo.error)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
displayUserInfo(tokenInfo.user);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Display authentication status
|
|
217
|
+
* @param {string} controllerUrl - Controller URL
|
|
218
|
+
* @param {string} environment - Environment key
|
|
219
|
+
* @param {Object|null} tokenInfo - Token information
|
|
220
|
+
*/
|
|
221
|
+
function displayStatus(controllerUrl, environment, tokenInfo) {
|
|
222
|
+
logger.log(chalk.bold('\nš Authentication Status\n'));
|
|
223
|
+
logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
|
|
224
|
+
logger.log(`Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
|
|
225
|
+
|
|
226
|
+
if (!tokenInfo) {
|
|
227
|
+
logger.log(`Status: ${chalk.red('ā Not authenticated')}`);
|
|
228
|
+
logger.log(`Token Type: ${chalk.gray('None')}\n`);
|
|
229
|
+
logger.log(chalk.yellow('š” Run "aifabrix login" to authenticate\n'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
displayTokenInfo(tokenInfo);
|
|
234
|
+
logger.log('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handle auth status command
|
|
239
|
+
* @async
|
|
240
|
+
* @function handleAuthStatus
|
|
241
|
+
* @param {Object} options - Command options
|
|
242
|
+
* @param {string} [options.controller] - Controller URL (uses developer ID-based default if not provided)
|
|
243
|
+
* @param {string} [options.environment] - Environment key (uses current environment from config if not provided)
|
|
244
|
+
* @returns {Promise<void>} Resolves when status is displayed
|
|
245
|
+
*/
|
|
246
|
+
async function handleAuthStatus(options) {
|
|
247
|
+
const configData = await getConfig();
|
|
248
|
+
const controllerUrl = await resolveControllerUrl(options, configData);
|
|
249
|
+
const environment = options.environment || await getCurrentEnvironment() || 'dev';
|
|
250
|
+
|
|
251
|
+
// Check device token first (preferred)
|
|
252
|
+
let tokenInfo = await checkDeviceToken(controllerUrl);
|
|
253
|
+
|
|
254
|
+
// If no device token, check client token
|
|
255
|
+
if (!tokenInfo) {
|
|
256
|
+
tokenInfo = await checkClientToken(controllerUrl, environment);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
displayStatus(controllerUrl, environment, tokenInfo);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { handleAuthStatus };
|
|
@@ -130,27 +130,32 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Build scope string from options
|
|
133
|
-
* @param {boolean} [
|
|
133
|
+
* @param {boolean} [online] - Whether to exclude offline_access (default: false, meaning offline tokens are default)
|
|
134
134
|
* @param {string} [customScope] - Custom scope string
|
|
135
135
|
* @returns {string} Scope string
|
|
136
136
|
*/
|
|
137
|
-
function buildScope(
|
|
137
|
+
function buildScope(online, customScope) {
|
|
138
138
|
const defaultScope = 'openid profile email';
|
|
139
139
|
|
|
140
140
|
if (customScope) {
|
|
141
|
-
// If custom scope provided, use it
|
|
142
|
-
|
|
141
|
+
// If custom scope provided, use it as-is
|
|
142
|
+
// If --online flag is used and scope contains offline_access, remove it
|
|
143
|
+
if (online && customScope.includes('offline_access')) {
|
|
144
|
+
return customScope.replace(/\s*offline_access\s*/g, ' ').trim().replace(/\s+/g, ' ');
|
|
145
|
+
}
|
|
146
|
+
// If not --online and scope doesn't have offline_access, add it (default behavior)
|
|
147
|
+
if (!online && !customScope.includes('offline_access')) {
|
|
143
148
|
return `${customScope} offline_access`;
|
|
144
149
|
}
|
|
145
150
|
return customScope;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
|
-
// Default scope
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
153
|
+
// Default scope: include offline_access unless --online is specified
|
|
154
|
+
if (online) {
|
|
155
|
+
return defaultScope;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
return defaultScope
|
|
158
|
+
return `${defaultScope} offline_access`;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
/**
|
|
@@ -200,16 +205,16 @@ function convertDeviceCodeResponse(apiResponse) {
|
|
|
200
205
|
* @async
|
|
201
206
|
* @param {string} controllerUrl - Controller URL
|
|
202
207
|
* @param {string} [environment] - Environment key from options
|
|
203
|
-
* @param {boolean} [
|
|
208
|
+
* @param {boolean} [online] - Whether to exclude offline_access scope (default: false, meaning offline tokens are default)
|
|
204
209
|
* @param {string} [scope] - Custom scope string
|
|
205
210
|
* @returns {Promise<{token: string, environment: string}>} Token and environment
|
|
206
211
|
*/
|
|
207
|
-
async function handleDeviceCodeLogin(controllerUrl, environment,
|
|
212
|
+
async function handleDeviceCodeLogin(controllerUrl, environment, online, scope) {
|
|
208
213
|
const envKey = await getEnvironmentKey(environment);
|
|
209
|
-
const requestScope = buildScope(
|
|
214
|
+
const requestScope = buildScope(online, scope);
|
|
210
215
|
|
|
211
216
|
logger.log(chalk.blue('\nš± Initiating device code flow...\n'));
|
|
212
|
-
if (
|
|
217
|
+
if (!online && requestScope.includes('offline_access')) {
|
|
213
218
|
logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
|
|
214
219
|
}
|
|
215
220
|
|
package/lib/commands/login.js
CHANGED
|
@@ -15,6 +15,7 @@ const { setCurrentEnvironment, saveClientToken } = require('../core/config');
|
|
|
15
15
|
const logger = require('../utils/logger');
|
|
16
16
|
const { handleCredentialsLogin } = require('./login-credentials');
|
|
17
17
|
const { handleDeviceCodeLogin } = require('./login-device');
|
|
18
|
+
const { getDefaultControllerUrl } = require('../utils/controller-url');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Determine and validate authentication method
|
|
@@ -61,8 +62,8 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
|
|
|
61
62
|
* @async
|
|
62
63
|
* @function handleLogin
|
|
63
64
|
* @param {Object} options - Login options
|
|
64
|
-
* @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
|
|
65
|
-
* @param {string} [options.method] - Authentication method ('device' or 'credentials')
|
|
65
|
+
* @param {string} [options.controller] - Controller URL (default: calculated based on developer ID, e.g., 'http://localhost:3000' for dev ID 0, 'http://localhost:3100' for dev ID 1)
|
|
66
|
+
* @param {string} [options.method] - Authentication method ('device' or 'credentials', default: 'device')
|
|
66
67
|
* @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
|
|
67
68
|
* @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
|
|
68
69
|
* @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
|
|
@@ -72,12 +73,18 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
|
|
|
72
73
|
*/
|
|
73
74
|
/**
|
|
74
75
|
* Normalizes and logs controller URL
|
|
76
|
+
* Calculates default URL based on developer ID if not provided
|
|
77
|
+
* @async
|
|
75
78
|
* @function normalizeControllerUrl
|
|
76
79
|
* @param {Object} options - Login options
|
|
77
|
-
* @returns {string} Normalized controller URL
|
|
80
|
+
* @returns {Promise<string>} Normalized controller URL
|
|
78
81
|
*/
|
|
79
|
-
function normalizeControllerUrl(options) {
|
|
80
|
-
|
|
82
|
+
async function normalizeControllerUrl(options) {
|
|
83
|
+
let controllerUrl = options.controller || options.url;
|
|
84
|
+
if (!controllerUrl) {
|
|
85
|
+
controllerUrl = await getDefaultControllerUrl();
|
|
86
|
+
}
|
|
87
|
+
controllerUrl = controllerUrl.replace(/\/$/, '');
|
|
81
88
|
logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
|
|
82
89
|
return controllerUrl;
|
|
83
90
|
}
|
|
@@ -108,8 +115,8 @@ async function handleEnvironmentConfig(options) {
|
|
|
108
115
|
* @param {Object} options - Login options
|
|
109
116
|
*/
|
|
110
117
|
function validateScopeOptions(method, options) {
|
|
111
|
-
if (method === 'credentials' && (options.
|
|
112
|
-
logger.log(chalk.yellow('ā ļø Warning: --
|
|
118
|
+
if (method === 'credentials' && (options.online || options.scope)) {
|
|
119
|
+
logger.log(chalk.yellow('ā ļø Warning: --online and --scope options are only available for device flow'));
|
|
113
120
|
logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -141,13 +148,13 @@ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
|
|
|
141
148
|
* @returns {Promise<{token: string, environment: string}>} Login result
|
|
142
149
|
*/
|
|
143
150
|
async function handleDeviceCodeLoginFlow(controllerUrl, options) {
|
|
144
|
-
return await handleDeviceCodeLogin(controllerUrl, options.environment, options.
|
|
151
|
+
return await handleDeviceCodeLogin(controllerUrl, options.environment, options.online, options.scope);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
async function handleLogin(options) {
|
|
148
155
|
logger.log(chalk.blue('\nš Logging in to Miso Controller...\n'));
|
|
149
156
|
|
|
150
|
-
const controllerUrl = normalizeControllerUrl(options);
|
|
157
|
+
const controllerUrl = await normalizeControllerUrl(options);
|
|
151
158
|
const environment = await handleEnvironmentConfig(options);
|
|
152
159
|
const method = await determineAuthMethod(options.method);
|
|
153
160
|
|
package/lib/commands/wizard.js
CHANGED
|
@@ -10,11 +10,12 @@ const path = require('path');
|
|
|
10
10
|
const fs = require('fs').promises;
|
|
11
11
|
const logger = require('../utils/logger');
|
|
12
12
|
const config = require('../core/config');
|
|
13
|
-
const {
|
|
13
|
+
const { getDeviceOnlyAuth } = require('../utils/token-manager');
|
|
14
14
|
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
15
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
15
16
|
const {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
createWizardSession,
|
|
18
|
+
updateWizardSession,
|
|
18
19
|
parseOpenApi,
|
|
19
20
|
detectType,
|
|
20
21
|
generateConfig,
|
|
@@ -44,29 +45,23 @@ const { generateWizardFiles } = require('../generator/wizard');
|
|
|
44
45
|
* @throws {Error} If validation fails
|
|
45
46
|
*/
|
|
46
47
|
async function validateAndCheckAppDirectory(appName) {
|
|
47
|
-
// Validate app name
|
|
48
48
|
if (!/^[a-z0-9-_]+$/.test(appName)) {
|
|
49
49
|
throw new Error('Application name must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
// Check if app directory already exists
|
|
53
51
|
const appPath = path.join(process.cwd(), 'integration', appName);
|
|
54
52
|
try {
|
|
55
53
|
await fs.access(appPath);
|
|
56
|
-
const { overwrite } = await require('inquirer').prompt([
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
]);
|
|
54
|
+
const { overwrite } = await require('inquirer').prompt([{
|
|
55
|
+
type: 'confirm',
|
|
56
|
+
name: 'overwrite',
|
|
57
|
+
message: `Directory ${appPath} already exists. Overwrite?`,
|
|
58
|
+
default: false
|
|
59
|
+
}]);
|
|
64
60
|
if (!overwrite) {
|
|
65
61
|
logger.log(chalk.yellow('Wizard cancelled.'));
|
|
66
62
|
return false;
|
|
67
63
|
}
|
|
68
64
|
} catch (error) {
|
|
69
|
-
// Directory doesn't exist, continue
|
|
70
65
|
if (error.code !== 'ENOENT') {
|
|
71
66
|
throw error;
|
|
72
67
|
}
|
|
@@ -75,37 +70,48 @@ async function validateAndCheckAppDirectory(appName) {
|
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
/**
|
|
78
|
-
* Handle mode selection step
|
|
73
|
+
* Handle mode selection step - create wizard session
|
|
79
74
|
* @async
|
|
80
75
|
* @function handleModeSelection
|
|
81
76
|
* @param {string} dataplaneUrl - Dataplane URL
|
|
82
77
|
* @param {Object} authConfig - Authentication configuration
|
|
83
|
-
* @returns {Promise<
|
|
78
|
+
* @returns {Promise<Object>} Object with mode and sessionId
|
|
84
79
|
* @throws {Error} If mode selection fails
|
|
85
80
|
*/
|
|
86
81
|
async function handleModeSelection(dataplaneUrl, authConfig) {
|
|
87
82
|
logger.log(chalk.blue('\nš Step 1: Mode Selection'));
|
|
88
83
|
const mode = await promptForMode();
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
|
|
84
|
+
const sessionResponse = await createWizardSession(dataplaneUrl, authConfig, mode);
|
|
85
|
+
if (!sessionResponse.success || !sessionResponse.data) {
|
|
86
|
+
const errorMsg = sessionResponse.formattedError ||
|
|
87
|
+
sessionResponse.error ||
|
|
88
|
+
sessionResponse.errorData?.detail ||
|
|
89
|
+
sessionResponse.message ||
|
|
90
|
+
(sessionResponse.status ? `HTTP ${sessionResponse.status}` : 'Unknown error');
|
|
91
|
+
throw new Error(`Failed to create wizard session: ${errorMsg}`);
|
|
92
|
+
}
|
|
93
|
+
const sessionId = sessionResponse.data.data?.sessionId || sessionResponse.data.sessionId;
|
|
94
|
+
if (!sessionId) {
|
|
95
|
+
throw new Error('Session ID not found in response');
|
|
92
96
|
}
|
|
93
|
-
return mode;
|
|
97
|
+
return { mode, sessionId };
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/**
|
|
97
|
-
* Handle source selection step
|
|
101
|
+
* Handle source selection step - update wizard session
|
|
98
102
|
* @async
|
|
99
103
|
* @function handleSourceSelection
|
|
100
104
|
* @param {string} dataplaneUrl - Dataplane URL
|
|
105
|
+
* @param {string} sessionId - Wizard session ID
|
|
101
106
|
* @param {Object} authConfig - Authentication configuration
|
|
102
107
|
* @returns {Promise<Object>} Object with sourceType and sourceData
|
|
103
108
|
* @throws {Error} If source selection fails
|
|
104
109
|
*/
|
|
105
|
-
async function handleSourceSelection(dataplaneUrl, authConfig) {
|
|
110
|
+
async function handleSourceSelection(dataplaneUrl, sessionId, authConfig) {
|
|
106
111
|
logger.log(chalk.blue('\nš Step 2: Source Selection'));
|
|
107
112
|
const sourceType = await promptForSourceType();
|
|
108
113
|
let sourceData = null;
|
|
114
|
+
const updateData = { currentStep: 1 };
|
|
109
115
|
|
|
110
116
|
if (sourceType === 'openapi-file') {
|
|
111
117
|
const filePath = await promptForOpenApiFile();
|
|
@@ -113,17 +119,19 @@ async function handleSourceSelection(dataplaneUrl, authConfig) {
|
|
|
113
119
|
} else if (sourceType === 'openapi-url') {
|
|
114
120
|
const url = await promptForOpenApiUrl();
|
|
115
121
|
sourceData = url;
|
|
122
|
+
updateData.openapiSpec = null; // Will be set after parsing
|
|
116
123
|
} else if (sourceType === 'mcp-server') {
|
|
117
124
|
const mcpDetails = await promptForMcpServer();
|
|
118
125
|
sourceData = JSON.stringify(mcpDetails);
|
|
126
|
+
updateData.mcpServerUrl = mcpDetails.url || null;
|
|
119
127
|
} else if (sourceType === 'known-platform') {
|
|
120
128
|
const platform = await promptForKnownPlatform();
|
|
121
129
|
sourceData = platform;
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
throw new Error(`Source selection failed: ${
|
|
132
|
+
const updateResponse = await updateWizardSession(dataplaneUrl, sessionId, authConfig, updateData);
|
|
133
|
+
if (!updateResponse.success) {
|
|
134
|
+
throw new Error(`Source selection failed: ${updateResponse.error || updateResponse.formattedError}`);
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
return { sourceType, sourceData };
|
|
@@ -379,11 +387,11 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
379
387
|
* @throws {Error} If wizard flow fails
|
|
380
388
|
*/
|
|
381
389
|
async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
|
|
382
|
-
// Step 1: Mode Selection
|
|
383
|
-
const mode = await handleModeSelection(dataplaneUrl, authConfig);
|
|
390
|
+
// Step 1: Mode Selection - Create wizard session
|
|
391
|
+
const { mode, sessionId } = await handleModeSelection(dataplaneUrl, authConfig);
|
|
384
392
|
|
|
385
|
-
// Step 2: Source Selection
|
|
386
|
-
const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, authConfig);
|
|
393
|
+
// Step 2: Source Selection - Update session
|
|
394
|
+
const { sourceType, sourceData } = await handleSourceSelection(dataplaneUrl, sessionId, authConfig);
|
|
387
395
|
|
|
388
396
|
// Step 3: Parse OpenAPI (if applicable)
|
|
389
397
|
const openApiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
|
|
@@ -430,30 +438,25 @@ async function executeWizardFlow(appName, dataplaneUrl, authConfig) {
|
|
|
430
438
|
* @throws {Error} If wizard fails
|
|
431
439
|
*/
|
|
432
440
|
async function handleWizard(options = {}) {
|
|
433
|
-
|
|
434
|
-
logger.log(chalk.blue('\nš§ AI Fabrix External System Wizard\n'));
|
|
441
|
+
logger.log(chalk.blue('\nš§ AI Fabrix External System Wizard\n'));
|
|
435
442
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
443
|
+
// Get or prompt for app name
|
|
444
|
+
let appName = options.app;
|
|
445
|
+
if (!appName) {
|
|
446
|
+
appName = await promptForAppName();
|
|
447
|
+
}
|
|
441
448
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
449
|
+
// Validate app name and check directory
|
|
450
|
+
const shouldContinue = await validateAndCheckAppDirectory(appName);
|
|
451
|
+
if (!shouldContinue) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
447
454
|
|
|
448
|
-
|
|
449
|
-
|
|
455
|
+
// Get dataplane URL and authentication
|
|
456
|
+
const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, appName);
|
|
450
457
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
} catch (error) {
|
|
454
|
-
logger.error(chalk.red(`\nā Wizard failed: ${error.message}`));
|
|
455
|
-
throw error;
|
|
456
|
-
}
|
|
458
|
+
// Execute wizard flow
|
|
459
|
+
await executeWizardFlow(appName, dataplaneUrl, authConfig);
|
|
457
460
|
}
|
|
458
461
|
|
|
459
462
|
/**
|
|
@@ -468,29 +471,20 @@ async function handleWizard(options = {}) {
|
|
|
468
471
|
async function setupDataplaneAndAuth(options, appName) {
|
|
469
472
|
const configData = await config.getConfig();
|
|
470
473
|
const environment = options.environment || 'dev';
|
|
471
|
-
const controllerUrl = options
|
|
474
|
+
const controllerUrl = await resolveControllerUrl(options, configData);
|
|
475
|
+
// Wizard requires device token authentication (user-level), not client credentials
|
|
476
|
+
const authConfig = await getDeviceOnlyAuth(controllerUrl);
|
|
472
477
|
|
|
473
|
-
// Get dataplane URL (either from option or from controller)
|
|
474
478
|
let dataplaneUrl = options.dataplane;
|
|
475
479
|
if (!dataplaneUrl) {
|
|
476
|
-
// Get authentication first
|
|
477
|
-
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
478
|
-
if (!authConfig.token && !authConfig.clientId) {
|
|
479
|
-
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Get dataplane URL from controller
|
|
483
480
|
logger.log(chalk.blue('š Getting dataplane URL from controller...'));
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
492
|
-
if (!authConfig.token && !authConfig.clientId) {
|
|
493
|
-
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
481
|
+
try {
|
|
482
|
+
dataplaneUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
|
|
483
|
+
logger.log(chalk.green(`ā Dataplane URL: ${dataplaneUrl}`));
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const example = `aifabrix wizard -a ${appName} --dataplane https://dataplane.example.com -e ${environment} -c ${controllerUrl}`;
|
|
486
|
+
throw new Error(`${error.message}\n\nš” For new applications, provide the dataplane URL using:\n --dataplane <dataplane-url>\n\n Example: ${example}`);
|
|
487
|
+
}
|
|
494
488
|
}
|
|
495
489
|
|
|
496
490
|
return { dataplaneUrl, authConfig };
|
package/lib/datasource/deploy.js
CHANGED
|
@@ -40,9 +40,12 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Extract dataplane URL from application response
|
|
43
|
-
//
|
|
43
|
+
// Try multiple possible locations for the URL
|
|
44
44
|
const application = response.data.data || response.data;
|
|
45
|
-
const dataplaneUrl = application.
|
|
45
|
+
const dataplaneUrl = application.url ||
|
|
46
|
+
application.dataplaneUrl ||
|
|
47
|
+
application.dataplane?.url ||
|
|
48
|
+
application.configuration?.dataplaneUrl;
|
|
46
49
|
|
|
47
50
|
if (!dataplaneUrl) {
|
|
48
51
|
logger.error(chalk.red('ā Dataplane URL not found in application response'));
|
|
@@ -25,6 +25,7 @@ const logger = require('../utils/logger');
|
|
|
25
25
|
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
26
26
|
const { detectAppType } = require('../utils/paths');
|
|
27
27
|
const { generateExternalSystemApplicationSchema } = require('../generator/external');
|
|
28
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
28
29
|
const {
|
|
29
30
|
loadVariablesYaml,
|
|
30
31
|
validateSystemFiles,
|
|
@@ -109,7 +110,7 @@ async function prepareDeploymentConfig(appName, options) {
|
|
|
109
110
|
|
|
110
111
|
const config = await getConfig();
|
|
111
112
|
const environment = options.environment || 'dev';
|
|
112
|
-
const controllerUrl = options
|
|
113
|
+
const controllerUrl = await resolveControllerUrl(options, config);
|
|
113
114
|
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
114
115
|
|
|
115
116
|
if (!authConfig.token && !authConfig.clientId) {
|
|
@@ -245,7 +246,7 @@ async function prepareDeploymentFiles(appName, options) {
|
|
|
245
246
|
|
|
246
247
|
const config = await getConfig();
|
|
247
248
|
const environment = options.environment || 'dev';
|
|
248
|
-
const controllerUrl = options
|
|
249
|
+
const controllerUrl = await resolveControllerUrl(options, config);
|
|
249
250
|
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
250
251
|
|
|
251
252
|
if (!authConfig.token && !authConfig.clientId) {
|