@aifabrix/builder 2.1.7 ā 2.2.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.
- package/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +17 -0
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
package/lib/commands/app.js
CHANGED
|
@@ -9,246 +9,11 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const fs = require('fs').promises;
|
|
13
|
-
const path = require('path');
|
|
14
12
|
const chalk = require('chalk');
|
|
15
|
-
const yaml = require('js-yaml');
|
|
16
|
-
const { getConfig } = require('../config');
|
|
17
|
-
const { authenticatedApiCall } = require('../utils/api');
|
|
18
13
|
const logger = require('../utils/logger');
|
|
19
|
-
const {
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
// Import createApp to auto-generate config if missing
|
|
23
|
-
let createApp;
|
|
24
|
-
try {
|
|
25
|
-
createApp = require('../app').createApp;
|
|
26
|
-
} catch {
|
|
27
|
-
createApp = null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Validation schema for application registration
|
|
32
|
-
*/
|
|
33
|
-
const registerApplicationSchema = {
|
|
34
|
-
environmentId: (val) => {
|
|
35
|
-
if (!val || val.length < 1) {
|
|
36
|
-
throw new Error('Invalid environment ID format');
|
|
37
|
-
}
|
|
38
|
-
return val;
|
|
39
|
-
},
|
|
40
|
-
key: (val) => {
|
|
41
|
-
if (!val || val.length < 1) {
|
|
42
|
-
throw new Error('Application key is required');
|
|
43
|
-
}
|
|
44
|
-
if (val.length > 50) {
|
|
45
|
-
throw new Error('Application key must be at most 50 characters');
|
|
46
|
-
}
|
|
47
|
-
if (!/^[a-z0-9-]+$/.test(val)) {
|
|
48
|
-
throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
|
|
49
|
-
}
|
|
50
|
-
return val;
|
|
51
|
-
},
|
|
52
|
-
displayName: (val) => {
|
|
53
|
-
if (!val || val.length < 1) {
|
|
54
|
-
throw new Error('Display name is required');
|
|
55
|
-
}
|
|
56
|
-
if (val.length > 100) {
|
|
57
|
-
throw new Error('Display name must be at most 100 characters');
|
|
58
|
-
}
|
|
59
|
-
return val;
|
|
60
|
-
},
|
|
61
|
-
description: (val) => val || undefined,
|
|
62
|
-
configuration: (val) => {
|
|
63
|
-
const validTypes = ['webapp', 'api', 'service', 'functionapp'];
|
|
64
|
-
const validRegistryModes = ['acr', 'external', 'public'];
|
|
65
|
-
|
|
66
|
-
if (!val || !val.type || !validTypes.includes(val.type)) {
|
|
67
|
-
throw new Error('Configuration type must be one of: webapp, api, service, functionapp');
|
|
68
|
-
}
|
|
69
|
-
if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
|
|
70
|
-
throw new Error('Registry mode must be one of: acr, external, public');
|
|
71
|
-
}
|
|
72
|
-
if (val.port !== undefined) {
|
|
73
|
-
if (!Number.isInteger(val.port) || val.port < 1 || val.port > 65535) {
|
|
74
|
-
throw new Error('Port must be an integer between 1 and 65535');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return val;
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Load variables.yaml file for an application
|
|
83
|
-
* @async
|
|
84
|
-
* @param {string} appKey - Application key
|
|
85
|
-
* @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
|
|
86
|
-
*/
|
|
87
|
-
async function loadVariablesYaml(appKey) {
|
|
88
|
-
const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const variablesContent = await fs.readFile(variablesPath, 'utf-8');
|
|
92
|
-
return { variables: yaml.load(variablesContent), created: false };
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (error.code === 'ENOENT') {
|
|
95
|
-
logger.log(chalk.yellow(`ā ļø variables.yaml not found for ${appKey}`));
|
|
96
|
-
logger.log(chalk.yellow('š Creating minimal configuration...\n'));
|
|
97
|
-
return { variables: null, created: true };
|
|
98
|
-
}
|
|
99
|
-
throw new Error(`Failed to read variables.yaml: ${error.message}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Create minimal application configuration if needed
|
|
105
|
-
* @async
|
|
106
|
-
* @param {string} appKey - Application key
|
|
107
|
-
* @param {Object} options - Registration options
|
|
108
|
-
* @returns {Promise<Object>} Variables after creation
|
|
109
|
-
*/
|
|
110
|
-
async function createMinimalAppIfNeeded(appKey, options) {
|
|
111
|
-
if (!createApp) {
|
|
112
|
-
throw new Error('Cannot auto-create application: createApp function not available');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
await createApp(appKey, {
|
|
116
|
-
port: options.port,
|
|
117
|
-
language: 'typescript',
|
|
118
|
-
database: false,
|
|
119
|
-
redis: false,
|
|
120
|
-
storage: false,
|
|
121
|
-
authentication: false
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
|
|
125
|
-
const variablesContent = await fs.readFile(variablesPath, 'utf-8');
|
|
126
|
-
return yaml.load(variablesContent);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extract application configuration from variables.yaml
|
|
131
|
-
* @param {Object} variables - Variables from YAML file
|
|
132
|
-
* @param {string} appKey - Application key
|
|
133
|
-
* @param {Object} options - Registration options
|
|
134
|
-
* @returns {Object} Extracted configuration
|
|
135
|
-
*/
|
|
136
|
-
function extractAppConfiguration(variables, appKey, options) {
|
|
137
|
-
const appKeyFromFile = variables.app?.key || appKey;
|
|
138
|
-
const displayName = variables.app?.name || options.name || appKey;
|
|
139
|
-
const description = variables.app?.description || '';
|
|
140
|
-
const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
|
|
141
|
-
const registryMode = 'external';
|
|
142
|
-
const port = variables.build?.port || options.port || 3000;
|
|
143
|
-
const language = variables.build?.language || 'typescript';
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
appKey: appKeyFromFile,
|
|
147
|
-
displayName,
|
|
148
|
-
description,
|
|
149
|
-
appType,
|
|
150
|
-
registryMode,
|
|
151
|
-
port,
|
|
152
|
-
language
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Validate application registration data
|
|
158
|
-
* @param {Object} config - Application configuration
|
|
159
|
-
* @param {string} originalAppKey - Original app key for error messages
|
|
160
|
-
* @throws {Error} If validation fails
|
|
161
|
-
*/
|
|
162
|
-
function validateAppRegistrationData(config, originalAppKey) {
|
|
163
|
-
const missingFields = [];
|
|
164
|
-
if (!config.appKey) missingFields.push('app.key');
|
|
165
|
-
if (!config.displayName) missingFields.push('app.name');
|
|
166
|
-
|
|
167
|
-
if (missingFields.length > 0) {
|
|
168
|
-
logger.error(chalk.red('ā Missing required fields in variables.yaml:'));
|
|
169
|
-
missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
|
|
170
|
-
logger.error(chalk.red(`\n Please update builder/${originalAppKey}/variables.yaml and try again.`));
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
registerApplicationSchema.key(config.appKey);
|
|
176
|
-
registerApplicationSchema.displayName(config.displayName);
|
|
177
|
-
registerApplicationSchema.configuration({
|
|
178
|
-
type: config.appType,
|
|
179
|
-
registryMode: config.registryMode,
|
|
180
|
-
port: config.port
|
|
181
|
-
});
|
|
182
|
-
} catch (error) {
|
|
183
|
-
logger.error(chalk.red(`ā Invalid configuration: ${error.message}`));
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Check if user is authenticated
|
|
190
|
-
* @async
|
|
191
|
-
* @returns {Promise<Object>} Configuration with API URL and token
|
|
192
|
-
*/
|
|
193
|
-
async function checkAuthentication() {
|
|
194
|
-
const config = await getConfig();
|
|
195
|
-
if (!config.apiUrl || !config.token) {
|
|
196
|
-
logger.error(chalk.red('ā Not logged in. Run: aifabrix login'));
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
return config;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Call registration API
|
|
204
|
-
* @async
|
|
205
|
-
* @param {string} apiUrl - API URL
|
|
206
|
-
* @param {string} token - Authentication token
|
|
207
|
-
* @param {string} environment - Environment ID
|
|
208
|
-
* @param {Object} registrationData - Registration data
|
|
209
|
-
* @returns {Promise<Object>} API response
|
|
210
|
-
*/
|
|
211
|
-
async function registerApplication(apiUrl, token, environment, registrationData) {
|
|
212
|
-
const response = await authenticatedApiCall(
|
|
213
|
-
`${apiUrl}/api/v1/environments/${encodeURIComponent(environment)}/applications/register`,
|
|
214
|
-
{
|
|
215
|
-
method: 'POST',
|
|
216
|
-
body: JSON.stringify(registrationData)
|
|
217
|
-
},
|
|
218
|
-
token
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
if (!response.success) {
|
|
222
|
-
logger.error(chalk.red(`ā Registration failed: ${response.error}`));
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return response.data;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Display registration success and credentials
|
|
231
|
-
* @param {Object} data - Registration response data
|
|
232
|
-
* @param {string} apiUrl - API URL
|
|
233
|
-
*/
|
|
234
|
-
function displayRegistrationResults(data, apiUrl) {
|
|
235
|
-
logger.log(chalk.green('ā
Application registered successfully!\n'));
|
|
236
|
-
logger.log(chalk.bold('š Application Details:'));
|
|
237
|
-
logger.log(` ID: ${data.application.id}`);
|
|
238
|
-
logger.log(` Key: ${data.application.key}`);
|
|
239
|
-
logger.log(` Display Name: ${data.application.displayName}\n`);
|
|
240
|
-
|
|
241
|
-
logger.log(chalk.bold.yellow('š CREDENTIALS (save these immediately):'));
|
|
242
|
-
logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
|
|
243
|
-
logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
|
|
244
|
-
|
|
245
|
-
logger.log(chalk.red('ā ļø IMPORTANT: Client Secret will not be shown again!\n'));
|
|
246
|
-
|
|
247
|
-
logger.log(chalk.bold('š Add to GitHub Secrets:'));
|
|
248
|
-
logger.log(chalk.cyan(` AIFABRIX_CLIENT_ID = ${data.credentials.clientId}`));
|
|
249
|
-
logger.log(chalk.cyan(` AIFABRIX_CLIENT_SECRET = ${data.credentials.clientSecret}`));
|
|
250
|
-
logger.log(chalk.cyan(` AIFABRIX_API_URL = ${apiUrl}\n`));
|
|
251
|
-
}
|
|
14
|
+
const { listApplications } = require('../app-list');
|
|
15
|
+
const { registerApplication } = require('../app-register');
|
|
16
|
+
const { rotateSecret } = require('../app-rotate-secret');
|
|
252
17
|
|
|
253
18
|
/**
|
|
254
19
|
* Setup application management commands
|
|
@@ -269,74 +34,7 @@ function setupAppCommands(program) {
|
|
|
269
34
|
.option('-d, --description <desc>', 'Override description')
|
|
270
35
|
.action(async(appKey, options) => {
|
|
271
36
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Load variables.yaml
|
|
275
|
-
const { variables, created } = await loadVariablesYaml(appKey);
|
|
276
|
-
let finalVariables = variables;
|
|
277
|
-
|
|
278
|
-
// Create minimal app if needed
|
|
279
|
-
if (created) {
|
|
280
|
-
finalVariables = await createMinimalAppIfNeeded(appKey, options);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Extract configuration
|
|
284
|
-
const appConfig = extractAppConfiguration(finalVariables, appKey, options);
|
|
285
|
-
|
|
286
|
-
// Validate configuration (pass original appKey for error messages)
|
|
287
|
-
validateAppRegistrationData(appConfig, appKey);
|
|
288
|
-
|
|
289
|
-
// Check authentication
|
|
290
|
-
const config = await checkAuthentication();
|
|
291
|
-
|
|
292
|
-
// Validate environment
|
|
293
|
-
const environment = registerApplicationSchema.environmentId(options.environment);
|
|
294
|
-
|
|
295
|
-
// Prepare registration data
|
|
296
|
-
const registrationData = {
|
|
297
|
-
environmentId: environment,
|
|
298
|
-
key: appConfig.appKey,
|
|
299
|
-
displayName: appConfig.displayName,
|
|
300
|
-
description: appConfig.description || options.description,
|
|
301
|
-
configuration: {
|
|
302
|
-
type: appConfig.appType,
|
|
303
|
-
registryMode: appConfig.registryMode,
|
|
304
|
-
port: appConfig.port,
|
|
305
|
-
language: appConfig.language
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
// Register application
|
|
310
|
-
const responseData = await registerApplication(
|
|
311
|
-
config.apiUrl,
|
|
312
|
-
config.token,
|
|
313
|
-
environment,
|
|
314
|
-
registrationData
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
// Save credentials to local secrets if localhost
|
|
318
|
-
if (isLocalhost(config.apiUrl)) {
|
|
319
|
-
const appKey = responseData.application.key;
|
|
320
|
-
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
321
|
-
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
|
|
325
|
-
await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
|
|
326
|
-
|
|
327
|
-
// Update env.template
|
|
328
|
-
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey);
|
|
329
|
-
|
|
330
|
-
logger.log(chalk.green('\nā Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
331
|
-
logger.log(chalk.green('ā env.template updated with MISO_CLIENTID and MISO_CLIENTSECRET\n'));
|
|
332
|
-
} catch (error) {
|
|
333
|
-
logger.warn(chalk.yellow(`ā ļø Could not save credentials locally: ${error.message}`));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Display results
|
|
338
|
-
displayRegistrationResults(responseData, config.apiUrl);
|
|
339
|
-
|
|
37
|
+
await registerApplication(appKey, options);
|
|
340
38
|
} catch (error) {
|
|
341
39
|
logger.error(chalk.red('ā Registration failed:'), error.message);
|
|
342
40
|
process.exit(1);
|
|
@@ -350,30 +48,7 @@ function setupAppCommands(program) {
|
|
|
350
48
|
.requiredOption('-e, --environment <env>', 'Environment ID or key')
|
|
351
49
|
.action(async(options) => {
|
|
352
50
|
try {
|
|
353
|
-
|
|
354
|
-
if (!config.apiUrl || !config.token) {
|
|
355
|
-
logger.error(chalk.red('ā Not logged in. Run: aifabrix login'));
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const response = await authenticatedApiCall(
|
|
360
|
-
`${config.apiUrl}/api/v1/applications?environmentId=${options.environment}`,
|
|
361
|
-
{},
|
|
362
|
-
config.token
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
if (!response.success || !response.data) {
|
|
366
|
-
logger.error(chalk.red('ā Failed to fetch applications'));
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
logger.log(chalk.bold('\nš± Applications:\n'));
|
|
371
|
-
response.data.forEach((app) => {
|
|
372
|
-
const hasPipeline = app.configuration?.pipeline?.isActive ? 'ā' : 'ā';
|
|
373
|
-
logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status})`);
|
|
374
|
-
});
|
|
375
|
-
logger.log('');
|
|
376
|
-
|
|
51
|
+
await listApplications(options);
|
|
377
52
|
} catch (error) {
|
|
378
53
|
logger.error(chalk.red('ā Failed to list applications:'), error.message);
|
|
379
54
|
process.exit(1);
|
|
@@ -382,70 +57,12 @@ function setupAppCommands(program) {
|
|
|
382
57
|
|
|
383
58
|
// Rotate secret command
|
|
384
59
|
app
|
|
385
|
-
.command('rotate-secret')
|
|
60
|
+
.command('rotate-secret <appKey>')
|
|
386
61
|
.description('Rotate pipeline ClientSecret for an application')
|
|
387
|
-
.requiredOption('-a, --app <appKey>', 'Application key')
|
|
388
62
|
.requiredOption('-e, --environment <env>', 'Environment ID or key')
|
|
389
|
-
.action(async(options) => {
|
|
63
|
+
.action(async(appKey, options) => {
|
|
390
64
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const config = await getConfig();
|
|
394
|
-
if (!config.apiUrl || !config.token) {
|
|
395
|
-
logger.error(chalk.red('ā Not logged in. Run: aifabrix login'));
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Validate environment
|
|
400
|
-
if (!options.environment || options.environment.length < 1) {
|
|
401
|
-
logger.error(chalk.red('ā Environment is required'));
|
|
402
|
-
process.exit(1);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const response = await authenticatedApiCall(
|
|
406
|
-
`${config.apiUrl}/api/v1/applications/${options.app}/rotate-secret?environmentId=${options.environment}`,
|
|
407
|
-
{
|
|
408
|
-
method: 'POST'
|
|
409
|
-
},
|
|
410
|
-
config.token
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
if (!response.success) {
|
|
414
|
-
logger.error(chalk.red(`ā Rotation failed: ${response.error}`));
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
logger.log(chalk.green('ā
Secret rotated successfully!\n'));
|
|
419
|
-
logger.log(chalk.bold('š Application Details:'));
|
|
420
|
-
logger.log(` Key: ${response.data.application?.key || options.app}`);
|
|
421
|
-
logger.log(` Environment: ${options.environment}\n`);
|
|
422
|
-
|
|
423
|
-
logger.log(chalk.bold.yellow('š NEW CREDENTIALS:'));
|
|
424
|
-
logger.log(chalk.yellow(` Client ID: ${response.data.credentials.clientId}`));
|
|
425
|
-
logger.log(chalk.yellow(` Client Secret: ${response.data.credentials.clientSecret}\n`));
|
|
426
|
-
logger.log(chalk.red('ā ļø Old secret is now invalid. Update GitHub Secrets!\n'));
|
|
427
|
-
|
|
428
|
-
// Save credentials to local secrets if localhost
|
|
429
|
-
if (isLocalhost(config.apiUrl)) {
|
|
430
|
-
const appKey = response.data.application?.key || options.app;
|
|
431
|
-
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
432
|
-
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
await saveLocalSecret(clientIdKey, response.data.credentials.clientId);
|
|
436
|
-
await saveLocalSecret(clientSecretKey, response.data.credentials.clientSecret);
|
|
437
|
-
|
|
438
|
-
// Update env.template
|
|
439
|
-
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey);
|
|
440
|
-
|
|
441
|
-
logger.log(chalk.green('ā Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
442
|
-
logger.log(chalk.green('ā env.template updated with MISO_CLIENTID and MISO_CLIENTSECRET'));
|
|
443
|
-
logger.log('');
|
|
444
|
-
} catch (error) {
|
|
445
|
-
logger.warn(chalk.yellow(`ā ļø Could not save credentials locally: ${error.message}`));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
65
|
+
await rotateSecret(appKey, options);
|
|
449
66
|
} catch (error) {
|
|
450
67
|
logger.error(chalk.red('ā Rotation failed:'), error.message);
|
|
451
68
|
process.exit(1);
|
package/lib/commands/login.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
const inquirer = require('inquirer');
|
|
13
13
|
const chalk = require('chalk');
|
|
14
14
|
const ora = require('ora');
|
|
15
|
-
const {
|
|
15
|
+
const { setCurrentEnvironment, saveDeviceToken, saveClientToken } = require('../config');
|
|
16
16
|
const { makeApiCall, initiateDeviceCodeFlow, pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
|
|
17
|
+
const { formatApiError } = require('../utils/api-error-handler');
|
|
18
|
+
const { loadClientCredentials } = require('../utils/token-manager');
|
|
17
19
|
const logger = require('../utils/logger');
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -118,7 +120,7 @@ async function getEnvironmentKey(environment) {
|
|
|
118
120
|
const envPrompt = await inquirer.prompt([{
|
|
119
121
|
type: 'input',
|
|
120
122
|
name: 'environment',
|
|
121
|
-
message: 'Environment key (e.g., dev, tst, pro):',
|
|
123
|
+
message: 'Environment key (e.g., miso, dev, tst, pro):',
|
|
122
124
|
validate: (input) => {
|
|
123
125
|
if (!input || input.trim().length === 0) {
|
|
124
126
|
return 'Environment key is required';
|
|
@@ -132,50 +134,112 @@ async function getEnvironmentKey(environment) {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
|
-
* Save
|
|
137
|
+
* Save device token configuration (root level, controller-specific)
|
|
138
|
+
* @async
|
|
139
|
+
* @param {string} controllerUrl - Controller URL (used as key)
|
|
140
|
+
* @param {string} token - Authentication token
|
|
141
|
+
* @param {string} refreshToken - Refresh token for token renewal
|
|
142
|
+
* @param {string} expiresAt - Token expiration time
|
|
143
|
+
*/
|
|
144
|
+
async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt) {
|
|
145
|
+
await saveDeviceToken(controllerUrl, token, refreshToken, expiresAt);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Save client credentials token configuration
|
|
136
150
|
* @async
|
|
137
151
|
* @param {string} controllerUrl - Controller URL
|
|
138
152
|
* @param {string} token - Authentication token
|
|
139
153
|
* @param {string} expiresAt - Token expiration time
|
|
140
|
-
* @param {string}
|
|
154
|
+
* @param {string} environment - Environment key
|
|
155
|
+
* @param {string} appName - Application name
|
|
141
156
|
*/
|
|
142
|
-
async function
|
|
143
|
-
await
|
|
144
|
-
apiUrl: controllerUrl,
|
|
145
|
-
token: token,
|
|
146
|
-
expiresAt: expiresAt,
|
|
147
|
-
environment: environment || undefined
|
|
148
|
-
});
|
|
157
|
+
async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, appName) {
|
|
158
|
+
await saveClientToken(environment, appName, controllerUrl, token, expiresAt);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
/**
|
|
152
162
|
* Handle credentials-based login
|
|
163
|
+
* Uses OpenAPI /api/v1/auth/token endpoint with x-client-id and x-client-secret headers
|
|
164
|
+
* Reads credentials from secrets.local.yaml using pattern <app-name>-client-idKeyVault
|
|
153
165
|
* @async
|
|
154
166
|
* @param {string} controllerUrl - Controller URL
|
|
155
|
-
* @param {string}
|
|
156
|
-
* @param {string} [
|
|
167
|
+
* @param {string} appName - Application name
|
|
168
|
+
* @param {string} [clientId] - Client ID from options (optional, overrides secrets.local.yaml)
|
|
169
|
+
* @param {string} [clientSecret] - Client Secret from options (optional, overrides secrets.local.yaml)
|
|
157
170
|
* @returns {Promise<string>} Authentication token
|
|
158
171
|
*/
|
|
159
|
-
async function handleCredentialsLogin(controllerUrl, clientId, clientSecret) {
|
|
160
|
-
|
|
172
|
+
async function handleCredentialsLogin(controllerUrl, appName, clientId, clientSecret) {
|
|
173
|
+
let credentials;
|
|
174
|
+
|
|
175
|
+
// Try to load from secrets.local.yaml if appName provided and credentials not provided
|
|
176
|
+
if (appName && !clientId && !clientSecret) {
|
|
177
|
+
credentials = await loadClientCredentials(appName);
|
|
178
|
+
if (!credentials) {
|
|
179
|
+
logger.log(chalk.yellow(`ā ļø Credentials not found in secrets.local.yaml for app '${appName}'`));
|
|
180
|
+
logger.log(chalk.gray(` Looking for: '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`));
|
|
181
|
+
logger.log(chalk.gray(' Prompting for credentials...\n'));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If still no credentials, prompt for them
|
|
186
|
+
if (!credentials) {
|
|
187
|
+
credentials = await promptForCredentials(clientId, clientSecret);
|
|
188
|
+
}
|
|
161
189
|
|
|
162
|
-
|
|
190
|
+
// OpenAPI spec: POST /api/v1/auth/token with x-client-id and x-client-secret headers
|
|
191
|
+
// Response: { success: boolean, token: string, expiresIn: number, expiresAt: string, timestamp: string }
|
|
192
|
+
const response = await makeApiCall(`${controllerUrl}/api/v1/auth/token`, {
|
|
163
193
|
method: 'POST',
|
|
164
194
|
headers: {
|
|
165
|
-
'Content-Type': 'application/json'
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
clientSecret: credentials.clientSecret
|
|
170
|
-
})
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
'x-client-id': credentials.clientId,
|
|
197
|
+
'x-client-secret': credentials.clientSecret
|
|
198
|
+
}
|
|
171
199
|
});
|
|
172
200
|
|
|
173
201
|
if (!response.success) {
|
|
174
|
-
|
|
202
|
+
const formattedError = response.formattedError || formatApiError(response);
|
|
203
|
+
logger.error(formattedError);
|
|
204
|
+
|
|
205
|
+
// Provide additional context for login failures
|
|
206
|
+
if (response.status === 401) {
|
|
207
|
+
logger.log(chalk.gray('\nš” Tip: Verify your client credentials are correct.'));
|
|
208
|
+
logger.log(chalk.gray(' Check secrets.local.yaml for:'));
|
|
209
|
+
logger.log(chalk.gray(` - ${appName}-client-idKeyVault`));
|
|
210
|
+
logger.log(chalk.gray(` - ${appName}-client-secretKeyVault`));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// OpenAPI spec response: { success: boolean, token: string, expiresIn: number, expiresAt: string, ... }
|
|
217
|
+
// Handle both flat and nested response structures (some APIs wrap in data field)
|
|
218
|
+
const apiResponse = response.data;
|
|
219
|
+
const responseData = apiResponse.data || apiResponse;
|
|
220
|
+
|
|
221
|
+
if (!responseData || !responseData.token) {
|
|
222
|
+
logger.error(chalk.red('ā Invalid response: missing token'));
|
|
223
|
+
if (responseData) {
|
|
224
|
+
logger.error(chalk.gray(`Response structure: ${JSON.stringify(responseData, null, 2)}`));
|
|
225
|
+
}
|
|
175
226
|
process.exit(1);
|
|
176
227
|
}
|
|
177
228
|
|
|
178
|
-
|
|
229
|
+
// Calculate expiration (use expiresAt if provided, otherwise calculate from expiresIn, default to 24 hours)
|
|
230
|
+
let expiresAt;
|
|
231
|
+
if (responseData.expiresAt) {
|
|
232
|
+
expiresAt = responseData.expiresAt;
|
|
233
|
+
} else if (responseData.expiresIn) {
|
|
234
|
+
expiresAt = new Date(Date.now() + responseData.expiresIn * 1000).toISOString();
|
|
235
|
+
} else {
|
|
236
|
+
expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
token: responseData.token,
|
|
241
|
+
expiresAt: expiresAt
|
|
242
|
+
};
|
|
179
243
|
}
|
|
180
244
|
|
|
181
245
|
/**
|
|
@@ -212,12 +276,22 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
212
276
|
spinner.succeed('Authentication approved!');
|
|
213
277
|
|
|
214
278
|
const token = tokenResponse.access_token;
|
|
279
|
+
const refreshToken = tokenResponse.refresh_token;
|
|
215
280
|
const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
|
|
216
281
|
|
|
217
|
-
|
|
282
|
+
// Save device token at root level (controller-specific, not environment-specific)
|
|
283
|
+
await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
|
|
284
|
+
|
|
285
|
+
// Still set current environment if provided (for other purposes)
|
|
286
|
+
if (envKey) {
|
|
287
|
+
await setCurrentEnvironment(envKey);
|
|
288
|
+
}
|
|
218
289
|
|
|
219
290
|
logger.log(chalk.green('\nā
Successfully logged in!'));
|
|
220
291
|
logger.log(chalk.gray(`Controller: ${controllerUrl}`));
|
|
292
|
+
if (envKey) {
|
|
293
|
+
logger.log(chalk.gray(`Environment: ${envKey}`));
|
|
294
|
+
}
|
|
221
295
|
logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
|
|
222
296
|
|
|
223
297
|
return { token, environment: envKey };
|
|
@@ -264,26 +338,46 @@ async function handleDeviceCodeLogin(controllerUrl, environment) {
|
|
|
264
338
|
* @async
|
|
265
339
|
* @function handleLogin
|
|
266
340
|
* @param {Object} options - Login options
|
|
267
|
-
* @param {string} [options.
|
|
341
|
+
* @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
|
|
268
342
|
* @param {string} [options.method] - Authentication method ('device' or 'credentials')
|
|
269
|
-
* @param {string} [options.
|
|
270
|
-
* @param {string} [options.
|
|
271
|
-
* @param {string} [options.
|
|
343
|
+
* @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
|
|
344
|
+
* @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
|
|
345
|
+
* @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
|
|
346
|
+
* @param {string} [options.environment] - Environment key (updates root-level environment in config.yaml)
|
|
272
347
|
* @returns {Promise<void>} Resolves when login completes
|
|
273
348
|
* @throws {Error} If login fails
|
|
274
349
|
*/
|
|
275
350
|
async function handleLogin(options) {
|
|
276
351
|
logger.log(chalk.blue('\nš Logging in to Miso Controller...\n'));
|
|
277
352
|
|
|
278
|
-
const controllerUrl = options.url.replace(/\/$/, '');
|
|
353
|
+
const controllerUrl = (options.controller || options.url || 'http://localhost:3000').replace(/\/$/, '');
|
|
279
354
|
logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
|
|
280
355
|
|
|
356
|
+
// Update root-level environment if provided
|
|
357
|
+
let environment = null;
|
|
358
|
+
if (options.environment) {
|
|
359
|
+
environment = options.environment.trim();
|
|
360
|
+
await setCurrentEnvironment(environment);
|
|
361
|
+
logger.log(chalk.gray(`Environment: ${environment}`));
|
|
362
|
+
} else {
|
|
363
|
+
// Get current environment from config
|
|
364
|
+
const { getCurrentEnvironment } = require('../config');
|
|
365
|
+
environment = await getCurrentEnvironment();
|
|
366
|
+
}
|
|
367
|
+
|
|
281
368
|
const method = await determineAuthMethod(options.method);
|
|
282
369
|
let token;
|
|
283
|
-
let
|
|
370
|
+
let expiresAt;
|
|
284
371
|
|
|
285
372
|
if (method === 'credentials') {
|
|
286
|
-
|
|
373
|
+
if (!options.app) {
|
|
374
|
+
logger.error(chalk.red('ā --app is required for credentials login method'));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
const loginResult = await handleCredentialsLogin(controllerUrl, options.app, options.clientId, options.clientSecret);
|
|
378
|
+
token = loginResult.token;
|
|
379
|
+
expiresAt = loginResult.expiresAt;
|
|
380
|
+
await saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, options.app);
|
|
287
381
|
} else if (method === 'device') {
|
|
288
382
|
const result = await handleDeviceCodeLogin(controllerUrl, options.environment);
|
|
289
383
|
token = result.token;
|
|
@@ -291,12 +385,12 @@ async function handleLogin(options) {
|
|
|
291
385
|
return; // Early return for device flow (already saved config)
|
|
292
386
|
}
|
|
293
387
|
|
|
294
|
-
// Save configuration for credentials method
|
|
295
|
-
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
296
|
-
await saveLoginConfig(controllerUrl, token, expiresAt, environment);
|
|
297
|
-
|
|
298
388
|
logger.log(chalk.green('\nā
Successfully logged in!'));
|
|
299
389
|
logger.log(chalk.gray(`Controller: ${controllerUrl}`));
|
|
390
|
+
logger.log(chalk.gray(`Environment: ${environment}`));
|
|
391
|
+
if (options.app) {
|
|
392
|
+
logger.log(chalk.gray(`App: ${options.app}`));
|
|
393
|
+
}
|
|
300
394
|
logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
|
|
301
395
|
}
|
|
302
396
|
|