@aifabrix/builder 2.44.6 → 2.45.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/.cursor/rules/cli-layout.mdc +7 -3
- package/jest.projects.js +56 -0
- package/lib/app/helpers.js +3 -3
- package/lib/app/index.js +3 -3
- package/lib/app/register.js +7 -6
- package/lib/app/restart-display.js +52 -21
- package/lib/app/rotate-secret.js +7 -6
- package/lib/app/run-helpers.js +15 -8
- package/lib/app/run.js +57 -9
- package/lib/app/show-display.js +7 -0
- package/lib/app/show.js +87 -5
- package/lib/build/index.js +9 -5
- package/lib/cli/infra-guided.js +42 -27
- package/lib/cli/installation-log-command.js +73 -0
- package/lib/cli/setup-app.js +11 -1
- package/lib/cli/setup-auth.js +94 -49
- package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
- package/lib/cli/setup-infra-up-platform-action.js +131 -0
- package/lib/cli/setup-infra.js +60 -119
- package/lib/cli/setup-platform.js +1 -1
- package/lib/cli/setup-utility-resolve.js +132 -0
- package/lib/cli/setup-utility.js +65 -51
- package/lib/commands/app-logs.js +81 -33
- package/lib/commands/auth-config.js +116 -18
- package/lib/commands/setup-modes.js +19 -6
- package/lib/commands/setup-prompts.js +41 -8
- package/lib/commands/setup.js +114 -9
- package/lib/commands/teardown.js +54 -5
- package/lib/commands/up-common.js +48 -14
- package/lib/commands/up-dataplane.js +21 -18
- package/lib/commands/up-miso.js +12 -8
- package/lib/commands/upload.js +5 -3
- package/lib/core/audit-logger.js +1 -34
- package/lib/core/config-admin-email.js +56 -0
- package/lib/core/config-normalize.js +60 -0
- package/lib/core/config-registered-controller-urls.js +54 -0
- package/lib/core/config.js +33 -50
- package/lib/core/secrets-ensure-infra.js +1 -1
- package/lib/core/secrets-env-content.js +86 -90
- package/lib/core/secrets-env-declarative-expand.js +170 -0
- package/lib/core/secrets-env-write.js +2 -0
- package/lib/core/secrets-load.js +106 -102
- package/lib/external-system/deploy.js +5 -1
- package/lib/internal/node-fs.js +2 -0
- package/lib/schema/application-schema.json +4 -0
- package/lib/schema/infra.parameter.yaml +10 -0
- package/lib/utils/app-config-resolver.js +24 -1
- package/lib/utils/applications-config-defaults.js +206 -0
- package/lib/utils/auth-config-validator.js +2 -12
- package/lib/utils/bash-secret-env.js +1 -1
- package/lib/utils/compose-generate-docker-compose.js +111 -6
- package/lib/utils/compose-generator.js +17 -8
- package/lib/utils/controller-url.js +50 -7
- package/lib/utils/env-copy.js +99 -14
- package/lib/utils/env-template.js +5 -1
- package/lib/utils/health-check-url.js +18 -15
- package/lib/utils/health-check.js +7 -5
- package/lib/utils/infra-optional-service-flags.js +69 -0
- package/lib/utils/installation-log-core.js +282 -0
- package/lib/utils/installation-log-record.js +237 -0
- package/lib/utils/installation-log.js +123 -0
- package/lib/utils/log-redaction.js +105 -0
- package/lib/utils/manifest-location.js +164 -0
- package/lib/utils/manifest-source-emit.js +162 -0
- package/lib/utils/paths.js +238 -89
- package/lib/utils/remote-secrets-loader.js +7 -1
- package/lib/utils/run-cli-flags.js +29 -0
- package/lib/utils/secrets-canonical.js +10 -3
- package/lib/utils/secrets-path.js +3 -4
- package/lib/utils/secrets-utils.js +20 -10
- package/lib/utils/system-builder-root.js +10 -2
- package/lib/utils/url-declarative-public-base.js +80 -12
- package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
- package/lib/utils/url-declarative-resolve-build.js +24 -393
- package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
- package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
- package/lib/utils/url-declarative-resolve.js +47 -7
- package/lib/utils/url-declarative-runtime-base-path.js +21 -1
- package/lib/utils/urls-local-registry-scan.js +103 -0
- package/lib/utils/urls-local-registry.js +161 -90
- package/package.json +3 -1
- package/templates/applications/dataplane/application.yaml +4 -0
- package/templates/applications/miso-controller/application.yaml +2 -0
- package/templates/applications/miso-controller/env.template +27 -29
- package/.npmrc.token +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL and developer-id normalization for runtime config.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Shared normalizers used by lib/core/config.js
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize controller URL for consistent storage and lookup
|
|
13
|
+
* @param {string} url - Controller URL to normalize
|
|
14
|
+
* @returns {string|undefined|null} Normalized URL or original falsy
|
|
15
|
+
*/
|
|
16
|
+
function normalizeControllerUrl(url) {
|
|
17
|
+
if (!url || typeof url !== 'string') {
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
let normalized = url.trim().replace(/\/+$/, '');
|
|
21
|
+
if (!normalized.match(/^https?:\/\//)) {
|
|
22
|
+
normalized = `http://${normalized}`;
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate and normalize developer ID
|
|
29
|
+
* @param {*} developerId - Developer ID value (can be string, number, undefined, or null)
|
|
30
|
+
* @returns {string} Normalized developer ID as string
|
|
31
|
+
* @throws {Error} If developer ID is invalid
|
|
32
|
+
*/
|
|
33
|
+
function validateAndNormalizeDeveloperId(developerId) {
|
|
34
|
+
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
35
|
+
|
|
36
|
+
if (typeof developerId === 'undefined' || developerId === null) {
|
|
37
|
+
return '0';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof developerId === 'number') {
|
|
41
|
+
if (developerId < 0 || !Number.isFinite(developerId)) {
|
|
42
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
43
|
+
}
|
|
44
|
+
return String(developerId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof developerId === 'string') {
|
|
48
|
+
if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
|
|
49
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
50
|
+
}
|
|
51
|
+
return developerId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
normalizeControllerUrl,
|
|
59
|
+
validateAndNormalizeDeveloperId
|
|
60
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive controller URLs stored in config (default + device token keys).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Registered controller URL list for auth --set-controller pick mode
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build sorted unique controller URLs from a config object.
|
|
13
|
+
* @param {Object} cfg - Parsed config.yaml object
|
|
14
|
+
* @param {(url: string) => string} normalizeControllerUrl - normalizer from config module
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
function buildRegisteredControllerUrlList(cfg, normalizeControllerUrl) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const add = (raw) => {
|
|
20
|
+
if (!raw || typeof raw !== 'string') return;
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
if (!/^https?:\/\//i.test(trimmed)) return;
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(trimmed);
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;
|
|
30
|
+
seen.add(normalizeControllerUrl(trimmed));
|
|
31
|
+
};
|
|
32
|
+
if (cfg.controller) {
|
|
33
|
+
add(cfg.controller);
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(cfg.device || {})) {
|
|
36
|
+
add(key);
|
|
37
|
+
}
|
|
38
|
+
return [...seen].sort((a, b) => a.localeCompare(b));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {() => Promise<Object>} getConfig - async config loader
|
|
43
|
+
* @param {(url: string) => string} normalizeControllerUrl
|
|
44
|
+
* @returns {Promise<string[]>}
|
|
45
|
+
*/
|
|
46
|
+
async function getRegisteredControllerUrlsWithLoader(getConfig, normalizeControllerUrl) {
|
|
47
|
+
const cfg = await getConfig();
|
|
48
|
+
return buildRegisteredControllerUrlList(cfg, normalizeControllerUrl);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
buildRegisteredControllerUrlList,
|
|
53
|
+
getRegisteredControllerUrlsWithLoader
|
|
54
|
+
};
|
package/lib/core/config.js
CHANGED
|
@@ -15,62 +15,15 @@ const os = require('os');
|
|
|
15
15
|
const { encryptToken, decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
|
|
16
16
|
const { ensureSecureFilePermissions, ensureSecureDirPermissions } = require('../utils/secure-file-permissions');
|
|
17
17
|
const { getRuntimeConfigDir, getRuntimeConfigFile } = require('./config-runtime-paths');
|
|
18
|
+
const { getAdminEmailFromConfig, setAdminEmailInConfig } = require('./config-admin-email');
|
|
19
|
+
const { getRegisteredControllerUrlsWithLoader } = require('./config-registered-controller-urls');
|
|
20
|
+
const { normalizeControllerUrl, validateAndNormalizeDeveloperId } = require('./config-normalize');
|
|
18
21
|
// Avoid importing paths.js here to prevent circular dependency; use shared runtime config dir helper.
|
|
19
22
|
// Config location: AIFABRIX_CONFIG dirname → AIFABRIX_HOME (with ~/.aifabrix fallback when config lives there) → ~/.aifabrix
|
|
20
23
|
|
|
21
24
|
// Cache for developer ID - loaded when getConfig() is first called
|
|
22
25
|
let cachedDeveloperId = null;
|
|
23
26
|
|
|
24
|
-
/**
|
|
25
|
-
* Normalize controller URL for consistent storage and lookup
|
|
26
|
-
* Removes trailing slashes and normalizes the URL format
|
|
27
|
-
* @param {string} url - Controller URL to normalize
|
|
28
|
-
* @returns {string} Normalized controller URL
|
|
29
|
-
*/
|
|
30
|
-
function normalizeControllerUrl(url) {
|
|
31
|
-
if (!url || typeof url !== 'string') {
|
|
32
|
-
return url;
|
|
33
|
-
}
|
|
34
|
-
// Remove trailing slashes
|
|
35
|
-
let normalized = url.trim().replace(/\/+$/, '');
|
|
36
|
-
// Ensure it starts with http:// or https://
|
|
37
|
-
if (!normalized.match(/^https?:\/\//)) {
|
|
38
|
-
// If it doesn't start with protocol, assume http://
|
|
39
|
-
normalized = `http://${normalized}`;
|
|
40
|
-
}
|
|
41
|
-
return normalized;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validate and normalize developer ID
|
|
46
|
-
* @param {*} developerId - Developer ID value (can be string, number, undefined, or null)
|
|
47
|
-
* @returns {string} Normalized developer ID as string
|
|
48
|
-
* @throws {Error} If developer ID is invalid
|
|
49
|
-
*/
|
|
50
|
-
function validateAndNormalizeDeveloperId(developerId) {
|
|
51
|
-
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
52
|
-
|
|
53
|
-
if (typeof developerId === 'undefined' || developerId === null) {
|
|
54
|
-
return '0';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (typeof developerId === 'number') {
|
|
58
|
-
if (developerId < 0 || !Number.isFinite(developerId)) {
|
|
59
|
-
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
60
|
-
}
|
|
61
|
-
return String(developerId);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof developerId === 'string') {
|
|
65
|
-
if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
|
|
66
|
-
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
67
|
-
}
|
|
68
|
-
return developerId;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
throw new Error('Developer ID must be a non-negative digit string or number');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
27
|
/**
|
|
75
28
|
* Ensure default config values exist
|
|
76
29
|
* @param {Object} config - Configuration object
|
|
@@ -258,6 +211,25 @@ async function getTlsEnabled() {
|
|
|
258
211
|
return cfg.tlsEnabled === true;
|
|
259
212
|
}
|
|
260
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Admin email stored in `config.yaml` by `aifabrix setup` (Keycloak / pgAdmin). Empty when unset.
|
|
216
|
+
*
|
|
217
|
+
* @returns {Promise<string>}
|
|
218
|
+
*/
|
|
219
|
+
async function getAdminEmail() {
|
|
220
|
+
return getAdminEmailFromConfig(getConfig);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Persist admin email to `config.yaml` (used on subsequent setup runs as the prompt default).
|
|
225
|
+
*
|
|
226
|
+
* @param {string} email
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
async function setAdminEmail(email) {
|
|
230
|
+
return setAdminEmailInConfig(getConfig, saveConfig, email);
|
|
231
|
+
}
|
|
232
|
+
|
|
261
233
|
/**
|
|
262
234
|
* Whether Traefik is enabled (`traefik: true` in config; infra compose includes the proxy).
|
|
263
235
|
* @returns {Promise<boolean>}
|
|
@@ -316,6 +288,14 @@ async function getControllerUrl() {
|
|
|
316
288
|
return config.controller || null;
|
|
317
289
|
}
|
|
318
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Controller URLs from config: `controller` plus `device` token keys.
|
|
293
|
+
* @returns {Promise<string[]>}
|
|
294
|
+
*/
|
|
295
|
+
async function getRegisteredControllerUrls() {
|
|
296
|
+
return getRegisteredControllerUrlsWithLoader(getConfig, normalizeControllerUrl);
|
|
297
|
+
}
|
|
298
|
+
|
|
319
299
|
function isTokenExpired(expiresAt) {
|
|
320
300
|
if (!expiresAt) return true;
|
|
321
301
|
const expirationTime = new Date(expiresAt).getTime();
|
|
@@ -454,6 +434,8 @@ const exportsObj = {
|
|
|
454
434
|
loadDeveloperId,
|
|
455
435
|
getCurrentEnvironment,
|
|
456
436
|
getTlsEnabled,
|
|
437
|
+
getAdminEmail,
|
|
438
|
+
setAdminEmail,
|
|
457
439
|
getTraefikEnabled,
|
|
458
440
|
setCurrentEnvironment,
|
|
459
441
|
resolveEnvironment,
|
|
@@ -469,6 +451,7 @@ const exportsObj = {
|
|
|
469
451
|
normalizeControllerUrl,
|
|
470
452
|
setControllerUrl,
|
|
471
453
|
getControllerUrl,
|
|
454
|
+
getRegisteredControllerUrls,
|
|
472
455
|
get CONFIG_DIR() {
|
|
473
456
|
return getRuntimeConfigDir();
|
|
474
457
|
},
|
|
@@ -71,7 +71,7 @@ function getInfraSecretKeysForUpInfra() {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Same merge as runtime `loadSecrets()` (
|
|
74
|
+
* Same merge as runtime `loadSecrets()` (primary `~/.aifabrix/secrets.local.yaml` plus `aifabrix-secrets` file or remote API).
|
|
75
75
|
* Ensures missing-key checks treat remote `--shared` keys as already satisfied.
|
|
76
76
|
*
|
|
77
77
|
* @returns {Promise<Object.<string, string>>}
|
|
@@ -37,8 +37,7 @@ const pathsUtil = require('../utils/paths');
|
|
|
37
37
|
const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
|
|
38
38
|
const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
|
|
39
39
|
const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
|
|
40
|
-
const {
|
|
41
|
-
const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
|
|
40
|
+
const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
|
|
42
41
|
const {
|
|
43
42
|
mergeDockerManifestPublishedPort,
|
|
44
43
|
rewriteDockerManifestPublicPortEnvLine
|
|
@@ -127,76 +126,6 @@ async function getDockerRedisDbEndpoints() {
|
|
|
127
126
|
return { redisHost, redisPort, dbHost, dbPort };
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
/**
|
|
131
|
-
* Config inputs for declarative url:// expansion (keeps expandDeclarativeUrlsIfPresent small).
|
|
132
|
-
* @param {string} appPath
|
|
133
|
-
* @returns {Promise<Object>}
|
|
134
|
-
*/
|
|
135
|
-
async function loadDeclarativeUrlExpandInputs(appPath) {
|
|
136
|
-
const userCfg = await config.getConfig();
|
|
137
|
-
let remoteServer = null;
|
|
138
|
-
try {
|
|
139
|
-
const rs = await config.getRemoteServer();
|
|
140
|
-
if (rs && String(rs).trim()) {
|
|
141
|
-
remoteServer = String(rs).trim();
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
remoteServer = null;
|
|
145
|
-
}
|
|
146
|
-
let developerIdRaw = null;
|
|
147
|
-
try {
|
|
148
|
-
developerIdRaw = await config.getDeveloperId();
|
|
149
|
-
} catch {
|
|
150
|
-
developerIdRaw = null;
|
|
151
|
-
}
|
|
152
|
-
let infraTlsEnabled = false;
|
|
153
|
-
try {
|
|
154
|
-
infraTlsEnabled = await config.getTlsEnabled();
|
|
155
|
-
} catch {
|
|
156
|
-
infraTlsEnabled = false;
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
userCfg,
|
|
160
|
-
remoteServer,
|
|
161
|
-
developerIdRaw,
|
|
162
|
-
infraTlsEnabled,
|
|
163
|
-
appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* After kv:// resolution, expand url:// references when application config exists.
|
|
169
|
-
* @param {string} resolved
|
|
170
|
-
* @param {string} appName
|
|
171
|
-
* @param {string} appPath
|
|
172
|
-
* @param {string|null} variablesPath
|
|
173
|
-
* @param {string} environment
|
|
174
|
-
* @param {boolean} envOnly
|
|
175
|
-
* @returns {Promise<string>}
|
|
176
|
-
*/
|
|
177
|
-
async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variablesPath, environment, envOnly) {
|
|
178
|
-
if (envOnly || !variablesPath) {
|
|
179
|
-
return resolved;
|
|
180
|
-
}
|
|
181
|
-
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
|
|
182
|
-
await loadDeclarativeUrlExpandInputs(appPath);
|
|
183
|
-
resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
|
|
184
|
-
if (!resolved.includes('url://')) {
|
|
185
|
-
return resolved;
|
|
186
|
-
}
|
|
187
|
-
return expandDeclarativeUrlsInEnvContent(resolved, {
|
|
188
|
-
profile: environment === 'docker' ? 'docker' : 'local',
|
|
189
|
-
currentAppKey: appName,
|
|
190
|
-
variablesPath,
|
|
191
|
-
useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
|
|
192
|
-
appEnvironmentScopedResources: appScopedFlag,
|
|
193
|
-
remoteServer,
|
|
194
|
-
developerIdRaw,
|
|
195
|
-
traefik: Boolean(userCfg.traefik),
|
|
196
|
-
infraTlsEnabled
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
129
|
/** Docker env transformations: ports, infra endpoints, PORT. */
|
|
201
130
|
async function applyDockerTransformations(resolved, variablesPath) {
|
|
202
131
|
resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
|
|
@@ -337,17 +266,17 @@ function mergeEnvContentPreservingExisting(newContent, existingMap) {
|
|
|
337
266
|
/**
|
|
338
267
|
* Merges a key-value map into existing .env file content, preserving comments and blank lines.
|
|
339
268
|
* For each KEY=value line in existing content, replaces value with newMap[KEY] when the key exists
|
|
340
|
-
* in newMap.
|
|
269
|
+
* in newMap. By default appends keys from newMap that did not appear in the file; set
|
|
270
|
+
* `appendMissingFromNewMap: false` to only update keys already present (e.g. `--reload` into a
|
|
271
|
+
* resolve-generated file so run-only keys like DB_0_NAME are not tacked on the end).
|
|
341
272
|
*
|
|
342
273
|
* @param {string} existingContent - Full existing .env file content
|
|
343
274
|
* @param {Object.<string, string>} newMap - New key-to-value map (e.g. from resolved or run env)
|
|
275
|
+
* @param {Object} [options] - Merge options
|
|
276
|
+
* @param {boolean} [options.appendMissingFromNewMap=true] - When false, do not append keys only in newMap
|
|
344
277
|
* @returns {string} Merged content with comments preserved
|
|
345
278
|
*/
|
|
346
|
-
function
|
|
347
|
-
if (!newMap || Object.keys(newMap).length === 0) {
|
|
348
|
-
return typeof existingContent === 'string' ? existingContent : '';
|
|
349
|
-
}
|
|
350
|
-
const lines = (existingContent || '').split(/\r?\n/);
|
|
279
|
+
function collectSeenKeysAndMergeEnvLines(lines, newMap) {
|
|
351
280
|
const seen = new Set();
|
|
352
281
|
const out = [];
|
|
353
282
|
for (const line of lines) {
|
|
@@ -365,8 +294,26 @@ function mergeEnvMapIntoContent(existingContent, newMap) {
|
|
|
365
294
|
}
|
|
366
295
|
out.push(line);
|
|
367
296
|
}
|
|
297
|
+
return { out, seen };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function appendMissingEnvKeysFromMap(out, seen, newMap) {
|
|
368
301
|
for (const key of Object.keys(newMap)) {
|
|
369
|
-
if (!seen.has(key))
|
|
302
|
+
if (!seen.has(key)) {
|
|
303
|
+
out.push(`${key}=${newMap[key]}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function mergeEnvMapIntoContent(existingContent, newMap, options = {}) {
|
|
309
|
+
if (!newMap || Object.keys(newMap).length === 0) {
|
|
310
|
+
return typeof existingContent === 'string' ? existingContent : '';
|
|
311
|
+
}
|
|
312
|
+
const appendMissing = options.appendMissingFromNewMap !== false;
|
|
313
|
+
const lines = (existingContent || '').split(/\r?\n/);
|
|
314
|
+
const { out, seen } = collectSeenKeysAndMergeEnvLines(lines, newMap);
|
|
315
|
+
if (appendMissing) {
|
|
316
|
+
appendMissingEnvKeysFromMap(out, seen, newMap);
|
|
370
317
|
}
|
|
371
318
|
return out.join('\n');
|
|
372
319
|
}
|
|
@@ -386,19 +333,72 @@ function resolveEnvContentToWrite(resolved, pathToPreserve) {
|
|
|
386
333
|
|
|
387
334
|
/**
|
|
388
335
|
* Generates and writes .env file. Newly resolved values win over existing .env; extra vars in existing .env are kept.
|
|
389
|
-
* When options.envOnly is true, only env.template is used; .env is written to options.appPath
|
|
336
|
+
* When `options.envOnly` is true, only env.template is used; .env is written to `options.appPath`.
|
|
337
|
+
*
|
|
338
|
+
* When `options.noWrite` is true, the function resolves the .env content in memory and skips
|
|
339
|
+
* both writes — neither `<appPath>/.env` nor `build.envOutputPath` is materialized — and returns
|
|
340
|
+
* `null`. Use this from non-resolve flows (register/rotate-secret/build/up-*) so resolved secrets
|
|
341
|
+
* never land on disk except when the user runs `aifabrix resolve <app>` explicitly.
|
|
342
|
+
*
|
|
390
343
|
* @async
|
|
391
344
|
* @param {string} appName - Name of the application
|
|
392
345
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
393
346
|
* @param {string} [environment='local'] - Environment context ('local' or 'docker')
|
|
394
347
|
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
395
|
-
* @param {Object} [options] - Optional: appPath, envOnly, skipOutputPath, preserveFromPath
|
|
396
|
-
*
|
|
348
|
+
* @param {Object} [options] - Optional: appPath, envOnly, skipOutputPath, preserveFromPath, noWrite,
|
|
349
|
+
* preferLocalEnvOutputPath (when **true**, `build.envOutputPath` is regenerated with **local** `url://` profile; **false**
|
|
350
|
+
* only when **both** `remote-server` is set and `applications.<app>.reload` is true — then docker flavor matches `builder/<app>/.env`)
|
|
351
|
+
* @param {boolean} [options.noWrite=false] - When true, resolve in-memory only; do not write
|
|
352
|
+
* `<appPath>/.env` and do not call `processEnvVariables`. Returns `null` in that case.
|
|
353
|
+
* @returns {Promise<string|null>} Path to generated .env file, or `null` when `noWrite` is true
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* // up-platform / up-miso / up-dataplane / register / rotate-secret / build flows:
|
|
357
|
+
* await generateEnvFile('dataplane', null, 'local', false, { noWrite: true });
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // aifabrix resolve <app> — the only legitimate writer of a persistent .env:
|
|
361
|
+
* await generateEnvFile('dataplane', undefined, 'docker', force, {
|
|
362
|
+
* appPath, envOnly, skipOutputPath: false, preserveFromPath: null
|
|
363
|
+
* });
|
|
397
364
|
*/
|
|
365
|
+
/**
|
|
366
|
+
* Materialize a resolved .env to `<appPath>/.env` and (optionally) copy through
|
|
367
|
+
* `build.envOutputPath`. Extracted so {@link generateEnvFile} can stay under the
|
|
368
|
+
* 20-statement limit while still expressing the in-memory vs on-disk branch clearly.
|
|
369
|
+
*
|
|
370
|
+
* @async
|
|
371
|
+
* @param {Object} params
|
|
372
|
+
* @param {string} params.appName
|
|
373
|
+
* @param {string} params.appPath
|
|
374
|
+
* @param {string} params.envPath - Resolved `<appPath>/.env` target
|
|
375
|
+
* @param {string} params.resolved - Fully resolved .env content
|
|
376
|
+
* @param {string|null} params.variablesPath - application.yaml path (or null when envOnly)
|
|
377
|
+
* @param {string} [params.secretsPath]
|
|
378
|
+
* @param {Object} params.opts - Caller options (preserveFromPath, skipOutputPath, preferLocalEnvOutputPath, appPath)
|
|
379
|
+
* @returns {Promise<string>} Path to the written .env file
|
|
380
|
+
*/
|
|
381
|
+
async function writeResolvedEnv({ appName, envPath, resolved, variablesPath, secretsPath, opts }) {
|
|
382
|
+
const preservePath = opts.preserveFromPath !== undefined && opts.preserveFromPath !== null ? opts.preserveFromPath : null;
|
|
383
|
+
const pathToPreserve = preservePath !== null ? preservePath : envPath;
|
|
384
|
+
const toWrite = resolveEnvContentToWrite(resolved, pathToPreserve);
|
|
385
|
+
fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
|
|
386
|
+
|
|
387
|
+
if (!opts.skipOutputPath) {
|
|
388
|
+
const { processEnvVariables } = require('../utils/env-copy');
|
|
389
|
+
await processEnvVariables(envPath, variablesPath, appName, secretsPath, {
|
|
390
|
+
preferLocalEnvOutputPath: opts.preferLocalEnvOutputPath === true,
|
|
391
|
+
appPath: opts.appPath || null
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return envPath;
|
|
395
|
+
}
|
|
396
|
+
|
|
398
397
|
async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, options = {}) {
|
|
399
398
|
const opts = options && typeof options === 'object' ? options : {};
|
|
400
399
|
const appPath = opts.appPath || pathsUtil.getBuilderPath(appName);
|
|
401
400
|
const envOnly = !!opts.envOnly;
|
|
401
|
+
const noWrite = opts.noWrite === true;
|
|
402
402
|
const variablesPath = envOnly ? null : resolveApplicationConfigPath(appPath);
|
|
403
403
|
const envPath = path.join(appPath, '.env');
|
|
404
404
|
|
|
@@ -409,18 +409,14 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
|
|
412
413
|
const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
|
|
413
|
-
const preservePath = opts.preserveFromPath !== undefined && opts.preserveFromPath !== null ? opts.preserveFromPath : null;
|
|
414
|
-
const pathToPreserve = preservePath !== null ? preservePath : envPath;
|
|
415
|
-
const toWrite = resolveEnvContentToWrite(resolved, pathToPreserve);
|
|
416
|
-
fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
|
|
417
414
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
await processEnvVariables(envPath, variablesPath, appName, secretsPath);
|
|
415
|
+
if (noWrite) {
|
|
416
|
+
return null;
|
|
421
417
|
}
|
|
422
418
|
|
|
423
|
-
return envPath;
|
|
419
|
+
return writeResolvedEnv({ appName, appPath, envPath, resolved, variablesPath, secretsPath, opts });
|
|
424
420
|
}
|
|
425
421
|
|
|
426
422
|
module.exports = {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative url:// expansion after kv:// (keeps secrets-env-content under max-lines).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Declarative url:// expansion after kv://; shared ctx builder; show URL helper
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
const pathsUtil = require('../utils/paths');
|
|
13
|
+
const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
|
|
14
|
+
const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
|
|
15
|
+
const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
|
|
16
|
+
const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
|
|
17
|
+
/**
|
|
18
|
+
* Config inputs for declarative url:// expansion.
|
|
19
|
+
* @param {string} appPath
|
|
20
|
+
* @returns {Promise<Object>}
|
|
21
|
+
*/
|
|
22
|
+
async function loadDeclarativeUrlExpandInputs(appPath) {
|
|
23
|
+
const userCfg = await config.getConfig();
|
|
24
|
+
let remoteServer = null;
|
|
25
|
+
try {
|
|
26
|
+
const rs = await config.getRemoteServer();
|
|
27
|
+
if (rs && String(rs).trim()) {
|
|
28
|
+
remoteServer = String(rs).trim();
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
remoteServer = null;
|
|
32
|
+
}
|
|
33
|
+
let developerIdRaw = null;
|
|
34
|
+
try {
|
|
35
|
+
developerIdRaw = await config.getDeveloperId();
|
|
36
|
+
} catch {
|
|
37
|
+
developerIdRaw = null;
|
|
38
|
+
}
|
|
39
|
+
let infraTlsEnabled = false;
|
|
40
|
+
try {
|
|
41
|
+
infraTlsEnabled = await config.getTlsEnabled();
|
|
42
|
+
} catch {
|
|
43
|
+
infraTlsEnabled = false;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
userCfg,
|
|
47
|
+
remoteServer,
|
|
48
|
+
developerIdRaw,
|
|
49
|
+
infraTlsEnabled,
|
|
50
|
+
appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build ctx for {@link expandDeclarativeUrlsInEnvContent} from preloaded inputs.
|
|
56
|
+
* @param {string} appName
|
|
57
|
+
* @param {string|null|undefined} variablesPath
|
|
58
|
+
* @param {string} environment
|
|
59
|
+
* @param {Object} inputs - Result of {@link loadDeclarativeUrlExpandInputs}
|
|
60
|
+
* @returns {Object}
|
|
61
|
+
*/
|
|
62
|
+
function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, inputs) {
|
|
63
|
+
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
|
|
64
|
+
let projectRoot = null;
|
|
65
|
+
try {
|
|
66
|
+
const r = pathsUtil.getProjectRoot();
|
|
67
|
+
if (r && String(r).trim()) {
|
|
68
|
+
projectRoot = String(r).trim();
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
projectRoot = null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
profile: environment === 'docker' ? 'docker' : 'local',
|
|
75
|
+
currentAppKey: appName,
|
|
76
|
+
variablesPath,
|
|
77
|
+
useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
|
|
78
|
+
appEnvironmentScopedResources: appScopedFlag,
|
|
79
|
+
remoteServer,
|
|
80
|
+
developerIdRaw,
|
|
81
|
+
infraTlsEnabled,
|
|
82
|
+
userCfg,
|
|
83
|
+
projectRoot
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve `url://public` and `url://internal` for the app (same rules as run `.env`, default `docker` profile).
|
|
89
|
+
* @param {string} appKey
|
|
90
|
+
* @param {string} appPath
|
|
91
|
+
* @param {string|null|undefined} variablesPath
|
|
92
|
+
* @param {string} [environment='docker'] - Matches `secrets-env-write` default so `url://internal` uses the same service host + vdir rules as the app `.env` copied for Docker run.
|
|
93
|
+
* @returns {Promise<{ publicUrl: string, internalUrl: string }|null>}
|
|
94
|
+
*/
|
|
95
|
+
async function resolveDeclarativeShowUrlsForApp(appKey, appPath, variablesPath, environment = 'docker') {
|
|
96
|
+
if (!variablesPath || !appPath || !appKey) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const inputs = await loadDeclarativeUrlExpandInputs(appPath);
|
|
100
|
+
const { parseSimpleEnvMap } = require('../utils/url-declarative-resolve');
|
|
101
|
+
let content = 'APP_SHOW_PUBLIC=url://public\nAPP_SHOW_INTERNAL=url://internal\n';
|
|
102
|
+
content = rewriteInactiveDeclarativeVdirPublicContent(content, variablesPath, inputs.userCfg);
|
|
103
|
+
if (!content.includes('url://')) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const ctx = buildDeclarativeUrlExpandContextFromInputs(appKey, variablesPath, environment, inputs);
|
|
107
|
+
const out = await expandDeclarativeUrlsInEnvContent(content, ctx);
|
|
108
|
+
const m = parseSimpleEnvMap(out);
|
|
109
|
+
const publicUrl = m.APP_SHOW_PUBLIC;
|
|
110
|
+
const internalUrl = m.APP_SHOW_INTERNAL;
|
|
111
|
+
if (
|
|
112
|
+
!publicUrl ||
|
|
113
|
+
!internalUrl ||
|
|
114
|
+
String(publicUrl).includes('url://') ||
|
|
115
|
+
String(internalUrl).includes('url://')
|
|
116
|
+
) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return { publicUrl: String(publicUrl).trim(), internalUrl: String(internalUrl).trim() };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* After kv:// resolution, expand url:// references when application config exists.
|
|
124
|
+
* Also refreshes {@link refreshUrlsLocalRegistryFromBuilder} whenever `variablesPath` is set so
|
|
125
|
+
* `urls.local.yaml` picks up `port` / `frontDoorRouting` changes even if `env.template` has no
|
|
126
|
+
* literal `url://` placeholders (they may already be expanded or absent).
|
|
127
|
+
* @param {string} resolved
|
|
128
|
+
* @param {string} appName
|
|
129
|
+
* @param {string} appPath
|
|
130
|
+
* @param {string|null} variablesPath
|
|
131
|
+
* @param {string} environment
|
|
132
|
+
* @param {boolean} envOnly
|
|
133
|
+
* @returns {Promise<string>}
|
|
134
|
+
*/
|
|
135
|
+
async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variablesPath, environment, envOnly) {
|
|
136
|
+
if (envOnly || !variablesPath) {
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
|
|
140
|
+
await loadDeclarativeUrlExpandInputs(appPath);
|
|
141
|
+
resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
|
|
142
|
+
const ctx = buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, {
|
|
143
|
+
userCfg,
|
|
144
|
+
remoteServer,
|
|
145
|
+
developerIdRaw,
|
|
146
|
+
infraTlsEnabled,
|
|
147
|
+
appScopedFlag
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
const pr = ctx.projectRoot && String(ctx.projectRoot).trim() ? String(ctx.projectRoot).trim() : '';
|
|
151
|
+
const registryRoot = pr || pathsUtil.getProjectRoot();
|
|
152
|
+
if (ctx.excludeCwdBuilderScan === true) {
|
|
153
|
+
refreshUrlsLocalRegistryFromBuilder(registryRoot, { excludeCwdBuilderScan: true });
|
|
154
|
+
} else {
|
|
155
|
+
refreshUrlsLocalRegistryFromBuilder(registryRoot);
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// best-effort: registry refresh must not block .env generation
|
|
159
|
+
}
|
|
160
|
+
if (!resolved.includes('url://')) {
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
return expandDeclarativeUrlsInEnvContent(resolved, ctx);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
expandDeclarativeUrlsIfPresent,
|
|
168
|
+
loadDeclarativeUrlExpandInputs,
|
|
169
|
+
resolveDeclarativeShowUrlsForApp
|
|
170
|
+
};
|
|
@@ -149,6 +149,8 @@ function parseEnvContentToMap(content) {
|
|
|
149
149
|
* Resolve .env in memory and write only to envOutputPath or temp (no builder/ or integration/).
|
|
150
150
|
* Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing, then from process.env, then names derived from `BASH_*` keys in the merged secrets store, so shell/install/build match private registry tooling.
|
|
151
151
|
*
|
|
152
|
+
* **Ephemeral / tooling path:** Used by `app-install`, `app-shell`, and `app-test` to materialize a disposable `.env` (defaults to a temp file under `os.tmpdir()`). This is **not** the same as `aifabrix resolve <app>` (which uses `generateEnvFile` and preserves `build.envOutputPath` / comments). Prefer `aifabrix resolve` when you need a durable repo `.env` for local development.
|
|
153
|
+
*
|
|
152
154
|
* @async
|
|
153
155
|
* @function resolveAndWriteEnvFile
|
|
154
156
|
* @param {string} appName - Application name
|