@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/paths.js
CHANGED
|
@@ -158,14 +158,8 @@ function getFallbackProjectRoot() {
|
|
|
158
158
|
return path.resolve(__dirname, '..', '..');
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
* Gets the project root directory by finding package.json
|
|
163
|
-
* Works reliably in all environments including Jest tests and CI
|
|
164
|
-
* @returns {string} Absolute path to project root
|
|
165
|
-
*/
|
|
166
161
|
/**
|
|
167
162
|
* Checks if global PROJECT_ROOT is valid
|
|
168
|
-
* @function checkGlobalProjectRoot
|
|
169
163
|
* @returns {string|null} Valid global root or null
|
|
170
164
|
*/
|
|
171
165
|
function checkGlobalProjectRoot() {
|
|
@@ -178,6 +172,12 @@ function checkGlobalProjectRoot() {
|
|
|
178
172
|
return null;
|
|
179
173
|
}
|
|
180
174
|
|
|
175
|
+
// In test environment, allow temp dir as project root (Jest sets NODE_ENV=test / JEST_WORKER_ID)
|
|
176
|
+
const isTestEnv = process.env.NODE_ENV === 'test' || typeof process.env.JEST_WORKER_ID !== 'undefined';
|
|
177
|
+
if (isTestEnv) {
|
|
178
|
+
return globalRoot;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
181
|
// Verify that __dirname is actually within globalRoot
|
|
182
182
|
const dirnameNormalized = path.resolve(__dirname);
|
|
183
183
|
const globalRootNormalized = path.resolve(globalRoot);
|
|
@@ -189,39 +189,29 @@ function checkGlobalProjectRoot() {
|
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* Tries different strategies to find project root
|
|
192
|
-
* @function tryFindProjectRoot
|
|
193
192
|
* @returns {string} Found project root
|
|
194
193
|
*/
|
|
195
194
|
function tryFindProjectRoot() {
|
|
196
|
-
// Strategy 1: Check global.PROJECT_ROOT
|
|
197
195
|
const globalRoot = checkGlobalProjectRoot();
|
|
198
196
|
if (globalRoot) {
|
|
199
197
|
cachedProjectRoot = globalRoot;
|
|
200
198
|
return cachedProjectRoot;
|
|
201
199
|
}
|
|
202
|
-
|
|
203
|
-
// Strategy 2: Walk up from __dirname
|
|
204
200
|
const foundRoot = findProjectRootByWalkingUp(__dirname);
|
|
205
201
|
if (foundRoot && hasPackageJson(foundRoot)) {
|
|
206
202
|
cachedProjectRoot = foundRoot;
|
|
207
203
|
return cachedProjectRoot;
|
|
208
204
|
}
|
|
209
|
-
|
|
210
|
-
// Strategy 3: Try process.cwd()
|
|
211
205
|
const cwdRoot = findProjectRootFromCwd();
|
|
212
206
|
if (cwdRoot && hasPackageJson(cwdRoot)) {
|
|
213
207
|
cachedProjectRoot = cwdRoot;
|
|
214
208
|
return cachedProjectRoot;
|
|
215
209
|
}
|
|
216
|
-
|
|
217
|
-
// Strategy 4: Fallback
|
|
218
210
|
const fallbackRoot = getFallbackProjectRoot();
|
|
219
211
|
if (hasPackageJson(fallbackRoot)) {
|
|
220
212
|
cachedProjectRoot = fallbackRoot;
|
|
221
213
|
return cachedProjectRoot;
|
|
222
214
|
}
|
|
223
|
-
|
|
224
|
-
// Last resort
|
|
225
215
|
cachedProjectRoot = fallbackRoot;
|
|
226
216
|
return cachedProjectRoot;
|
|
227
217
|
}
|
|
@@ -236,10 +226,7 @@ function getProjectRoot() {
|
|
|
236
226
|
}
|
|
237
227
|
|
|
238
228
|
/**
|
|
239
|
-
* Returns the applications base directory
|
|
240
|
-
* Dev 0: <home>/applications
|
|
241
|
-
* Dev > 0: <home>/applications-dev-{id}
|
|
242
|
-
*
|
|
229
|
+
* Returns the applications base directory. Dev 0: <home>/applications; Dev > 0: <home>/applications-dev-{id}
|
|
243
230
|
* @param {number|string} developerId - Developer ID
|
|
244
231
|
* @returns {string} Absolute path to applications base directory
|
|
245
232
|
*/
|
|
@@ -253,19 +240,13 @@ function getApplicationsBaseDir(developerId) {
|
|
|
253
240
|
}
|
|
254
241
|
|
|
255
242
|
/**
|
|
256
|
-
* Returns the developer-specific application directory.
|
|
257
|
-
* Dev 0: points to applications/ (root)
|
|
258
|
-
* Dev > 0: <home>/applications-dev-{id} (root)
|
|
259
|
-
*
|
|
243
|
+
* Returns the developer-specific application directory. Dev 0: applications/; Dev > 0: applications-dev-{id}
|
|
260
244
|
* @param {string} appName - Application name
|
|
261
245
|
* @param {number|string} developerId - Developer ID
|
|
262
|
-
* @returns {string}
|
|
246
|
+
* @returns {string} Developer-specific app directory (root)
|
|
263
247
|
*/
|
|
264
248
|
function getDevDirectory(appName, developerId) {
|
|
265
249
|
const baseDir = getApplicationsBaseDir(developerId);
|
|
266
|
-
// All files should be generated at the root of the applications folder
|
|
267
|
-
// Dev 0: <home>/applications
|
|
268
|
-
// Dev > 0: <home>/applications-dev-{id}
|
|
269
250
|
return baseDir;
|
|
270
251
|
}
|
|
271
252
|
|
|
@@ -286,7 +267,6 @@ function getAppPath(appName, appType) {
|
|
|
286
267
|
|
|
287
268
|
/**
|
|
288
269
|
* Base directory for integration/builder: project root when cwd is inside project, else cwd.
|
|
289
|
-
* So deploy works when run from integration/<app> (e.g. node deploy.js), and tests using temp dirs still work.
|
|
290
270
|
* @returns {string} Directory to resolve integration/ and builder/ from
|
|
291
271
|
*/
|
|
292
272
|
function getIntegrationBuilderBaseDir() {
|
|
@@ -301,7 +281,6 @@ function getIntegrationBuilderBaseDir() {
|
|
|
301
281
|
|
|
302
282
|
/**
|
|
303
283
|
* Gets the integration folder path for external systems.
|
|
304
|
-
* Uses project root when cwd is inside project so deploy works when run from integration/<app> (e.g. node deploy.js).
|
|
305
284
|
* @param {string} appName - Application name
|
|
306
285
|
* @returns {string} Absolute path to integration directory
|
|
307
286
|
*/
|
|
@@ -314,9 +293,21 @@ function getIntegrationPath(appName) {
|
|
|
314
293
|
}
|
|
315
294
|
|
|
316
295
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
296
|
+
* Resolves build.context from application.yaml to an absolute path.
|
|
297
|
+
* Used as the canonical app code directory for local mount and (when remote) Mutagen local path.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} configDir - Directory containing the application config (e.g. builder/<appKey>/)
|
|
300
|
+
* @param {string} [buildContext='.'] - build.context value (relative to configDir)
|
|
301
|
+
* @returns {string} Absolute path to the app code directory
|
|
302
|
+
*/
|
|
303
|
+
function resolveBuildContext(configDir, buildContext) {
|
|
304
|
+
const dir = (configDir && typeof configDir === 'string') ? configDir : '';
|
|
305
|
+
const ctx = (buildContext && typeof buildContext === 'string') ? buildContext : '.';
|
|
306
|
+
return path.resolve(dir, ctx);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Gets the builder folder path. Uses AIFABRIX_BUILDER_DIR when set, else project root.
|
|
320
311
|
* @param {string} appName - Application name
|
|
321
312
|
* @returns {string} Absolute path to builder directory
|
|
322
313
|
*/
|
|
@@ -456,6 +447,29 @@ async function detectAppType(appName, _options = {}) {
|
|
|
456
447
|
if (builderResult) return builderResult;
|
|
457
448
|
throw new Error(`App '${appName}' not found in integration/${appName} or builder/${appName}`);
|
|
458
449
|
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Resolve-specific app path: prefer integration + env.template only (env-only mode).
|
|
453
|
+
* If integration/<appName>/env.template exists, use that directory without requiring application.yaml.
|
|
454
|
+
* Otherwise fall back to detectAppType (integration or builder with full config).
|
|
455
|
+
*
|
|
456
|
+
* @param {string} appName - Application name
|
|
457
|
+
* @returns {Promise<{appPath: string, envOnly: boolean}>} appPath and envOnly (true when only env.template is used)
|
|
458
|
+
* @throws {Error} When app not found in integration or builder
|
|
459
|
+
*/
|
|
460
|
+
async function getResolveAppPath(appName) {
|
|
461
|
+
if (!appName || typeof appName !== 'string') {
|
|
462
|
+
throw new Error('App name is required and must be a string');
|
|
463
|
+
}
|
|
464
|
+
const integrationPath = getIntegrationPath(appName);
|
|
465
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
466
|
+
if (fs.existsSync(integrationPath) && fs.existsSync(envTemplatePath)) {
|
|
467
|
+
return { appPath: integrationPath, envOnly: true };
|
|
468
|
+
}
|
|
469
|
+
const result = await detectAppType(appName);
|
|
470
|
+
return { appPath: result.appPath, envOnly: false };
|
|
471
|
+
}
|
|
472
|
+
|
|
459
473
|
module.exports = {
|
|
460
474
|
getAifabrixHome,
|
|
461
475
|
getConfigDirForPaths,
|
|
@@ -465,9 +479,11 @@ module.exports = {
|
|
|
465
479
|
getProjectRoot,
|
|
466
480
|
getIntegrationPath,
|
|
467
481
|
getBuilderPath,
|
|
482
|
+
resolveBuildContext,
|
|
468
483
|
getDeployJsonPath,
|
|
469
484
|
resolveApplicationConfigPath,
|
|
470
485
|
detectAppType,
|
|
486
|
+
getResolveAppPath,
|
|
471
487
|
clearProjectRootCache
|
|
472
488
|
};
|
|
473
489
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Use getContainerPort for container/Docker/deployment/registration; use getLocalPort
|
|
6
6
|
* for local .env and dev-id–adjusted host port.
|
|
7
7
|
*
|
|
8
|
-
* @fileoverview Port resolution from variables (port, build.containerPort
|
|
8
|
+
* @fileoverview Port resolution from variables (port, build.containerPort)
|
|
9
9
|
* @author AI Fabrix Team
|
|
10
10
|
* @version 2.0.0
|
|
11
11
|
*/
|
|
@@ -17,8 +17,8 @@ const yaml = require('js-yaml');
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolve container port from variables object.
|
|
20
|
-
* Precedence: build.containerPort → port → defaultPort.
|
|
21
|
-
*
|
|
20
|
+
* Precedence: build.containerPort (if set and non-empty) → port (main port) → defaultPort.
|
|
21
|
+
* When containerPort is empty or missing, main port is used (e.g. keycloak 8082:8080 vs miso 3000:3000).
|
|
22
22
|
*
|
|
23
23
|
* @param {Object} variables - Parsed application config (or subset with build, port)
|
|
24
24
|
* @param {number} [defaultPort=3000] - Default when neither build.containerPort nor port is set
|
|
@@ -26,13 +26,19 @@ const yaml = require('js-yaml');
|
|
|
26
26
|
*/
|
|
27
27
|
function getContainerPort(variables, defaultPort = 3000) {
|
|
28
28
|
const v = variables || {};
|
|
29
|
-
|
|
29
|
+
const containerPort = v.build?.containerPort;
|
|
30
|
+
const useMain = containerPort === undefined || containerPort === null ||
|
|
31
|
+
(typeof containerPort === 'string' && containerPort.trim() === '');
|
|
32
|
+
if (!useMain && typeof containerPort === 'number' && containerPort > 0) {
|
|
33
|
+
return containerPort;
|
|
34
|
+
}
|
|
35
|
+
return v.port ?? defaultPort;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
39
|
* Resolve local (development) port from variables object.
|
|
34
|
-
* Precedence: build.localPort (
|
|
35
|
-
* Used for
|
|
40
|
+
* Precedence: build.localPort (when positive integer) → port → defaultPort.
|
|
41
|
+
* Used for env-copy, env-ports, getLocalPortFromPath, run compose host port.
|
|
36
42
|
*
|
|
37
43
|
* @param {Object} variables - Parsed application config
|
|
38
44
|
* @param {number} [defaultPort=3000] - Default when neither build.localPort nor port is set
|
|
@@ -40,9 +46,9 @@ function getContainerPort(variables, defaultPort = 3000) {
|
|
|
40
46
|
*/
|
|
41
47
|
function getLocalPort(variables, defaultPort = 3000) {
|
|
42
48
|
const v = variables || {};
|
|
43
|
-
const
|
|
44
|
-
if (typeof
|
|
45
|
-
return
|
|
49
|
+
const localPort = v.build?.localPort;
|
|
50
|
+
if (typeof localPort === 'number' && localPort > 0) {
|
|
51
|
+
return localPort;
|
|
46
52
|
}
|
|
47
53
|
return v.port ?? defaultPort;
|
|
48
54
|
}
|
|
@@ -67,7 +73,7 @@ function loadVariablesFromPath(variablesPath) {
|
|
|
67
73
|
|
|
68
74
|
/**
|
|
69
75
|
* Resolve container port from application config path.
|
|
70
|
-
*
|
|
76
|
+
* When containerPort is empty or missing, returns main port. Null only when file missing or no port set.
|
|
71
77
|
*
|
|
72
78
|
* @param {string} variablesPath - Path to application config
|
|
73
79
|
* @returns {number|null} Container port or null
|
|
@@ -77,14 +83,20 @@ function getContainerPortFromPath(variablesPath) {
|
|
|
77
83
|
if (!v) {
|
|
78
84
|
return null;
|
|
79
85
|
}
|
|
80
|
-
const
|
|
86
|
+
const containerPort = v.build?.containerPort;
|
|
87
|
+
const useMain = containerPort === undefined || containerPort === null ||
|
|
88
|
+
(typeof containerPort === 'string' && containerPort.trim() === '');
|
|
89
|
+
if (!useMain && typeof containerPort === 'number' && containerPort > 0) {
|
|
90
|
+
return containerPort;
|
|
91
|
+
}
|
|
92
|
+
const p = v.port;
|
|
81
93
|
return (p !== undefined && p !== null) ? p : null;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/**
|
|
85
97
|
* Resolve local port from application config path.
|
|
86
|
-
*
|
|
87
|
-
* Returns null when file is missing or neither
|
|
98
|
+
* Same rule as getLocalPort: build.localPort (when positive integer) → port.
|
|
99
|
+
* Returns null when file is missing or neither localPort nor port is set.
|
|
88
100
|
*
|
|
89
101
|
* @param {string} variablesPath - Path to application config
|
|
90
102
|
* @returns {number|null} Local port or null
|
|
@@ -94,9 +106,9 @@ function getLocalPortFromPath(variablesPath) {
|
|
|
94
106
|
if (!v) {
|
|
95
107
|
return null;
|
|
96
108
|
}
|
|
97
|
-
const
|
|
98
|
-
if (typeof
|
|
99
|
-
return
|
|
109
|
+
const localPort = v.build?.localPort;
|
|
110
|
+
if (typeof localPort === 'number' && localPort > 0) {
|
|
111
|
+
return localPort;
|
|
100
112
|
}
|
|
101
113
|
const p = v.port;
|
|
102
114
|
return (p !== undefined && p !== null) ? p : null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Resolve Builder Server URL and client cert for cert-authenticated dev API calls
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const config = require('../core/config');
|
|
8
|
+
const { getCertDir, readClientCertPem } = require('./dev-cert-helper');
|
|
9
|
+
const { getConfigDirForPaths } = require('./paths');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a string is an http(s) URL (for aifabrix-secrets remote mode).
|
|
13
|
+
* @param {string} value - Config value
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function isRemoteSecretsUrl(value) {
|
|
17
|
+
return typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get Builder Server URL and client cert PEM when remote is configured; otherwise null.
|
|
22
|
+
* Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
|
|
23
|
+
* @returns {Promise<{ serverUrl: string, clientCertPem: string }|null>}
|
|
24
|
+
*/
|
|
25
|
+
async function getRemoteDevAuth() {
|
|
26
|
+
const serverUrl = await config.getRemoteServer();
|
|
27
|
+
if (!serverUrl) return null;
|
|
28
|
+
const devId = await config.getDeveloperId();
|
|
29
|
+
const certDir = getCertDir(getConfigDirForPaths(), devId);
|
|
30
|
+
const clientCertPem = readClientCertPem(certDir);
|
|
31
|
+
if (!clientCertPem) return null;
|
|
32
|
+
return { serverUrl, clientCertPem };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
isRemoteSecretsUrl,
|
|
37
|
+
getRemoteDevAuth
|
|
38
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Docker environment – DOCKER_HOST and TLS cert path when docker-endpoint is set.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Env overlay for Docker CLI when using remote Builder Server
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const config = require('../core/config');
|
|
11
|
+
const { getCertDir } = require('./dev-cert-helper');
|
|
12
|
+
const { getConfigDirForPaths } = require('./paths');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* If remote Docker is configured (docker-endpoint + cert.pem, key.pem, and ca.pem present),
|
|
16
|
+
* returns env vars for Docker CLI: DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH.
|
|
17
|
+
* Docker requires ca.pem in DOCKER_CERT_PATH for TLS; if it is missing we return {} so
|
|
18
|
+
* the CLI uses local Docker and avoids "open ca.pem: no such file or directory".
|
|
19
|
+
*
|
|
20
|
+
* @returns {Promise<Object>} Env overlay (may be empty)
|
|
21
|
+
*/
|
|
22
|
+
async function getRemoteDockerEnv() {
|
|
23
|
+
const endpoint = await config.getDockerEndpoint();
|
|
24
|
+
if (!endpoint || typeof endpoint !== 'string' || !endpoint.trim()) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const devId = await config.getDeveloperId();
|
|
28
|
+
const certDir = getCertDir(getConfigDirForPaths(), devId);
|
|
29
|
+
const certPath = path.join(certDir, 'cert.pem');
|
|
30
|
+
const keyPath = path.join(certDir, 'key.pem');
|
|
31
|
+
const caPath = path.join(certDir, 'ca.pem');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath) || !fs.existsSync(caPath)) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
DOCKER_HOST: endpoint.trim(),
|
|
38
|
+
DOCKER_TLS_VERIFY: '1',
|
|
39
|
+
DOCKER_CERT_PATH: certDir
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { getRemoteDockerEnv };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
|
|
3
|
+
* Used for .env resolution only; values are never persisted to disk.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Remote shared secrets loader for .env generation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const config = require('../core/config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetches shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
|
|
14
|
+
* @returns {Promise<Object|null>} Key-value secrets from API or null
|
|
15
|
+
*/
|
|
16
|
+
async function loadRemoteSharedSecrets() {
|
|
17
|
+
const { isRemoteSecretsUrl, getRemoteDevAuth } = require('./remote-dev-auth');
|
|
18
|
+
const devApi = require('../api/dev.api');
|
|
19
|
+
const configSecretsPath = await config.getSecretsPath();
|
|
20
|
+
if (!configSecretsPath || !isRemoteSecretsUrl(configSecretsPath)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const auth = await getRemoteDevAuth();
|
|
24
|
+
if (!auth) return null;
|
|
25
|
+
try {
|
|
26
|
+
const items = await devApi.listSecrets(auth.serverUrl, auth.clientCertPem);
|
|
27
|
+
if (!Array.isArray(items)) return null;
|
|
28
|
+
const obj = {};
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
if (item && typeof item.name === 'string' && item.value !== undefined) {
|
|
31
|
+
obj[item.name] = String(item.value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return obj;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Merges remote shared secrets with user secrets. User wins on same key.
|
|
42
|
+
* @param {Object} userSecrets - User secrets object
|
|
43
|
+
* @param {Object} remoteSecrets - Remote API secrets (key-value)
|
|
44
|
+
* @returns {Object} Merged object
|
|
45
|
+
*/
|
|
46
|
+
function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
|
|
47
|
+
const merged = { ...userSecrets };
|
|
48
|
+
if (!remoteSecrets || typeof remoteSecrets !== 'object') return merged;
|
|
49
|
+
for (const key of Object.keys(remoteSecrets)) {
|
|
50
|
+
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
51
|
+
merged[key] = remoteSecrets[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
loadRemoteSharedSecrets,
|
|
59
|
+
mergeUserWithRemoteSecrets
|
|
60
|
+
};
|
|
@@ -17,6 +17,54 @@ const crypto = require('crypto');
|
|
|
17
17
|
const logger = require('./logger');
|
|
18
18
|
const pathsUtil = require('./paths');
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse key-value pairs from YAML-like lines (last occurrence wins per key).
|
|
22
|
+
* @param {string} content - Raw YAML content
|
|
23
|
+
* @returns {Object} Parsed object
|
|
24
|
+
*/
|
|
25
|
+
function parseYamlKeyValueLines(content) {
|
|
26
|
+
const result = {};
|
|
27
|
+
const keyValueRe = /^\s*([^#:]+):\s*(.*)$/;
|
|
28
|
+
const lines = content.split(/\r?\n/);
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
32
|
+
const m = line.match(keyValueRe);
|
|
33
|
+
if (!m) continue;
|
|
34
|
+
const key = m[1].trim();
|
|
35
|
+
let value = m[2].trim();
|
|
36
|
+
if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"'))) {
|
|
37
|
+
value = value.slice(1, -1).replace(/\\'/g, '\'').replace(/\\"/g, '"');
|
|
38
|
+
}
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse YAML content tolerating duplicate keys (last occurrence wins).
|
|
46
|
+
* Use for secrets files that may have been appended to repeatedly.
|
|
47
|
+
* Tries yaml.load first; on "duplicate key" error falls back to line-by-line parse.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} content - Raw YAML content
|
|
50
|
+
* @returns {Object} Parsed object (last value wins for duplicate keys)
|
|
51
|
+
*/
|
|
52
|
+
function loadYamlTolerantOfDuplicateKeys(content) {
|
|
53
|
+
if (!content || typeof content !== 'string') {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = yaml.load(content);
|
|
58
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const msg = err.message || '';
|
|
61
|
+
if (!msg.includes('duplicate') && !msg.includes('duplicated mapping')) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parseYamlKeyValueLines(content);
|
|
66
|
+
}
|
|
67
|
+
|
|
20
68
|
/**
|
|
21
69
|
* Skips commented or empty lines when scanning env.template
|
|
22
70
|
* @param {string} line - Single line
|
|
@@ -127,7 +175,7 @@ function loadExistingSecrets(resolvedPath) {
|
|
|
127
175
|
|
|
128
176
|
try {
|
|
129
177
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
130
|
-
const secrets =
|
|
178
|
+
const secrets = loadYamlTolerantOfDuplicateKeys(content);
|
|
131
179
|
return typeof secrets === 'object' ? secrets : {};
|
|
132
180
|
} catch (error) {
|
|
133
181
|
logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
|
|
@@ -136,7 +184,7 @@ function loadExistingSecrets(resolvedPath) {
|
|
|
136
184
|
}
|
|
137
185
|
|
|
138
186
|
/**
|
|
139
|
-
* Saves secrets file
|
|
187
|
+
* Saves secrets file (full overwrite). Use appendSecretsToFile to add keys without changing existing content.
|
|
140
188
|
* @function saveSecretsFile
|
|
141
189
|
* @param {string} resolvedPath - Path to secrets file
|
|
142
190
|
* @param {Object} secrets - Secrets object to save
|
|
@@ -158,6 +206,45 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
158
206
|
fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
|
|
159
207
|
}
|
|
160
208
|
|
|
209
|
+
const YAML_DUMP_OPTS = { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false };
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Appends secret keys to the end of the secrets file without modifying existing content (preserves comments and structure).
|
|
213
|
+
* Creates the file if it does not exist. For existing files, new keys are appended.
|
|
214
|
+
* When the file has duplicate keys, use loadExistingSecrets (tolerant parse) to read; last occurrence wins.
|
|
215
|
+
*
|
|
216
|
+
* @function appendSecretsToFile
|
|
217
|
+
* @param {string} resolvedPath - Path to secrets file
|
|
218
|
+
* @param {Object} secrets - Key-value object to append (only these keys are written)
|
|
219
|
+
* @throws {Error} If write fails
|
|
220
|
+
*/
|
|
221
|
+
function appendSecretsToFile(resolvedPath, secrets) {
|
|
222
|
+
if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const dir = path.dirname(resolvedPath);
|
|
226
|
+
if (!fs.existsSync(dir)) {
|
|
227
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const appendContent = yaml.dump(secrets, YAML_DUMP_OPTS);
|
|
231
|
+
|
|
232
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
233
|
+
fs.writeFileSync(resolvedPath, appendContent, { mode: 0o600 });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let existing = '';
|
|
238
|
+
try {
|
|
239
|
+
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
240
|
+
existing = typeof raw === 'string' ? raw : '';
|
|
241
|
+
} catch (err) {
|
|
242
|
+
logger.warn(`Could not read existing secrets file: ${err.message}; appending new keys only.`);
|
|
243
|
+
}
|
|
244
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
245
|
+
fs.writeFileSync(resolvedPath, existing + separator + appendContent, { mode: 0o600 });
|
|
246
|
+
}
|
|
247
|
+
|
|
161
248
|
/**
|
|
162
249
|
* Generates missing secret keys in secrets file
|
|
163
250
|
* Scans env.template for kv:// references and adds missing keys with secure defaults
|
|
@@ -188,8 +275,7 @@ async function generateMissingSecrets(envTemplate, secretsPath) {
|
|
|
188
275
|
newSecrets[key] = generateSecretValue(key);
|
|
189
276
|
}
|
|
190
277
|
|
|
191
|
-
|
|
192
|
-
saveSecretsFile(resolvedPath, updatedSecrets);
|
|
278
|
+
appendSecretsToFile(resolvedPath, newSecrets);
|
|
193
279
|
|
|
194
280
|
logger.log(`✓ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
|
|
195
281
|
return missingKeys;
|
|
@@ -227,11 +313,11 @@ postgres-passwordKeyVault: "admin123"
|
|
|
227
313
|
|
|
228
314
|
# Redis Secrets
|
|
229
315
|
redis-passwordKeyVault: ""
|
|
230
|
-
redis-
|
|
316
|
+
redis-url: "redis://\${REDIS_HOST}:\${REDIS_PORT}"
|
|
231
317
|
|
|
232
318
|
# Keycloak Secrets
|
|
233
319
|
keycloak-admin-passwordKeyVault: "admin123"
|
|
234
|
-
keycloak-
|
|
320
|
+
keycloak-server-url: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
|
|
235
321
|
`;
|
|
236
322
|
|
|
237
323
|
fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
|
|
@@ -240,8 +326,10 @@ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
|
|
|
240
326
|
module.exports = {
|
|
241
327
|
findMissingSecretKeys,
|
|
242
328
|
generateSecretValue,
|
|
329
|
+
loadYamlTolerantOfDuplicateKeys,
|
|
243
330
|
loadExistingSecrets,
|
|
244
331
|
saveSecretsFile,
|
|
332
|
+
appendSecretsToFile,
|
|
245
333
|
generateMissingSecrets,
|
|
246
334
|
createDefaultSecrets
|
|
247
335
|
};
|
|
@@ -166,7 +166,7 @@ function getPortFromLocalEnv(localEnv) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Gets port from application config file (
|
|
169
|
+
* Gets port from application config file (port only). Uses port-resolver.
|
|
170
170
|
* @function getPortFromVariablesFile
|
|
171
171
|
* @param {string} variablesPath - Path to application config
|
|
172
172
|
* @returns {number|null} Port value or null
|
|
@@ -199,7 +199,7 @@ function applyDeveloperIdAdjustment(baseAppPort, devIdNum) {
|
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
201
|
* Calculate application port following override chain and developer-id adjustment
|
|
202
|
-
* Override chain: env-config.yaml → config.yaml → application.yaml
|
|
202
|
+
* Override chain: env-config.yaml → config.yaml → application.yaml port
|
|
203
203
|
* @async
|
|
204
204
|
* @function calculateAppPort
|
|
205
205
|
* @param {string} [variablesPath] - Path to application config
|
|
@@ -212,7 +212,7 @@ async function calculateAppPort(variablesPath, localEnv, envContent, devIdNum) {
|
|
|
212
212
|
// Start with env-config value
|
|
213
213
|
let baseAppPort = getPortFromLocalEnv(localEnv);
|
|
214
214
|
|
|
215
|
-
// Override with application config
|
|
215
|
+
// Override with application config port (strongest)
|
|
216
216
|
const variablesPort = getPortFromVariablesFile(variablesPath);
|
|
217
217
|
if (variablesPort !== null) {
|
|
218
218
|
baseAppPort = variablesPort;
|
|
@@ -247,13 +247,32 @@ function updateLocalhostUrls(content, baseAppPort, appPort) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
250
|
+
* Get the env var name used for PORT in env.template (e.g. PORT=${MISO_PORT} -> MISO_PORT).
|
|
251
|
+
* Used when generating .env for envOutputPath (local, not reload) so we set that var to localPort.
|
|
252
|
+
* @param {string} [variablesPath] - Path to application config (env.template lives in same dir)
|
|
253
|
+
* @returns {string|null} Variable name or null
|
|
254
|
+
*/
|
|
255
|
+
function getPortVarFromEnvTemplatePath(variablesPath) {
|
|
256
|
+
if (!variablesPath || !fs.existsSync(variablesPath)) return null;
|
|
257
|
+
const templatePath = path.join(path.dirname(variablesPath), 'env.template');
|
|
258
|
+
if (!fs.existsSync(templatePath)) return null;
|
|
259
|
+
try {
|
|
260
|
+
const content = fs.readFileSync(templatePath, 'utf8');
|
|
261
|
+
const m = content.match(/^PORT\s*=\s*\$\{([A-Za-z0-9_]+)\}/m);
|
|
262
|
+
return m ? m[1] : null;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Adjust infra-related ports in resolved .env content for local environment.
|
|
270
|
+
* Own case: when we generate .env for envOutputPath (not reload), we use localPort (application.yaml build.localPort or port).
|
|
271
|
+
* Sets PORT and the template port var (e.g. MISO_PORT) to localPort so the generated .env is correct for local use.
|
|
253
272
|
* @async
|
|
254
273
|
* @function adjustLocalEnvPortsInContent
|
|
255
274
|
* @param {string} envContent - Resolved .env content
|
|
256
|
-
* @param {string} [variablesPath] - Path to application config (to read
|
|
275
|
+
* @param {string} [variablesPath] - Path to application config (to read port and template port var)
|
|
257
276
|
* @returns {Promise<string>} Updated content with local ports
|
|
258
277
|
*/
|
|
259
278
|
/**
|
|
@@ -348,6 +367,11 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
348
367
|
updated = await rewriteInfraEndpoints(updated, 'local');
|
|
349
368
|
|
|
350
369
|
const envVars = await buildEnvVarsForInterpolation(devIdNum);
|
|
370
|
+
envVars.PORT = String(appPort);
|
|
371
|
+
const portVar = getPortVarFromEnvTemplatePath(variablesPath);
|
|
372
|
+
if (portVar) {
|
|
373
|
+
envVars[portVar] = String(appPort);
|
|
374
|
+
}
|
|
351
375
|
updated = interpolateEnvVars(updated, envVars);
|
|
352
376
|
|
|
353
377
|
return updated;
|
|
@@ -447,24 +471,8 @@ function ensureNonEmptySecrets(secrets) {
|
|
|
447
471
|
* @returns {Object} Validation result
|
|
448
472
|
*/
|
|
449
473
|
function validateSecrets(envTemplate, secrets) {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
const lines = envTemplate.split('\n');
|
|
453
|
-
for (const line of lines) {
|
|
454
|
-
if (isCommentOrEmptyLine(line)) continue;
|
|
455
|
-
let match;
|
|
456
|
-
kvPattern.lastIndex = 0;
|
|
457
|
-
while ((match = kvPattern.exec(line)) !== null) {
|
|
458
|
-
const secretKey = match[1];
|
|
459
|
-
if (!(secretKey in secrets)) {
|
|
460
|
-
missing.push(`kv://${secretKey}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return {
|
|
465
|
-
valid: missing.length === 0,
|
|
466
|
-
missing
|
|
467
|
-
};
|
|
474
|
+
const missing = collectMissingSecrets(envTemplate, secrets);
|
|
475
|
+
return { valid: missing.length === 0, missing };
|
|
468
476
|
}
|
|
469
477
|
|
|
470
478
|
module.exports = {
|