@aifabrix/builder 2.40.2 → 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 +6 -4
- package/integration/hubspot/test.js +1 -1
- 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-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 +12 -3
- 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/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 +8 -0
- 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 +43 -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/validator.js +65 -0
- package/package.json +2 -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/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() {
|
|
@@ -195,39 +189,29 @@ function checkGlobalProjectRoot() {
|
|
|
195
189
|
|
|
196
190
|
/**
|
|
197
191
|
* Tries different strategies to find project root
|
|
198
|
-
* @function tryFindProjectRoot
|
|
199
192
|
* @returns {string} Found project root
|
|
200
193
|
*/
|
|
201
194
|
function tryFindProjectRoot() {
|
|
202
|
-
// Strategy 1: Check global.PROJECT_ROOT
|
|
203
195
|
const globalRoot = checkGlobalProjectRoot();
|
|
204
196
|
if (globalRoot) {
|
|
205
197
|
cachedProjectRoot = globalRoot;
|
|
206
198
|
return cachedProjectRoot;
|
|
207
199
|
}
|
|
208
|
-
|
|
209
|
-
// Strategy 2: Walk up from __dirname
|
|
210
200
|
const foundRoot = findProjectRootByWalkingUp(__dirname);
|
|
211
201
|
if (foundRoot && hasPackageJson(foundRoot)) {
|
|
212
202
|
cachedProjectRoot = foundRoot;
|
|
213
203
|
return cachedProjectRoot;
|
|
214
204
|
}
|
|
215
|
-
|
|
216
|
-
// Strategy 3: Try process.cwd()
|
|
217
205
|
const cwdRoot = findProjectRootFromCwd();
|
|
218
206
|
if (cwdRoot && hasPackageJson(cwdRoot)) {
|
|
219
207
|
cachedProjectRoot = cwdRoot;
|
|
220
208
|
return cachedProjectRoot;
|
|
221
209
|
}
|
|
222
|
-
|
|
223
|
-
// Strategy 4: Fallback
|
|
224
210
|
const fallbackRoot = getFallbackProjectRoot();
|
|
225
211
|
if (hasPackageJson(fallbackRoot)) {
|
|
226
212
|
cachedProjectRoot = fallbackRoot;
|
|
227
213
|
return cachedProjectRoot;
|
|
228
214
|
}
|
|
229
|
-
|
|
230
|
-
// Last resort
|
|
231
215
|
cachedProjectRoot = fallbackRoot;
|
|
232
216
|
return cachedProjectRoot;
|
|
233
217
|
}
|
|
@@ -242,10 +226,7 @@ function getProjectRoot() {
|
|
|
242
226
|
}
|
|
243
227
|
|
|
244
228
|
/**
|
|
245
|
-
* Returns the applications base directory
|
|
246
|
-
* Dev 0: <home>/applications
|
|
247
|
-
* Dev > 0: <home>/applications-dev-{id}
|
|
248
|
-
*
|
|
229
|
+
* Returns the applications base directory. Dev 0: <home>/applications; Dev > 0: <home>/applications-dev-{id}
|
|
249
230
|
* @param {number|string} developerId - Developer ID
|
|
250
231
|
* @returns {string} Absolute path to applications base directory
|
|
251
232
|
*/
|
|
@@ -259,19 +240,13 @@ function getApplicationsBaseDir(developerId) {
|
|
|
259
240
|
}
|
|
260
241
|
|
|
261
242
|
/**
|
|
262
|
-
* Returns the developer-specific application directory.
|
|
263
|
-
* Dev 0: points to applications/ (root)
|
|
264
|
-
* Dev > 0: <home>/applications-dev-{id} (root)
|
|
265
|
-
*
|
|
243
|
+
* Returns the developer-specific application directory. Dev 0: applications/; Dev > 0: applications-dev-{id}
|
|
266
244
|
* @param {string} appName - Application name
|
|
267
245
|
* @param {number|string} developerId - Developer ID
|
|
268
|
-
* @returns {string}
|
|
246
|
+
* @returns {string} Developer-specific app directory (root)
|
|
269
247
|
*/
|
|
270
248
|
function getDevDirectory(appName, developerId) {
|
|
271
249
|
const baseDir = getApplicationsBaseDir(developerId);
|
|
272
|
-
// All files should be generated at the root of the applications folder
|
|
273
|
-
// Dev 0: <home>/applications
|
|
274
|
-
// Dev > 0: <home>/applications-dev-{id}
|
|
275
250
|
return baseDir;
|
|
276
251
|
}
|
|
277
252
|
|
|
@@ -292,7 +267,6 @@ function getAppPath(appName, appType) {
|
|
|
292
267
|
|
|
293
268
|
/**
|
|
294
269
|
* Base directory for integration/builder: project root when cwd is inside project, else cwd.
|
|
295
|
-
* So deploy works when run from integration/<app> (e.g. node deploy.js), and tests using temp dirs still work.
|
|
296
270
|
* @returns {string} Directory to resolve integration/ and builder/ from
|
|
297
271
|
*/
|
|
298
272
|
function getIntegrationBuilderBaseDir() {
|
|
@@ -307,7 +281,6 @@ function getIntegrationBuilderBaseDir() {
|
|
|
307
281
|
|
|
308
282
|
/**
|
|
309
283
|
* Gets the integration folder path for external systems.
|
|
310
|
-
* Uses project root when cwd is inside project so deploy works when run from integration/<app> (e.g. node deploy.js).
|
|
311
284
|
* @param {string} appName - Application name
|
|
312
285
|
* @returns {string} Absolute path to integration directory
|
|
313
286
|
*/
|
|
@@ -320,9 +293,21 @@ function getIntegrationPath(appName) {
|
|
|
320
293
|
}
|
|
321
294
|
|
|
322
295
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
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.
|
|
326
311
|
* @param {string} appName - Application name
|
|
327
312
|
* @returns {string} Absolute path to builder directory
|
|
328
313
|
*/
|
|
@@ -462,6 +447,29 @@ async function detectAppType(appName, _options = {}) {
|
|
|
462
447
|
if (builderResult) return builderResult;
|
|
463
448
|
throw new Error(`App '${appName}' not found in integration/${appName} or builder/${appName}`);
|
|
464
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
|
+
|
|
465
473
|
module.exports = {
|
|
466
474
|
getAifabrixHome,
|
|
467
475
|
getConfigDirForPaths,
|
|
@@ -471,9 +479,11 @@ module.exports = {
|
|
|
471
479
|
getProjectRoot,
|
|
472
480
|
getIntegrationPath,
|
|
473
481
|
getBuilderPath,
|
|
482
|
+
resolveBuildContext,
|
|
474
483
|
getDeployJsonPath,
|
|
475
484
|
resolveApplicationConfigPath,
|
|
476
485
|
detectAppType,
|
|
486
|
+
getResolveAppPath,
|
|
477
487
|
clearProjectRootCache
|
|
478
488
|
};
|
|
479
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 = {
|
|
@@ -54,8 +54,8 @@ async function getActualSecretsPath(secretsPath, _appName) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Cascading lookup: user's file first (
|
|
58
|
-
const userSecretsPath = path.join(paths.
|
|
57
|
+
// Cascading lookup: user's file first (primary home: AIFABRIX_HOME or ~/.aifabrix)
|
|
58
|
+
const userSecretsPath = path.join(paths.getConfigDirForPaths(), 'secrets.local.yaml');
|
|
59
59
|
|
|
60
60
|
// Check config.yaml for canonical secrets path
|
|
61
61
|
let buildSecretsPath = null;
|