@aifabrix/builder 2.22.2 → 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +210 -80
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secure.js +59 -24
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +3 -3
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +7 -5
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/scripts/install-local.js +210 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/app-readme.js
CHANGED
|
@@ -44,11 +44,21 @@ function formatAppDisplayName(appName) {
|
|
|
44
44
|
* Loads and compiles README.md template
|
|
45
45
|
* @returns {Function} Compiled Handlebars template
|
|
46
46
|
* @throws {Error} If template not found
|
|
47
|
+
* @private
|
|
47
48
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
49
|
+
function _loadReadmeTemplate() {
|
|
50
|
+
// Use getProjectRoot to reliably find templates in all environments
|
|
51
|
+
const { getProjectRoot } = require('./utils/paths');
|
|
52
|
+
const projectRoot = getProjectRoot();
|
|
53
|
+
const templatePath = path.join(projectRoot, 'templates', 'applications', 'README.md.hbs');
|
|
54
|
+
|
|
50
55
|
if (!fsSync.existsSync(templatePath)) {
|
|
51
|
-
|
|
56
|
+
// Provide helpful error message with actual paths checked
|
|
57
|
+
const errorMessage = `README template not found at ${templatePath}\n` +
|
|
58
|
+
` Project root: ${projectRoot}\n` +
|
|
59
|
+
` Templates directory: ${path.join(projectRoot, 'templates', 'applications')}\n` +
|
|
60
|
+
` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
|
|
61
|
+
throw new Error(errorMessage);
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
@@ -62,7 +72,6 @@ function loadReadmeTemplate() {
|
|
|
62
72
|
* @returns {string} README.md content
|
|
63
73
|
*/
|
|
64
74
|
function generateReadmeMd(appName, config) {
|
|
65
|
-
const template = loadReadmeTemplate();
|
|
66
75
|
const displayName = formatAppDisplayName(appName);
|
|
67
76
|
const imageName = `aifabrix/${appName}`;
|
|
68
77
|
const port = config.port || 3000;
|
|
@@ -88,7 +97,116 @@ function generateReadmeMd(appName, config) {
|
|
|
88
97
|
hasAnyService
|
|
89
98
|
};
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
// Always generate comprehensive README programmatically to ensure consistency
|
|
101
|
+
// regardless of template file content
|
|
102
|
+
return generateComprehensiveReadme(context);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates comprehensive README.md content programmatically
|
|
107
|
+
* @param {Object} context - Template context
|
|
108
|
+
* @returns {string} Comprehensive README.md content
|
|
109
|
+
*/
|
|
110
|
+
function generateComprehensiveReadme(context) {
|
|
111
|
+
const { appName, displayName, imageName, port, registry, hasDatabase, hasRedis, hasStorage, hasAuthentication, hasAnyService } = context;
|
|
112
|
+
|
|
113
|
+
let prerequisites = 'Before running this application, ensure the following prerequisites are met:\n';
|
|
114
|
+
prerequisites += '- `@aifabrix/builder` installed globally\n';
|
|
115
|
+
prerequisites += '- Docker Desktop running\n';
|
|
116
|
+
|
|
117
|
+
if (hasAnyService) {
|
|
118
|
+
if (hasDatabase) {
|
|
119
|
+
prerequisites += '- PostgreSQL database\n';
|
|
120
|
+
}
|
|
121
|
+
if (hasRedis) {
|
|
122
|
+
prerequisites += '- Redis\n';
|
|
123
|
+
}
|
|
124
|
+
if (hasStorage) {
|
|
125
|
+
prerequisites += '- File storage configured\n';
|
|
126
|
+
}
|
|
127
|
+
if (hasAuthentication) {
|
|
128
|
+
prerequisites += '- Authentication/RBAC configured\n';
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
prerequisites += '- Infrastructure running\n';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let troubleshooting = '';
|
|
135
|
+
if (hasDatabase) {
|
|
136
|
+
troubleshooting = `### Database Connection Issues
|
|
137
|
+
|
|
138
|
+
If you encounter database connection errors, ensure:
|
|
139
|
+
- PostgreSQL is running and accessible
|
|
140
|
+
- Database credentials are correctly configured in your \`.env\` file
|
|
141
|
+
- The database name matches your configuration
|
|
142
|
+
- Verify infrastructure is running and PostgreSQL is accessible`;
|
|
143
|
+
} else {
|
|
144
|
+
troubleshooting = 'Verify infrastructure is running.';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return `# ${displayName} Builder
|
|
148
|
+
|
|
149
|
+
Build, run, and deploy ${displayName}.
|
|
150
|
+
|
|
151
|
+
## Prerequisites
|
|
152
|
+
|
|
153
|
+
${prerequisites}
|
|
154
|
+
|
|
155
|
+
## Quick Start
|
|
156
|
+
|
|
157
|
+
### 1. Install
|
|
158
|
+
|
|
159
|
+
Install the AI Fabrix Builder CLI if you haven't already.
|
|
160
|
+
|
|
161
|
+
### 2. Configure
|
|
162
|
+
|
|
163
|
+
Configure your application settings in \`variables.yaml\`.
|
|
164
|
+
|
|
165
|
+
### 3. Build & Run Locally
|
|
166
|
+
|
|
167
|
+
Build the application:
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
aifabrix build ${appName}
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
Run the application:
|
|
173
|
+
\`\`\`bash
|
|
174
|
+
aifabrix run ${appName}
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
The application will be available at http://localhost:${port} (default: ${port}).
|
|
178
|
+
|
|
179
|
+
### 4. Deploy to Azure
|
|
180
|
+
|
|
181
|
+
Push to registry:
|
|
182
|
+
\`\`\`bash
|
|
183
|
+
aifabrix push ${appName} --registry ${registry} --tag "v1.0.0,latest"
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
- **Port**: ${port} (default: 3000)
|
|
189
|
+
- **Image**: ${imageName}:latest
|
|
190
|
+
- **Registry**: ${registry}
|
|
191
|
+
|
|
192
|
+
## Docker Commands
|
|
193
|
+
|
|
194
|
+
View logs:
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
docker logs aifabrix-${appName} -f
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
Stop the application:
|
|
200
|
+
\`\`\`bash
|
|
201
|
+
aifabrix down ${appName}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## Troubleshooting
|
|
205
|
+
|
|
206
|
+
${troubleshooting}
|
|
207
|
+
|
|
208
|
+
For more information, see the [AI Fabrix Builder documentation](https://docs.aifabrix.com).
|
|
209
|
+
`;
|
|
92
210
|
}
|
|
93
211
|
|
|
94
212
|
/**
|
package/lib/app-rotate-secret.js
CHANGED
|
@@ -20,6 +20,36 @@ const { updateEnvTemplate } = require('./utils/env-template');
|
|
|
20
20
|
const { getEnvironmentPrefix } = require('./app-register');
|
|
21
21
|
const { generateEnvFile } = require('./secrets');
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Find device token from config by trying each stored URL
|
|
25
|
+
* @async
|
|
26
|
+
* @param {Object} deviceConfig - Device configuration object
|
|
27
|
+
* @returns {Promise<Object|null>} Token result with token and controllerUrl, or null if not found
|
|
28
|
+
*/
|
|
29
|
+
async function findDeviceTokenFromConfig(deviceConfig) {
|
|
30
|
+
const deviceUrls = Object.keys(deviceConfig);
|
|
31
|
+
if (deviceUrls.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const storedUrl of deviceUrls) {
|
|
36
|
+
try {
|
|
37
|
+
const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
|
|
38
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
|
|
39
|
+
if (deviceToken && deviceToken.token) {
|
|
40
|
+
return {
|
|
41
|
+
token: deviceToken.token,
|
|
42
|
+
controllerUrl: deviceToken.controller || normalizedStoredUrl
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Continue to next URL
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
/**
|
|
24
54
|
* Validate environment parameter
|
|
25
55
|
* @param {string} environment - Environment ID or key
|
|
@@ -31,20 +61,83 @@ function validateEnvironment(environment) {
|
|
|
31
61
|
}
|
|
32
62
|
}
|
|
33
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Validate credentials object structure
|
|
66
|
+
* @param {Object} credentials - Credentials object to validate
|
|
67
|
+
* @returns {boolean} True if credentials are valid
|
|
68
|
+
*/
|
|
69
|
+
function isValidCredentials(credentials) {
|
|
70
|
+
return credentials &&
|
|
71
|
+
typeof credentials === 'object' &&
|
|
72
|
+
typeof credentials.clientId === 'string' &&
|
|
73
|
+
typeof credentials.clientSecret === 'string';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract credentials from API response
|
|
78
|
+
* Handles multiple response formats:
|
|
79
|
+
* 1. Direct format: { success: true, data: { credentials: {...} } }
|
|
80
|
+
* 2. Wrapped format: { success: true, data: { success: true, data: { credentials: {...} } } }
|
|
81
|
+
* @param {Object} response - API response from centralized API client
|
|
82
|
+
* @returns {Object|null} Object with credentials and message, or null if not found
|
|
83
|
+
*/
|
|
84
|
+
function extractCredentials(response) {
|
|
85
|
+
// Note: response.data is already validated in validateResponse
|
|
86
|
+
const apiResponse = response.data;
|
|
87
|
+
|
|
88
|
+
// Try wrapped format first: response.data.data.credentials
|
|
89
|
+
if (apiResponse.data && apiResponse.data.credentials) {
|
|
90
|
+
const credentials = apiResponse.data.credentials;
|
|
91
|
+
if (isValidCredentials(credentials)) {
|
|
92
|
+
return {
|
|
93
|
+
credentials: credentials,
|
|
94
|
+
message: apiResponse.data.message || apiResponse.message
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try direct format: response.data.credentials
|
|
100
|
+
if (apiResponse.credentials) {
|
|
101
|
+
const credentials = apiResponse.credentials;
|
|
102
|
+
if (isValidCredentials(credentials)) {
|
|
103
|
+
return {
|
|
104
|
+
credentials: credentials,
|
|
105
|
+
message: apiResponse.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
34
113
|
/**
|
|
35
114
|
* Validate API response structure
|
|
36
115
|
* @param {Object} response - API response
|
|
116
|
+
* @returns {Object} Object with credentials and message
|
|
37
117
|
* @throws {Error} If response structure is invalid
|
|
38
118
|
*/
|
|
39
119
|
function validateResponse(response) {
|
|
40
120
|
if (!response.data || typeof response.data !== 'object') {
|
|
121
|
+
logger.error(chalk.red('❌ Invalid response: missing data'));
|
|
122
|
+
logger.error(chalk.gray('\nAPI response type:'), typeof response.data);
|
|
123
|
+
logger.error(chalk.gray('API response:'), JSON.stringify(response.data, null, 2));
|
|
124
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
125
|
+
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
41
126
|
throw new Error('Invalid response: missing data');
|
|
42
127
|
}
|
|
43
128
|
|
|
44
|
-
const
|
|
45
|
-
|
|
129
|
+
const result = extractCredentials(response);
|
|
130
|
+
|
|
131
|
+
if (!result) {
|
|
132
|
+
logger.error(chalk.red('❌ Invalid response: missing or invalid credentials'));
|
|
133
|
+
logger.error(chalk.gray('\nAPI response type:'), typeof response.data);
|
|
134
|
+
logger.error(chalk.gray('API response:'), JSON.stringify(response.data, null, 2));
|
|
135
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
136
|
+
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
46
137
|
throw new Error('Invalid response: missing or invalid credentials');
|
|
47
138
|
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
48
141
|
}
|
|
49
142
|
|
|
50
143
|
/**
|
|
@@ -79,69 +172,135 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
|
|
|
79
172
|
}
|
|
80
173
|
|
|
81
174
|
/**
|
|
82
|
-
*
|
|
175
|
+
* Get device token from provided controller URL
|
|
83
176
|
* @async
|
|
84
|
-
* @param {string}
|
|
85
|
-
* @
|
|
86
|
-
* @param {string} options.environment - Environment ID or key
|
|
87
|
-
* @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
|
|
88
|
-
* @throws {Error} If rotation fails
|
|
177
|
+
* @param {string} controllerUrl - Controller URL
|
|
178
|
+
* @returns {Promise<Object|null>} Object with token and controllerUrl, or null if failed
|
|
89
179
|
*/
|
|
90
|
-
async function
|
|
91
|
-
|
|
180
|
+
async function getTokenFromUrl(controllerUrl) {
|
|
181
|
+
try {
|
|
182
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
183
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
184
|
+
if (deviceToken && deviceToken.token) {
|
|
185
|
+
return {
|
|
186
|
+
token: deviceToken.token,
|
|
187
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
|
|
192
|
+
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
92
197
|
|
|
93
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Validate and handle missing authentication token
|
|
200
|
+
* @param {string|null} token - Authentication token
|
|
201
|
+
* @param {string|null} controllerUrl - Controller URL
|
|
202
|
+
* @param {string} [providedUrl] - Original provided URL for error context
|
|
203
|
+
*/
|
|
204
|
+
function validateAuthToken(token, controllerUrl, providedUrl) {
|
|
205
|
+
if (!token || !controllerUrl) {
|
|
206
|
+
const formattedError = formatAuthenticationError({
|
|
207
|
+
controllerUrl: providedUrl || undefined,
|
|
208
|
+
message: 'No valid authentication found'
|
|
209
|
+
});
|
|
210
|
+
logger.error(formattedError);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
94
214
|
|
|
95
|
-
|
|
96
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Get authentication token for rotation
|
|
217
|
+
* @async
|
|
218
|
+
* @param {string} [controllerUrl] - Optional controller URL
|
|
219
|
+
* @param {Object} config - Configuration object
|
|
220
|
+
* @returns {Promise<Object>} Object with token and actualControllerUrl
|
|
221
|
+
* @throws {Error} If authentication fails
|
|
222
|
+
*/
|
|
223
|
+
async function getRotationAuthToken(controllerUrl, config) {
|
|
97
224
|
let token = null;
|
|
98
225
|
let actualControllerUrl = null;
|
|
99
226
|
|
|
100
227
|
// If controller URL provided, try to get device token
|
|
101
228
|
if (controllerUrl) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
token = deviceToken.token;
|
|
107
|
-
actualControllerUrl = deviceToken.controller || normalizedUrl;
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
// Show which controller URL failed
|
|
111
|
-
logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
|
|
112
|
-
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
113
|
-
process.exit(1);
|
|
229
|
+
const tokenResult = await getTokenFromUrl(controllerUrl);
|
|
230
|
+
if (tokenResult) {
|
|
231
|
+
token = tokenResult.token;
|
|
232
|
+
actualControllerUrl = tokenResult.controllerUrl;
|
|
114
233
|
}
|
|
115
234
|
}
|
|
116
235
|
|
|
117
236
|
// If no token yet, try to find any device token in config
|
|
118
237
|
if (!token && config.device) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
|
|
124
|
-
const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
|
|
125
|
-
if (deviceToken && deviceToken.token) {
|
|
126
|
-
token = deviceToken.token;
|
|
127
|
-
actualControllerUrl = deviceToken.controller || normalizedStoredUrl;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
// Continue to next URL
|
|
132
|
-
}
|
|
133
|
-
}
|
|
238
|
+
const tokenResult = await findDeviceTokenFromConfig(config.device);
|
|
239
|
+
if (tokenResult) {
|
|
240
|
+
token = tokenResult.token;
|
|
241
|
+
actualControllerUrl = tokenResult.controllerUrl;
|
|
134
242
|
}
|
|
135
243
|
}
|
|
136
244
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
245
|
+
validateAuthToken(token, actualControllerUrl, controllerUrl);
|
|
246
|
+
return { token, actualControllerUrl };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Save credentials locally and update env files
|
|
251
|
+
* @async
|
|
252
|
+
* @param {string} appKey - Application key
|
|
253
|
+
* @param {Object} credentials - Credentials object
|
|
254
|
+
* @param {string} actualControllerUrl - Controller URL
|
|
255
|
+
* @throws {Error} If saving fails
|
|
256
|
+
*/
|
|
257
|
+
async function saveCredentialsLocally(appKey, credentials, actualControllerUrl) {
|
|
258
|
+
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
259
|
+
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await saveLocalSecret(clientIdKey, credentials.clientId);
|
|
263
|
+
await saveLocalSecret(clientSecretKey, credentials.clientSecret);
|
|
264
|
+
|
|
265
|
+
// Update env.template if localhost
|
|
266
|
+
if (isLocalhost(actualControllerUrl)) {
|
|
267
|
+
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
|
|
268
|
+
|
|
269
|
+
// Regenerate .env file with updated credentials
|
|
270
|
+
try {
|
|
271
|
+
await generateEnvFile(appKey, null, 'local');
|
|
272
|
+
logger.log(chalk.green('✓ .env file updated with new credentials'));
|
|
273
|
+
} catch (error) {
|
|
274
|
+
logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
278
|
+
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
|
|
279
|
+
} else {
|
|
280
|
+
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
144
284
|
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Rotate secret for an application
|
|
289
|
+
* @async
|
|
290
|
+
* @param {string} appKey - Application key
|
|
291
|
+
* @param {Object} options - Command options
|
|
292
|
+
* @param {string} options.environment - Environment ID or key
|
|
293
|
+
* @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
|
|
294
|
+
* @throws {Error} If rotation fails
|
|
295
|
+
*/
|
|
296
|
+
async function rotateSecret(appKey, options) {
|
|
297
|
+
logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
|
|
298
|
+
|
|
299
|
+
const config = await getConfig();
|
|
300
|
+
|
|
301
|
+
// Get authentication token
|
|
302
|
+
const controllerUrl = options.controller || null;
|
|
303
|
+
const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
|
|
145
304
|
|
|
146
305
|
// Validate environment
|
|
147
306
|
validateEnvironment(options.environment);
|
|
@@ -158,40 +317,11 @@ async function rotateSecret(appKey, options) {
|
|
|
158
317
|
process.exit(1);
|
|
159
318
|
}
|
|
160
319
|
|
|
161
|
-
// Validate response structure
|
|
162
|
-
validateResponse(response);
|
|
320
|
+
// Validate response structure and extract credentials
|
|
321
|
+
const { credentials, message } = validateResponse(response);
|
|
163
322
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Save credentials to local secrets (always save when rotating)
|
|
168
|
-
const clientIdKey = `${appKey}-client-idKeyVault`;
|
|
169
|
-
const clientSecretKey = `${appKey}-client-secretKeyVault`;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
await saveLocalSecret(clientIdKey, credentials.clientId);
|
|
173
|
-
await saveLocalSecret(clientSecretKey, credentials.clientSecret);
|
|
174
|
-
|
|
175
|
-
// Update env.template if localhost
|
|
176
|
-
if (isLocalhost(actualControllerUrl)) {
|
|
177
|
-
await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, actualControllerUrl);
|
|
178
|
-
|
|
179
|
-
// Regenerate .env file with updated credentials
|
|
180
|
-
try {
|
|
181
|
-
await generateEnvFile(appKey, null, 'local');
|
|
182
|
-
logger.log(chalk.green('✓ .env file updated with new credentials'));
|
|
183
|
-
} catch (error) {
|
|
184
|
-
logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
|
|
188
|
-
logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
|
|
189
|
-
} else {
|
|
190
|
-
logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
|
|
191
|
-
}
|
|
192
|
-
} catch (error) {
|
|
193
|
-
logger.warn(chalk.yellow(`⚠️ Could not save credentials locally: ${error.message}`));
|
|
194
|
-
}
|
|
323
|
+
// Save credentials locally
|
|
324
|
+
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
195
325
|
|
|
196
326
|
// Display results
|
|
197
327
|
displayRotationResults(appKey, options.environment, credentials, actualControllerUrl, message);
|