@aifabrix/builder 2.40.0 → 2.41.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 +7 -5
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +29 -0
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-auth.js +1 -0
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +19 -4
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/auth-status.js +36 -3
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +13 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +49 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validate.js +1 -1
- package/lib/validation/validator.js +65 -0
- package/package.json +4 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/external-system/external-system.json.hbs +1 -16
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
package/lib/utils/device-code.js
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const {
|
|
13
|
+
parseDeviceCodeResponse,
|
|
14
|
+
parseTokenResponse,
|
|
15
|
+
checkTokenExpiration,
|
|
16
|
+
processPollingResponse
|
|
17
|
+
} = require('./device-code-helpers');
|
|
18
|
+
|
|
12
19
|
// Lazy require to avoid circular dependency
|
|
13
20
|
let makeApiCall;
|
|
14
21
|
function getMakeApiCall() {
|
|
@@ -19,40 +26,6 @@ function getMakeApiCall() {
|
|
|
19
26
|
return makeApiCall;
|
|
20
27
|
}
|
|
21
28
|
|
|
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
29
|
/**
|
|
57
30
|
* Initiates OAuth2 Device Code Flow
|
|
58
31
|
* Calls the device code endpoint to get device_code and user_code
|
|
@@ -70,7 +43,6 @@ async function initiateDeviceCodeFlow(controllerUrl, environment, scope) {
|
|
|
70
43
|
throw new Error('Environment key is required');
|
|
71
44
|
}
|
|
72
45
|
|
|
73
|
-
// Default scope for backward compatibility
|
|
74
46
|
const defaultScope = 'openid profile email';
|
|
75
47
|
const requestScope = scope || defaultScope;
|
|
76
48
|
|
|
@@ -92,298 +64,6 @@ async function initiateDeviceCodeFlow(controllerUrl, environment, scope) {
|
|
|
92
64
|
return parseDeviceCodeResponse(response);
|
|
93
65
|
}
|
|
94
66
|
|
|
95
|
-
/**
|
|
96
|
-
* Checks if token has expired based on elapsed time
|
|
97
|
-
* @function checkTokenExpiration
|
|
98
|
-
* @param {number} startTime - Start time in milliseconds
|
|
99
|
-
* @param {number} expiresIn - Expiration time in seconds
|
|
100
|
-
* @throws {Error} If token has expired
|
|
101
|
-
*/
|
|
102
|
-
function checkTokenExpiration(startTime, expiresIn) {
|
|
103
|
-
const maxWaitTime = (expiresIn + 30) * 1000;
|
|
104
|
-
if (Date.now() - startTime > maxWaitTime) {
|
|
105
|
-
throw new Error('Device code expired: Maximum polling time exceeded');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Parses token response from API
|
|
111
|
-
* Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
|
|
112
|
-
* @function parseTokenResponse
|
|
113
|
-
* @param {Object} response - API response object
|
|
114
|
-
* @returns {Object|null} Parsed token response or null if pending
|
|
115
|
-
*/
|
|
116
|
-
function parseTokenResponse(response) {
|
|
117
|
-
// OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
|
|
118
|
-
const apiResponse = response.data;
|
|
119
|
-
const responseData = apiResponse.data || apiResponse;
|
|
120
|
-
|
|
121
|
-
const error = responseData.error || apiResponse.error;
|
|
122
|
-
if (error === 'authorization_pending' || error === 'slow_down') {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
|
|
127
|
-
const accessToken = responseData.accessToken;
|
|
128
|
-
const refreshToken = responseData.refreshToken;
|
|
129
|
-
const expiresIn = responseData.expiresIn || 3600;
|
|
130
|
-
|
|
131
|
-
if (!accessToken) {
|
|
132
|
-
throw new Error('Invalid token response: missing accessToken');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Return in snake_case for internal consistency (used by existing code)
|
|
136
|
-
return {
|
|
137
|
-
access_token: accessToken,
|
|
138
|
-
refresh_token: refreshToken,
|
|
139
|
-
expires_in: expiresIn
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Creates a validation error with detailed information
|
|
145
|
-
* @function createValidationError
|
|
146
|
-
* @param {Object} response - Full API response object
|
|
147
|
-
* @returns {Error} Validation error with formattedError and errorData attached
|
|
148
|
-
*/
|
|
149
|
-
/**
|
|
150
|
-
* Attaches formatted error to validation error
|
|
151
|
-
* @function attachFormattedError
|
|
152
|
-
* @param {Error} validationError - Validation error object
|
|
153
|
-
* @param {Object} response - API response
|
|
154
|
-
*/
|
|
155
|
-
function attachFormattedError(validationError, response) {
|
|
156
|
-
if (response && response.formattedError) {
|
|
157
|
-
validationError.formattedError = response.formattedError;
|
|
158
|
-
validationError.message = `Token polling failed:\n${response.formattedError}`;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Builds detailed error message from error data
|
|
164
|
-
* @function buildDetailedErrorMessage
|
|
165
|
-
* @param {Object} errorData - Error data object
|
|
166
|
-
* @returns {string} Detailed error message
|
|
167
|
-
*/
|
|
168
|
-
function buildDetailedErrorMessage(errorData) {
|
|
169
|
-
const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
|
|
170
|
-
let errorMsg = `Token polling failed: ${detail}`;
|
|
171
|
-
|
|
172
|
-
if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
|
173
|
-
errorMsg += '\n\nValidation errors:';
|
|
174
|
-
errorData.errors.forEach(err => {
|
|
175
|
-
const field = err.field || err.path || 'validation';
|
|
176
|
-
const message = err.message || 'Invalid value';
|
|
177
|
-
if (field === 'validation' || field === 'unknown') {
|
|
178
|
-
errorMsg += `\n • ${message}`;
|
|
179
|
-
} else {
|
|
180
|
-
errorMsg += `\n • ${field}: ${message}`;
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return errorMsg;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Attaches error data to validation error
|
|
190
|
-
* @function attachErrorData
|
|
191
|
-
* @param {Error} validationError - Validation error object
|
|
192
|
-
* @param {Object} response - API response
|
|
193
|
-
*/
|
|
194
|
-
function attachErrorData(validationError, response) {
|
|
195
|
-
if (response && response.errorData) {
|
|
196
|
-
validationError.errorData = response.errorData;
|
|
197
|
-
validationError.errorType = response.errorType || 'validation';
|
|
198
|
-
|
|
199
|
-
if (!validationError.formattedError) {
|
|
200
|
-
validationError.message = buildDetailedErrorMessage(response.errorData);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function createValidationError(response) {
|
|
206
|
-
const validationError = new Error('Token polling failed: Validation error');
|
|
207
|
-
|
|
208
|
-
attachFormattedError(validationError, response);
|
|
209
|
-
attachErrorData(validationError, response);
|
|
210
|
-
|
|
211
|
-
return validationError;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Handles polling errors
|
|
216
|
-
* @function handlePollingErrors
|
|
217
|
-
* @param {string} error - Error code
|
|
218
|
-
* @param {number} status - HTTP status code
|
|
219
|
-
* @param {Object} response - Full API response object (for accessing formattedError and errorData)
|
|
220
|
-
* @throws {Error} For fatal errors
|
|
221
|
-
* @returns {boolean} True if should continue polling
|
|
222
|
-
*/
|
|
223
|
-
function handlePollingErrors(error, status, response) {
|
|
224
|
-
if (error === 'authorization_pending' || status === 202) {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check error field first, then status code
|
|
229
|
-
if (error === 'authorization_declined') {
|
|
230
|
-
throw new Error('Authorization declined: User denied the request');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (error === 'expired_token' || status === 410) {
|
|
234
|
-
throw new Error('Device code expired: Please restart the authentication process');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (error === 'slow_down') {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Handle validation errors with detailed message
|
|
242
|
-
// Check for validation_error, status 400, or specific validation error codes
|
|
243
|
-
if (error === 'validation_error' || status === 400 ||
|
|
244
|
-
error === 'INVALID_TOKEN' || error === 'INVALID_ACCESS_TOKEN') {
|
|
245
|
-
throw createValidationError(response);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
throw new Error(`Token polling failed: ${error}`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Waits for next polling interval
|
|
253
|
-
* @async
|
|
254
|
-
* @function waitForNextPoll
|
|
255
|
-
* @param {number} interval - Polling interval in seconds
|
|
256
|
-
* @param {boolean} slowDown - Whether to slow down
|
|
257
|
-
*/
|
|
258
|
-
async function waitForNextPoll(interval, slowDown) {
|
|
259
|
-
const waitInterval = slowDown ? interval * 2 : interval;
|
|
260
|
-
await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Extracts error from API response
|
|
265
|
-
* @param {Object} response - API response object
|
|
266
|
-
* @returns {string} Error code or 'Unknown error'
|
|
267
|
-
*/
|
|
268
|
-
/**
|
|
269
|
-
* Checks if error code indicates validation error
|
|
270
|
-
* @function isValidationErrorCode
|
|
271
|
-
* @param {string} errorCode - Error code to check
|
|
272
|
-
* @returns {boolean} True if validation error
|
|
273
|
-
*/
|
|
274
|
-
function isValidationErrorCode(errorCode) {
|
|
275
|
-
return errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN';
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Extracts error from structured error data
|
|
280
|
-
* @function extractStructuredError
|
|
281
|
-
* @param {Object} response - API response with errorData
|
|
282
|
-
* @returns {string} Error message or code
|
|
283
|
-
*/
|
|
284
|
-
function extractStructuredError(response) {
|
|
285
|
-
const errorData = response.errorData;
|
|
286
|
-
|
|
287
|
-
// For validation errors, return the error type so we can handle it specially
|
|
288
|
-
if (response.errorType === 'validation') {
|
|
289
|
-
return 'validation_error';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Check if error code indicates validation error (e.g., INVALID_TOKEN)
|
|
293
|
-
const errorCode = errorData.error || errorData.code || response.error;
|
|
294
|
-
if (isValidationErrorCode(errorCode)) {
|
|
295
|
-
return 'validation_error';
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Return the error message from structured error
|
|
299
|
-
return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Extracts error from fallback response structure
|
|
304
|
-
* @function extractFallbackError
|
|
305
|
-
* @param {Object} response - API response
|
|
306
|
-
* @returns {string} Error code
|
|
307
|
-
*/
|
|
308
|
-
function extractFallbackError(response) {
|
|
309
|
-
const apiResponse = response.data || {};
|
|
310
|
-
const errorData = typeof apiResponse === 'object' ? apiResponse : {};
|
|
311
|
-
const errorCode = errorData.error || response.error || 'Unknown error';
|
|
312
|
-
|
|
313
|
-
// Check if error code indicates validation error (e.g., INVALID_TOKEN)
|
|
314
|
-
if (isValidationErrorCode(errorCode)) {
|
|
315
|
-
return 'validation_error';
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return errorCode;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function extractPollingError(response) {
|
|
322
|
-
// Check for structured error data first (from api-error-handler)
|
|
323
|
-
if (response.errorData) {
|
|
324
|
-
return extractStructuredError(response);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Fallback to original extraction logic
|
|
328
|
-
return extractFallbackError(response);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Handles successful polling response
|
|
333
|
-
* @param {Object} response - API response object
|
|
334
|
-
* @returns {Object|null} Token response or null if pending
|
|
335
|
-
*/
|
|
336
|
-
function handleSuccessfulPoll(response) {
|
|
337
|
-
const tokenResponse = parseTokenResponse(response);
|
|
338
|
-
if (tokenResponse) {
|
|
339
|
-
return tokenResponse;
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Processes polling response and determines next action
|
|
346
|
-
* @async
|
|
347
|
-
* @function processPollingResponse
|
|
348
|
-
* @param {Object} response - API response object
|
|
349
|
-
* @param {number} interval - Polling interval in seconds
|
|
350
|
-
* @returns {Promise<Object|null>} Token response if complete, null if should continue
|
|
351
|
-
*/
|
|
352
|
-
async function processPollingResponse(response, interval) {
|
|
353
|
-
if (response.success) {
|
|
354
|
-
// Check if response contains an error code even though success is true
|
|
355
|
-
const apiResponse = response.data || {};
|
|
356
|
-
const responseData = apiResponse.data || apiResponse;
|
|
357
|
-
const errorCode = responseData.error || apiResponse.error || response.error;
|
|
358
|
-
|
|
359
|
-
// If there's an error code like INVALID_TOKEN, treat it as a validation error
|
|
360
|
-
if (errorCode && (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN')) {
|
|
361
|
-
throw createValidationError(response);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const tokenResponse = handleSuccessfulPoll(response);
|
|
365
|
-
if (tokenResponse) {
|
|
366
|
-
return tokenResponse;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const error = errorCode;
|
|
370
|
-
const slowDown = error === 'slow_down';
|
|
371
|
-
await waitForNextPoll(interval, slowDown);
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const error = extractPollingError(response);
|
|
376
|
-
const shouldContinue = handlePollingErrors(error, response.status, response);
|
|
377
|
-
|
|
378
|
-
if (shouldContinue) {
|
|
379
|
-
const slowDown = error === 'slow_down';
|
|
380
|
-
await waitForNextPoll(interval, slowDown);
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
67
|
/**
|
|
388
68
|
* Polls for token during Device Code Flow
|
|
389
69
|
* Continuously polls the token endpoint until user approves or flow expires
|
|
@@ -431,27 +111,47 @@ async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresI
|
|
|
431
111
|
}
|
|
432
112
|
}
|
|
433
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Builds verification URL with user_code query parameter so the device page can pre-fill the code.
|
|
116
|
+
*
|
|
117
|
+
* @function buildVerificationUrlWithUserCode
|
|
118
|
+
* @param {string} verificationUri - Base verification URL (e.g. http://localhost:8182/realms/aifabrix/device)
|
|
119
|
+
* @param {string} userCode - User code to append as user_code query param
|
|
120
|
+
* @returns {string} Full URL with ?user_code=<code> or &user_code=<code>
|
|
121
|
+
*/
|
|
122
|
+
function buildVerificationUrlWithUserCode(verificationUri, userCode) {
|
|
123
|
+
if (!verificationUri || !userCode) {
|
|
124
|
+
return verificationUri || '';
|
|
125
|
+
}
|
|
126
|
+
const separator = verificationUri.includes('?') ? '&' : '?';
|
|
127
|
+
return `${verificationUri}${separator}user_code=${encodeURIComponent(userCode)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
434
130
|
/**
|
|
435
131
|
* Displays device code information to the user
|
|
436
|
-
* Formats user code and verification URL for easy reading
|
|
132
|
+
* Formats user code and verification URL for easy reading. Uses a URL with user_code in the query
|
|
133
|
+
* so the device page can pre-fill the code and the user does not need to type it.
|
|
437
134
|
*
|
|
438
135
|
* @function displayDeviceCodeInfo
|
|
439
136
|
* @param {string} userCode - User code to display
|
|
440
|
-
* @param {string} verificationUri - Verification URL
|
|
137
|
+
* @param {string} verificationUri - Verification URL (base, without user_code)
|
|
441
138
|
* @param {Object} logger - Logger instance with log method
|
|
442
139
|
* @param {Object} chalk - Chalk instance for colored output
|
|
443
140
|
*/
|
|
444
141
|
function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
|
|
142
|
+
const visitUrl = buildVerificationUrlWithUserCode(verificationUri, userCode);
|
|
445
143
|
logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
446
144
|
logger.log(chalk.cyan(' Device Code Flow Authentication'));
|
|
447
145
|
logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
448
146
|
logger.log(chalk.yellow('To complete authentication:'));
|
|
449
|
-
logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(
|
|
450
|
-
logger.log(chalk.gray(' 2.
|
|
451
|
-
logger.log(chalk.gray(' 3. Approve the request\n'));
|
|
147
|
+
logger.log(chalk.gray(' 1. Visit (code is in the URL): ') + chalk.blue.underline(visitUrl));
|
|
148
|
+
logger.log(chalk.gray(' 2. Approve the request\n'));
|
|
452
149
|
logger.log(chalk.gray('Waiting for approval...'));
|
|
453
150
|
}
|
|
454
151
|
|
|
152
|
+
/** Timeout for token refresh request (ms). Longer than default to allow for slow controller/Keycloak. */
|
|
153
|
+
const REFRESH_TOKEN_TIMEOUT_MS = 60000;
|
|
154
|
+
|
|
455
155
|
/**
|
|
456
156
|
* Refresh device code access token using refresh token
|
|
457
157
|
* Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
|
|
@@ -469,13 +169,13 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
469
169
|
}
|
|
470
170
|
|
|
471
171
|
const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
|
|
472
|
-
// Send both refresh_token (OAuth2 RFC 6749 / Keycloak) and refreshToken (camelCase) so controller accepts either
|
|
473
172
|
const response = await getMakeApiCall()(url, {
|
|
474
173
|
method: 'POST',
|
|
475
174
|
headers: {
|
|
476
175
|
'Content-Type': 'application/json'
|
|
477
176
|
},
|
|
478
|
-
body: JSON.stringify({ refresh_token: refreshToken, refreshToken })
|
|
177
|
+
body: JSON.stringify({ refresh_token: refreshToken, refreshToken }),
|
|
178
|
+
signal: AbortSignal.timeout(REFRESH_TOKEN_TIMEOUT_MS)
|
|
479
179
|
});
|
|
480
180
|
|
|
481
181
|
if (!response.success) {
|
|
@@ -483,7 +183,6 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
483
183
|
throw new Error(`Failed to refresh token: ${errorMsg}`);
|
|
484
184
|
}
|
|
485
185
|
|
|
486
|
-
// Parse response using existing parseTokenResponse function
|
|
487
186
|
const tokenResponse = parseTokenResponse(response);
|
|
488
187
|
if (!tokenResponse) {
|
|
489
188
|
throw new Error('Invalid refresh token response');
|
|
@@ -491,10 +190,12 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
491
190
|
|
|
492
191
|
return tokenResponse;
|
|
493
192
|
}
|
|
193
|
+
|
|
494
194
|
module.exports = {
|
|
495
195
|
initiateDeviceCodeFlow,
|
|
496
196
|
pollDeviceCodeToken,
|
|
497
197
|
displayDeviceCodeInfo,
|
|
498
198
|
refreshDeviceToken,
|
|
499
|
-
parseTokenResponse
|
|
199
|
+
parseTokenResponse,
|
|
200
|
+
buildVerificationUrlWithUserCode
|
|
500
201
|
};
|
|
@@ -95,9 +95,19 @@ function handleDockerClose(code, ctx) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function runDockerBuildProcess(buildOpts) {
|
|
98
|
-
const { imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject } = buildOpts;
|
|
99
|
-
const
|
|
100
|
-
|
|
98
|
+
const { imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject, env = {}, buildArgs = {} } = buildOpts;
|
|
99
|
+
const spawnEnv = { ...process.env, ...env };
|
|
100
|
+
const args = ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath];
|
|
101
|
+
// Pass NPM_TOKEN/PYPI_TOKEN etc. so private registry auth works during RUN npm install / pip install
|
|
102
|
+
for (const [key, value] of Object.entries(buildArgs)) {
|
|
103
|
+
if (value !== null && value !== undefined && String(value).length > 0) {
|
|
104
|
+
args.push('--build-arg', `${key}=${String(value)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
args.push(contextPath);
|
|
108
|
+
const dockerProcess = spawn('docker', args, {
|
|
109
|
+
shell: process.platform === 'win32',
|
|
110
|
+
env: spawnEnv
|
|
101
111
|
});
|
|
102
112
|
let stdoutBuffer = '';
|
|
103
113
|
let stderrBuffer = '';
|
|
@@ -138,13 +148,15 @@ function runDockerBuildProcess(buildOpts) {
|
|
|
138
148
|
* @param {string} dockerfilePath - Path to Dockerfile
|
|
139
149
|
* @param {string} contextPath - Build context path
|
|
140
150
|
* @param {string} tag - Image tag
|
|
151
|
+
* @param {Object} [buildArgs={}] - Optional build args (e.g. NPM_TOKEN, PYPI_TOKEN) for private registries
|
|
141
152
|
* @returns {Promise<void>} Resolves when build completes
|
|
142
153
|
* @throws {Error} If build fails
|
|
143
154
|
*/
|
|
144
|
-
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
155
|
+
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {}) {
|
|
145
156
|
const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
|
|
146
157
|
const fsSync = require('fs');
|
|
147
158
|
const path = require('path');
|
|
159
|
+
const { getRemoteDockerEnv } = require('./remote-docker-env');
|
|
148
160
|
dockerfilePath = path.resolve(dockerfilePath);
|
|
149
161
|
contextPath = path.resolve(contextPath);
|
|
150
162
|
|
|
@@ -162,8 +174,21 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
176
|
|
|
177
|
+
const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
|
|
178
|
+
const remoteEnv = isTest ? {} : await getRemoteDockerEnv();
|
|
179
|
+
const resolvedBuildArgs = buildArgs && typeof buildArgs === 'object' ? buildArgs : {};
|
|
165
180
|
return new Promise((resolve, reject) => {
|
|
166
|
-
runDockerBuildProcess({
|
|
181
|
+
runDockerBuildProcess({
|
|
182
|
+
imageName,
|
|
183
|
+
tag,
|
|
184
|
+
dockerfilePath,
|
|
185
|
+
contextPath,
|
|
186
|
+
spinner,
|
|
187
|
+
resolve,
|
|
188
|
+
reject,
|
|
189
|
+
env: remoteEnv,
|
|
190
|
+
buildArgs: resolvedBuildArgs
|
|
191
|
+
});
|
|
167
192
|
});
|
|
168
193
|
}
|
|
169
194
|
|
|
@@ -179,14 +204,18 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
|
179
204
|
* @throws {Error} If build fails
|
|
180
205
|
*/
|
|
181
206
|
async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
|
|
182
|
-
|
|
207
|
+
const buildArgs = (options && options.buildArgs) || {};
|
|
208
|
+
await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs);
|
|
183
209
|
|
|
184
210
|
// Tag image if additional tag provided
|
|
185
211
|
if (options && options.tag && options.tag !== 'latest') {
|
|
186
212
|
const { promisify } = require('util');
|
|
187
213
|
const { exec } = require('child_process');
|
|
188
214
|
const run = promisify(exec);
|
|
189
|
-
|
|
215
|
+
const { getRemoteDockerEnv } = require('./remote-docker-env');
|
|
216
|
+
const remoteEnv = await getRemoteDockerEnv();
|
|
217
|
+
const env = { ...process.env, ...remoteEnv };
|
|
218
|
+
await run(`docker tag ${imageName}:${tag} ${imageName}:latest`, { env });
|
|
190
219
|
}
|
|
191
220
|
}
|
|
192
221
|
|
|
@@ -215,7 +244,10 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
|
|
|
215
244
|
const { promisify } = require('util');
|
|
216
245
|
const { exec } = require('child_process');
|
|
217
246
|
const run = promisify(exec);
|
|
218
|
-
|
|
247
|
+
const { getRemoteDockerEnv } = require('./remote-docker-env');
|
|
248
|
+
const remoteEnv = await getRemoteDockerEnv();
|
|
249
|
+
const env = { ...process.env, ...remoteEnv };
|
|
250
|
+
await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`, { env });
|
|
219
251
|
logger.log(chalk.green(`✓ Tagged image: ${imageName}:${tag}`));
|
|
220
252
|
} catch (err) {
|
|
221
253
|
logger.log(chalk.yellow(`⚠️ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
|
package/lib/utils/env-copy.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const fsp = require('fs').promises;
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const yaml = require('js-yaml');
|
|
14
15
|
const chalk = require('chalk');
|
|
@@ -72,6 +73,44 @@ function resolveEnvOutputPath(rawOutputPath, variablesPath) {
|
|
|
72
73
|
return outputPath;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Writes .env to envOutputPath for reload path: merge run .env into existing file.
|
|
78
|
+
* @async
|
|
79
|
+
* @param {string} outputPath - Resolved output path
|
|
80
|
+
* @param {string} runEnvPath - Path to .env.run
|
|
81
|
+
*/
|
|
82
|
+
async function writeEnvOutputForReload(outputPath, runEnvPath) {
|
|
83
|
+
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
84
|
+
const runContent = await fsp.readFile(runEnvPath, 'utf8');
|
|
85
|
+
const runMap = parseEnvContentToMap(runContent);
|
|
86
|
+
let toWrite = runContent;
|
|
87
|
+
if (fs.existsSync(outputPath)) {
|
|
88
|
+
const existingContent = await fsp.readFile(outputPath, 'utf8');
|
|
89
|
+
toWrite = mergeEnvMapIntoContent(existingContent, runMap);
|
|
90
|
+
}
|
|
91
|
+
await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
|
|
92
|
+
logger.log(chalk.green(`✓ Wrote .env to envOutputPath (same as container, for --reload): ${outputPath}`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Writes local .env to envOutputPath (no reload).
|
|
97
|
+
* @async
|
|
98
|
+
* @param {string} appName - Application name
|
|
99
|
+
* @param {string} outputPath - Resolved output path
|
|
100
|
+
*/
|
|
101
|
+
async function writeEnvOutputForLocal(appName, outputPath) {
|
|
102
|
+
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
103
|
+
const localContent = await generateEnvContent(appName, null, 'local', false);
|
|
104
|
+
let toWrite = localContent;
|
|
105
|
+
if (fs.existsSync(outputPath)) {
|
|
106
|
+
const existingContent = await fsp.readFile(outputPath, 'utf8');
|
|
107
|
+
const localMap = parseEnvContentToMap(localContent);
|
|
108
|
+
toWrite = mergeEnvMapIntoContent(existingContent, localMap);
|
|
109
|
+
}
|
|
110
|
+
await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
|
|
111
|
+
logger.log(chalk.green(`✓ Wrote .env to envOutputPath (localPort): ${outputPath}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
75
114
|
/**
|
|
76
115
|
* Calculate developer-specific app port
|
|
77
116
|
* @param {number} baseAppPort - Base application port
|
|
@@ -180,6 +219,42 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
180
219
|
return envContent;
|
|
181
220
|
}
|
|
182
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Write regenerated local .env to output path (merge with existing if present).
|
|
224
|
+
* @async
|
|
225
|
+
* @param {string} outputPath - Resolved output path
|
|
226
|
+
* @param {string} appName - Application name
|
|
227
|
+
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
228
|
+
* @param {string} envOutputPathLabel - Label for log message (e.g. variables.build.envOutputPath)
|
|
229
|
+
*/
|
|
230
|
+
async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel) {
|
|
231
|
+
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
232
|
+
const localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
|
|
233
|
+
let toWrite = localEnvContent;
|
|
234
|
+
if (fs.existsSync(outputPath)) {
|
|
235
|
+
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
236
|
+
const localMap = parseEnvContentToMap(localEnvContent);
|
|
237
|
+
toWrite = mergeEnvMapIntoContent(existingContent, localMap);
|
|
238
|
+
}
|
|
239
|
+
fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
|
|
240
|
+
logger.log(chalk.green(`✓ Generated local .env at: ${envOutputPathLabel}`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Write patched .env to output path (fallback when appName not provided).
|
|
245
|
+
* @async
|
|
246
|
+
* @param {string} envPath - Path to generated .env file
|
|
247
|
+
* @param {string} outputPath - Resolved output path
|
|
248
|
+
* @param {Object} variables - Loaded variables config
|
|
249
|
+
* @param {string} envOutputPathLabel - Label for log message
|
|
250
|
+
*/
|
|
251
|
+
async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
|
|
252
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
253
|
+
const patchedContent = await patchEnvContentForLocal(envContent, variables);
|
|
254
|
+
fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
|
|
255
|
+
logger.log(chalk.green(`✓ Copied .env to: ${envOutputPathLabel}`));
|
|
256
|
+
}
|
|
257
|
+
|
|
183
258
|
/**
|
|
184
259
|
* Process and optionally copy env file to envOutputPath if configured
|
|
185
260
|
* Regenerates .env file with env=local for local development (apps/.env)
|
|
@@ -191,7 +266,7 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
191
266
|
* @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
|
|
192
267
|
*/
|
|
193
268
|
async function processEnvVariables(envPath, variablesPath, appName, secretsPath) {
|
|
194
|
-
if (!fs.existsSync(variablesPath)) {
|
|
269
|
+
if (!variablesPath || !fs.existsSync(variablesPath)) {
|
|
195
270
|
return;
|
|
196
271
|
}
|
|
197
272
|
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
@@ -200,29 +275,24 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
|
|
|
200
275
|
return;
|
|
201
276
|
}
|
|
202
277
|
|
|
203
|
-
// Resolve output path
|
|
204
278
|
const outputPath = resolveEnvOutputPath(variables.build.envOutputPath, variablesPath);
|
|
205
279
|
const outputDir = path.dirname(outputPath);
|
|
206
280
|
if (!fs.existsSync(outputDir)) {
|
|
207
281
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
208
282
|
}
|
|
209
283
|
|
|
210
|
-
|
|
284
|
+
const label = variables.build.envOutputPath;
|
|
211
285
|
if (appName) {
|
|
212
|
-
|
|
213
|
-
const localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
|
|
214
|
-
fs.writeFileSync(outputPath, localEnvContent, { mode: 0o600 });
|
|
215
|
-
logger.log(chalk.green(`✓ Generated local .env at: ${variables.build.envOutputPath}`));
|
|
286
|
+
await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label);
|
|
216
287
|
} else {
|
|
217
|
-
|
|
218
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
219
|
-
const patchedContent = await patchEnvContentForLocal(envContent, variables);
|
|
220
|
-
fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
|
|
221
|
-
logger.log(chalk.green(`✓ Copied .env to: ${variables.build.envOutputPath}`));
|
|
288
|
+
await writePatchedEnvToOutputPath(envPath, outputPath, variables, label);
|
|
222
289
|
}
|
|
223
290
|
}
|
|
224
291
|
|
|
225
292
|
module.exports = {
|
|
226
|
-
processEnvVariables
|
|
293
|
+
processEnvVariables,
|
|
294
|
+
resolveEnvOutputPath,
|
|
295
|
+
writeEnvOutputForReload,
|
|
296
|
+
writeEnvOutputForLocal
|
|
227
297
|
};
|
|
228
298
|
|