@aifabrix/builder 2.4.0 → 2.5.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/README.md +3 -0
- package/lib/app-down.js +123 -0
- package/lib/app.js +4 -2
- package/lib/build.js +19 -13
- package/lib/cli.js +52 -9
- package/lib/config.js +25 -3
- package/lib/env-reader.js +3 -2
- package/lib/generator.js +0 -9
- package/lib/infra.js +30 -3
- package/lib/schema/application-schema.json +0 -15
- package/lib/schema/env-config.yaml +8 -8
- package/lib/secrets.js +164 -250
- package/lib/templates.js +10 -18
- package/lib/utils/api-error-handler.js +182 -147
- package/lib/utils/api.js +144 -354
- package/lib/utils/build-copy.js +6 -13
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/device-code.js +349 -0
- package/lib/utils/env-config-loader.js +102 -0
- package/lib/utils/env-copy.js +131 -0
- package/lib/utils/env-endpoints.js +209 -0
- package/lib/utils/env-map.js +116 -0
- package/lib/utils/env-ports.js +60 -0
- package/lib/utils/environment-checker.js +39 -6
- package/lib/utils/image-name.js +49 -0
- package/lib/utils/paths.js +22 -20
- package/lib/utils/secrets-generator.js +3 -3
- package/lib/utils/secrets-helpers.js +359 -0
- package/lib/utils/secrets-path.js +12 -36
- package/lib/utils/secrets-url.js +38 -0
- package/lib/utils/secrets-utils.js +0 -41
- package/lib/utils/variable-transformer.js +0 -9
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +3 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/infra/compose.yaml +4 -0
- package/templates/infra/compose.yaml.hbs +9 -4
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Device Code Flow Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth2 Device Code Flow (RFC 8628) authentication
|
|
5
|
+
* Supports device code initiation, token polling, and token refresh
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Device code flow utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Lazy require to avoid circular dependency
|
|
13
|
+
let makeApiCall;
|
|
14
|
+
function getMakeApiCall() {
|
|
15
|
+
if (!makeApiCall) {
|
|
16
|
+
const api = require('./api');
|
|
17
|
+
makeApiCall = api.makeApiCall;
|
|
18
|
+
}
|
|
19
|
+
return makeApiCall;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parses device code response from API
|
|
24
|
+
* Matches OpenAPI DeviceCodeResponse schema (camelCase)
|
|
25
|
+
* @function parseDeviceCodeResponse
|
|
26
|
+
* @param {Object} response - API response object
|
|
27
|
+
* @returns {Object} Parsed device code response
|
|
28
|
+
* @throws {Error} If response is invalid
|
|
29
|
+
*/
|
|
30
|
+
function parseDeviceCodeResponse(response) {
|
|
31
|
+
// OpenAPI spec: { success: boolean, data: DeviceCodeResponse, timestamp: string }
|
|
32
|
+
const apiResponse = response.data;
|
|
33
|
+
const responseData = apiResponse.data || apiResponse;
|
|
34
|
+
|
|
35
|
+
// OpenAPI spec uses camelCase: deviceCode, userCode, verificationUri, expiresIn, interval
|
|
36
|
+
const deviceCode = responseData.deviceCode;
|
|
37
|
+
const userCode = responseData.userCode;
|
|
38
|
+
const verificationUri = responseData.verificationUri;
|
|
39
|
+
const expiresIn = responseData.expiresIn || 600;
|
|
40
|
+
const interval = responseData.interval || 5;
|
|
41
|
+
|
|
42
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
43
|
+
throw new Error('Invalid device code response: missing required fields');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Return in snake_case for internal consistency (used by existing code)
|
|
47
|
+
return {
|
|
48
|
+
device_code: deviceCode,
|
|
49
|
+
user_code: userCode,
|
|
50
|
+
verification_uri: verificationUri,
|
|
51
|
+
expires_in: expiresIn,
|
|
52
|
+
interval: interval
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initiates OAuth2 Device Code Flow
|
|
58
|
+
* Calls the device code endpoint to get device_code and user_code
|
|
59
|
+
*
|
|
60
|
+
* @async
|
|
61
|
+
* @function initiateDeviceCodeFlow
|
|
62
|
+
* @param {string} controllerUrl - Base URL of the controller
|
|
63
|
+
* @param {string} environment - Environment key (e.g., 'miso', 'dev', 'tst', 'pro')
|
|
64
|
+
* @returns {Promise<Object>} Device code response with device_code, user_code, verification_uri, expires_in, interval
|
|
65
|
+
* @throws {Error} If initiation fails
|
|
66
|
+
*/
|
|
67
|
+
async function initiateDeviceCodeFlow(controllerUrl, environment) {
|
|
68
|
+
if (!environment || typeof environment !== 'string') {
|
|
69
|
+
throw new Error('Environment key is required');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const url = `${controllerUrl}/api/v1/auth/login?environment=${encodeURIComponent(environment)}`;
|
|
73
|
+
const response = await getMakeApiCall()(url, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json'
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.success) {
|
|
81
|
+
throw new Error(`Device code initiation failed: ${response.error || 'Unknown error'}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return parseDeviceCodeResponse(response);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks if token has expired based on elapsed time
|
|
89
|
+
* @function checkTokenExpiration
|
|
90
|
+
* @param {number} startTime - Start time in milliseconds
|
|
91
|
+
* @param {number} expiresIn - Expiration time in seconds
|
|
92
|
+
* @throws {Error} If token has expired
|
|
93
|
+
*/
|
|
94
|
+
function checkTokenExpiration(startTime, expiresIn) {
|
|
95
|
+
const maxWaitTime = (expiresIn + 30) * 1000;
|
|
96
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
97
|
+
throw new Error('Device code expired: Maximum polling time exceeded');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parses token response from API
|
|
103
|
+
* Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
|
|
104
|
+
* @function parseTokenResponse
|
|
105
|
+
* @param {Object} response - API response object
|
|
106
|
+
* @returns {Object|null} Parsed token response or null if pending
|
|
107
|
+
*/
|
|
108
|
+
function parseTokenResponse(response) {
|
|
109
|
+
// OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
|
|
110
|
+
const apiResponse = response.data;
|
|
111
|
+
const responseData = apiResponse.data || apiResponse;
|
|
112
|
+
|
|
113
|
+
const error = responseData.error || apiResponse.error;
|
|
114
|
+
if (error === 'authorization_pending' || error === 'slow_down') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
|
|
119
|
+
const accessToken = responseData.accessToken;
|
|
120
|
+
const refreshToken = responseData.refreshToken;
|
|
121
|
+
const expiresIn = responseData.expiresIn || 3600;
|
|
122
|
+
|
|
123
|
+
if (!accessToken) {
|
|
124
|
+
throw new Error('Invalid token response: missing accessToken');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Return in snake_case for internal consistency (used by existing code)
|
|
128
|
+
return {
|
|
129
|
+
access_token: accessToken,
|
|
130
|
+
refresh_token: refreshToken,
|
|
131
|
+
expires_in: expiresIn
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handles polling errors
|
|
137
|
+
* @function handlePollingErrors
|
|
138
|
+
* @param {string} error - Error code
|
|
139
|
+
* @param {number} status - HTTP status code
|
|
140
|
+
* @throws {Error} For fatal errors
|
|
141
|
+
* @returns {boolean} True if should continue polling
|
|
142
|
+
*/
|
|
143
|
+
function handlePollingErrors(error, status) {
|
|
144
|
+
if (error === 'authorization_pending' || status === 202) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check error field first, then status code
|
|
149
|
+
if (error === 'authorization_declined') {
|
|
150
|
+
throw new Error('Authorization declined: User denied the request');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (error === 'expired_token' || status === 410) {
|
|
154
|
+
throw new Error('Device code expired: Please restart the authentication process');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (error === 'slow_down') {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error(`Token polling failed: ${error}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Waits for next polling interval
|
|
166
|
+
* @async
|
|
167
|
+
* @function waitForNextPoll
|
|
168
|
+
* @param {number} interval - Polling interval in seconds
|
|
169
|
+
* @param {boolean} slowDown - Whether to slow down
|
|
170
|
+
*/
|
|
171
|
+
async function waitForNextPoll(interval, slowDown) {
|
|
172
|
+
const waitInterval = slowDown ? interval * 2 : interval;
|
|
173
|
+
await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extracts error from API response
|
|
178
|
+
* @param {Object} response - API response object
|
|
179
|
+
* @returns {string} Error code or 'Unknown error'
|
|
180
|
+
*/
|
|
181
|
+
function extractPollingError(response) {
|
|
182
|
+
const apiResponse = response.data || {};
|
|
183
|
+
const errorData = typeof apiResponse === 'object' ? apiResponse : {};
|
|
184
|
+
return errorData.error || response.error || 'Unknown error';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handles successful polling response
|
|
189
|
+
* @param {Object} response - API response object
|
|
190
|
+
* @returns {Object|null} Token response or null if pending
|
|
191
|
+
*/
|
|
192
|
+
function handleSuccessfulPoll(response) {
|
|
193
|
+
const tokenResponse = parseTokenResponse(response);
|
|
194
|
+
if (tokenResponse) {
|
|
195
|
+
return tokenResponse;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Processes polling response and determines next action
|
|
202
|
+
* @async
|
|
203
|
+
* @function processPollingResponse
|
|
204
|
+
* @param {Object} response - API response object
|
|
205
|
+
* @param {number} interval - Polling interval in seconds
|
|
206
|
+
* @returns {Promise<Object|null>} Token response if complete, null if should continue
|
|
207
|
+
*/
|
|
208
|
+
async function processPollingResponse(response, interval) {
|
|
209
|
+
if (response.success) {
|
|
210
|
+
const tokenResponse = handleSuccessfulPoll(response);
|
|
211
|
+
if (tokenResponse) {
|
|
212
|
+
return tokenResponse;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const apiResponse = response.data;
|
|
216
|
+
const responseData = apiResponse.data || apiResponse;
|
|
217
|
+
const error = responseData.error || apiResponse.error;
|
|
218
|
+
const slowDown = error === 'slow_down';
|
|
219
|
+
await waitForNextPoll(interval, slowDown);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const error = extractPollingError(response);
|
|
224
|
+
const shouldContinue = handlePollingErrors(error, response.status);
|
|
225
|
+
|
|
226
|
+
if (shouldContinue) {
|
|
227
|
+
const slowDown = error === 'slow_down';
|
|
228
|
+
await waitForNextPoll(interval, slowDown);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Polls for token during Device Code Flow
|
|
237
|
+
* Continuously polls the token endpoint until user approves or flow expires
|
|
238
|
+
*
|
|
239
|
+
* @async
|
|
240
|
+
* @function pollDeviceCodeToken
|
|
241
|
+
* @param {string} controllerUrl - Base URL of the controller
|
|
242
|
+
* @param {string} deviceCode - Device code from initiation
|
|
243
|
+
* @param {number} interval - Polling interval in seconds
|
|
244
|
+
* @param {number} expiresIn - Expiration time in seconds
|
|
245
|
+
* @param {Function} [onPoll] - Optional callback called on each poll attempt
|
|
246
|
+
* @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
|
|
247
|
+
* @throws {Error} If polling fails or token is expired/declined
|
|
248
|
+
*/
|
|
249
|
+
async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresIn, onPoll) {
|
|
250
|
+
if (!deviceCode || typeof deviceCode !== 'string') {
|
|
251
|
+
throw new Error('Device code is required');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const url = `${controllerUrl}/api/v1/auth/login/device/token`;
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
|
|
257
|
+
// eslint-disable-next-line no-constant-condition
|
|
258
|
+
while (true) {
|
|
259
|
+
checkTokenExpiration(startTime, expiresIn);
|
|
260
|
+
|
|
261
|
+
if (onPoll) {
|
|
262
|
+
onPoll();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const response = await getMakeApiCall()(url, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'Content-Type': 'application/json'
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
deviceCode: deviceCode
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const tokenResponse = await processPollingResponse(response, interval);
|
|
276
|
+
if (tokenResponse) {
|
|
277
|
+
return tokenResponse;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Displays device code information to the user
|
|
284
|
+
* Formats user code and verification URL for easy reading
|
|
285
|
+
*
|
|
286
|
+
* @function displayDeviceCodeInfo
|
|
287
|
+
* @param {string} userCode - User code to display
|
|
288
|
+
* @param {string} verificationUri - Verification URL
|
|
289
|
+
* @param {Object} logger - Logger instance with log method
|
|
290
|
+
* @param {Object} chalk - Chalk instance for colored output
|
|
291
|
+
*/
|
|
292
|
+
function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
|
|
293
|
+
logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
294
|
+
logger.log(chalk.cyan(' Device Code Flow Authentication'));
|
|
295
|
+
logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
296
|
+
logger.log(chalk.yellow('To complete authentication:'));
|
|
297
|
+
logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(verificationUri));
|
|
298
|
+
logger.log(chalk.gray(' 2. Enter code: ') + chalk.bold.cyan(userCode));
|
|
299
|
+
logger.log(chalk.gray(' 3. Approve the request\n'));
|
|
300
|
+
logger.log(chalk.gray('Waiting for approval...'));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Refresh device code access token using refresh token
|
|
305
|
+
* Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
|
|
306
|
+
*
|
|
307
|
+
* @async
|
|
308
|
+
* @function refreshDeviceToken
|
|
309
|
+
* @param {string} controllerUrl - Base URL of the controller
|
|
310
|
+
* @param {string} refreshToken - Refresh token from previous authentication
|
|
311
|
+
* @returns {Promise<Object>} Token response with access_token, refresh_token, expires_in
|
|
312
|
+
* @throws {Error} If refresh fails or refresh token is invalid/expired
|
|
313
|
+
*/
|
|
314
|
+
async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
315
|
+
if (!refreshToken || typeof refreshToken !== 'string') {
|
|
316
|
+
throw new Error('Refresh token is required');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
|
|
320
|
+
const response = await makeApiCall(url, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'application/json'
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({ refreshToken })
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!response.success) {
|
|
329
|
+
const errorMsg = response.error || 'Unknown error';
|
|
330
|
+
throw new Error(`Failed to refresh token: ${errorMsg}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Parse response using existing parseTokenResponse function
|
|
334
|
+
const tokenResponse = parseTokenResponse(response);
|
|
335
|
+
if (!tokenResponse) {
|
|
336
|
+
throw new Error('Invalid refresh token response');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return tokenResponse;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
initiateDeviceCodeFlow,
|
|
344
|
+
pollDeviceCodeToken,
|
|
345
|
+
displayDeviceCodeInfo,
|
|
346
|
+
refreshDeviceToken,
|
|
347
|
+
parseTokenResponse
|
|
348
|
+
};
|
|
349
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment config loader
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Loads lib/schema/env-config.yaml for env variable interpolation
|
|
5
|
+
* Merges with user's env-config file if configured in ~/.aifabrix/config.yaml
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const config = require('../config');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Loads user env-config file if configured
|
|
19
|
+
* @async
|
|
20
|
+
* @function loadUserEnvConfig
|
|
21
|
+
* @returns {Promise<Object|null>} Parsed user env-config or null if not configured
|
|
22
|
+
*/
|
|
23
|
+
async function loadUserEnvConfig() {
|
|
24
|
+
try {
|
|
25
|
+
const userEnvConfigPath = await config.getAifabrixEnvConfigPath();
|
|
26
|
+
if (!userEnvConfigPath) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve path (support absolute and relative paths)
|
|
31
|
+
const resolvedPath = path.isAbsolute(userEnvConfigPath)
|
|
32
|
+
? userEnvConfigPath
|
|
33
|
+
: path.resolve(process.cwd(), userEnvConfigPath);
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
40
|
+
const userConfig = yaml.load(content);
|
|
41
|
+
return userConfig && typeof userConfig === 'object' ? userConfig : null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Gracefully handle errors - fallback to base config only
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merges user env-config with base env-config
|
|
50
|
+
* User values override/extend base values
|
|
51
|
+
* @function mergeEnvConfigs
|
|
52
|
+
* @param {Object} baseConfig - Base env-config from lib/schema/env-config.yaml
|
|
53
|
+
* @param {Object|null} userConfig - User env-config or null
|
|
54
|
+
* @returns {Object} Merged env-config
|
|
55
|
+
*/
|
|
56
|
+
function mergeEnvConfigs(baseConfig, userConfig) {
|
|
57
|
+
if (!userConfig || typeof userConfig !== 'object') {
|
|
58
|
+
return baseConfig || {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Deep merge environments
|
|
62
|
+
const merged = { ...baseConfig };
|
|
63
|
+
if (userConfig.environments && typeof userConfig.environments === 'object') {
|
|
64
|
+
merged.environments = { ...(baseConfig.environments || {}) };
|
|
65
|
+
for (const [env, envVars] of Object.entries(userConfig.environments)) {
|
|
66
|
+
if (envVars && typeof envVars === 'object') {
|
|
67
|
+
merged.environments[env] = {
|
|
68
|
+
...(merged.environments[env] || {}),
|
|
69
|
+
...envVars
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load env config YAML used for environment variable interpolation
|
|
80
|
+
* Loads base config from lib/schema/env-config.yaml and merges with user config if configured
|
|
81
|
+
* @async
|
|
82
|
+
* @function loadEnvConfig
|
|
83
|
+
* @returns {Promise<Object>} Parsed and merged env-config YAML
|
|
84
|
+
* @throws {Error} If base file cannot be read or parsed
|
|
85
|
+
*/
|
|
86
|
+
async function loadEnvConfig() {
|
|
87
|
+
// Load base env-config.yaml
|
|
88
|
+
const envConfigPath = path.join(__dirname, '..', 'schema', 'env-config.yaml');
|
|
89
|
+
const content = fs.readFileSync(envConfigPath, 'utf8');
|
|
90
|
+
const baseConfig = yaml.load(content) || {};
|
|
91
|
+
|
|
92
|
+
// Load user env-config if configured
|
|
93
|
+
const userConfig = await loadUserEnvConfig();
|
|
94
|
+
|
|
95
|
+
// Merge user config with base (user overrides/extends base)
|
|
96
|
+
return mergeEnvConfigs(baseConfig, userConfig);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
loadEnvConfig
|
|
101
|
+
};
|
|
102
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment copy and port update utilities
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Copy .env to app output and apply local/dockerside port rules with dev offsets
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const logger = require('./logger');
|
|
16
|
+
const config = require('../config');
|
|
17
|
+
const devConfig = require('../utils/dev-config');
|
|
18
|
+
const { rewriteInfraEndpoints } = require('./env-endpoints');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process and optionally copy env file to envOutputPath if configured
|
|
22
|
+
* Regenerates .env file with env=local for local development (apps/.env)
|
|
23
|
+
* @async
|
|
24
|
+
* @function processEnvVariables
|
|
25
|
+
* @param {string} envPath - Path to generated .env file
|
|
26
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
27
|
+
* @param {string} appName - Application name (for regenerating with local env)
|
|
28
|
+
* @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
|
|
29
|
+
*/
|
|
30
|
+
async function processEnvVariables(envPath, variablesPath, appName, secretsPath) {
|
|
31
|
+
if (!fs.existsSync(variablesPath)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
35
|
+
const variables = yaml.load(variablesContent);
|
|
36
|
+
if (!variables?.build?.envOutputPath || variables.build.envOutputPath === null) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Resolve output path: absolute stays as-is; relative is resolved against variables.yaml directory
|
|
40
|
+
const rawOutputPath = variables.build.envOutputPath;
|
|
41
|
+
let outputPath;
|
|
42
|
+
if (path.isAbsolute(rawOutputPath)) {
|
|
43
|
+
outputPath = rawOutputPath;
|
|
44
|
+
} else {
|
|
45
|
+
const variablesDir = path.dirname(variablesPath);
|
|
46
|
+
outputPath = path.resolve(variablesDir, rawOutputPath);
|
|
47
|
+
}
|
|
48
|
+
if (!outputPath.endsWith('.env')) {
|
|
49
|
+
if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
|
|
50
|
+
outputPath = path.join(outputPath, '.env');
|
|
51
|
+
} else {
|
|
52
|
+
outputPath = path.join(outputPath, '.env');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const outputDir = path.dirname(outputPath);
|
|
56
|
+
if (!fs.existsSync(outputDir)) {
|
|
57
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Regenerate .env file with env=local instead of copying docker-generated file
|
|
61
|
+
// This ensures all variables use localhost instead of docker service names
|
|
62
|
+
if (appName) {
|
|
63
|
+
const { generateEnvContent } = require('../secrets');
|
|
64
|
+
// Generate local .env content (without writing to builder/.env to avoid overwriting docker version)
|
|
65
|
+
const localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
|
|
66
|
+
// Write to output path
|
|
67
|
+
fs.writeFileSync(outputPath, localEnvContent, { mode: 0o600 });
|
|
68
|
+
logger.log(chalk.green(`✓ Generated local .env at: ${variables.build.envOutputPath}`));
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: if appName not provided, use old patching approach
|
|
71
|
+
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
72
|
+
// Determine base app port and compute developer-specific app port
|
|
73
|
+
const baseAppPort = variables.build?.localPort || variables.port || 3000;
|
|
74
|
+
const devIdRaw = process.env.AIFABRIX_DEVELOPERID;
|
|
75
|
+
// Best effort: parse from env first, otherwise rely on config (may throw if async, so guarded below)
|
|
76
|
+
let devIdNum = Number.isFinite(parseInt(devIdRaw, 10)) ? parseInt(devIdRaw, 10) : null;
|
|
77
|
+
try {
|
|
78
|
+
if (devIdNum === null) {
|
|
79
|
+
// Try to read developer-id from config file synchronously if present
|
|
80
|
+
const configPath = config && config.CONFIG_FILE ? config.CONFIG_FILE : null;
|
|
81
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const cfgContent = fs.readFileSync(configPath, 'utf8');
|
|
84
|
+
const cfg = yaml.load(cfgContent) || {};
|
|
85
|
+
const raw = cfg['developer-id'];
|
|
86
|
+
if (typeof raw === 'number') {
|
|
87
|
+
devIdNum = raw;
|
|
88
|
+
} else if (typeof raw === 'string' && /^[0-9]+$/.test(raw)) {
|
|
89
|
+
devIdNum = parseInt(raw, 10);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore, will fallback to 0
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (devIdNum === null || Number.isNaN(devIdNum)) {
|
|
96
|
+
devIdNum = 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
devIdNum = 0;
|
|
101
|
+
}
|
|
102
|
+
const appPort = devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
|
|
103
|
+
const infraPorts = devConfig.getDevPorts(devIdNum);
|
|
104
|
+
|
|
105
|
+
// Update PORT (replace or append)
|
|
106
|
+
if (/^PORT\s*=.*$/m.test(envContent)) {
|
|
107
|
+
envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${appPort}`);
|
|
108
|
+
} else {
|
|
109
|
+
envContent = `${envContent}\nPORT=${appPort}\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Update localhost URLs that point to the base app port to the dev-specific app port
|
|
113
|
+
const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
|
|
114
|
+
envContent = envContent.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
|
|
115
|
+
const num = parseInt(portNum, 10);
|
|
116
|
+
if (num === baseAppPort) {
|
|
117
|
+
return `${prefix}${appPort}${rest || ''}`;
|
|
118
|
+
}
|
|
119
|
+
return match;
|
|
120
|
+
});
|
|
121
|
+
// Rewrite infra endpoints using env-config mapping for local context
|
|
122
|
+
envContent = await rewriteInfraEndpoints(envContent, 'local', infraPorts);
|
|
123
|
+
fs.writeFileSync(outputPath, envContent, { mode: 0o600 });
|
|
124
|
+
logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
processEnvVariables
|
|
130
|
+
};
|
|
131
|
+
|