@aifabrix/builder 2.44.5 → 2.44.6
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 +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +48 -2
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +134 -61
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility.js +78 -33
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +214 -31
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -338
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/up-common.js +79 -19
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +6 -5
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +4 -3
- package/lib/commands/wizard-headless.js +2 -1
- package/lib/commands/wizard.js +2 -1
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +4 -1
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/external-readme.js +71 -2
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +23 -12
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
package/lib/core/secrets.js
CHANGED
|
@@ -4,774 +4,34 @@
|
|
|
4
4
|
* This module handles secret resolution and environment file generation.
|
|
5
5
|
* Resolves kv:// references from secrets files and generates .env files.
|
|
6
6
|
*
|
|
7
|
+
* Implementation is split across secrets-load.js, secrets-env-content.js, secrets-admin-env.js, secrets-names.js.
|
|
8
|
+
*
|
|
7
9
|
* @fileoverview Secret resolution and environment management for AI Fabrix Builder
|
|
8
10
|
* @author AI Fabrix Team
|
|
9
11
|
* @version 2.0.0
|
|
10
12
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
16
|
-
const config = require('./config');
|
|
17
|
-
const {
|
|
18
|
-
interpolateEnvVars,
|
|
19
|
-
collectMissingSecrets,
|
|
20
|
-
formatMissingSecretsFileInfo,
|
|
21
|
-
replaceKvInContent,
|
|
22
|
-
loadEnvTemplate,
|
|
23
|
-
adjustLocalEnvPortsInContent,
|
|
24
|
-
rewriteInfraEndpoints,
|
|
25
|
-
readYamlAtPath,
|
|
26
|
-
applyCanonicalSecretsOverride,
|
|
27
|
-
validateSecrets
|
|
28
|
-
} = require('../utils/secrets-helpers');
|
|
29
|
-
const { buildEnvVarMap } = require('../utils/env-map');
|
|
30
|
-
const {
|
|
31
|
-
mergeInfraParameterDefaultsForCli,
|
|
32
|
-
getInfraParameterCatalog,
|
|
33
|
-
readRelaxedCatalogDefaults
|
|
34
|
-
} = require('../parameters/infra-parameter-catalog');
|
|
35
|
-
const { resolveServicePortsInEnvContent } = require('../utils/secrets-url');
|
|
36
|
-
const {
|
|
37
|
-
updatePortForDocker,
|
|
38
|
-
getBaseDockerEnv,
|
|
39
|
-
applyDockerEnvOverride,
|
|
40
|
-
getContainerPortFromDockerEnv
|
|
41
|
-
} = require('./secrets-docker-env');
|
|
42
|
-
const { getContainerPortFromPath, loadVariablesFromPath } = require('../utils/port-resolver');
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { validateSecrets } = require('../utils/secrets-helpers');
|
|
43
17
|
const {
|
|
44
18
|
generateMissingSecrets,
|
|
45
19
|
createDefaultSecrets
|
|
46
20
|
} = require('../utils/secrets-generator');
|
|
47
|
-
const
|
|
48
|
-
const {
|
|
49
|
-
resolveSecretsPath,
|
|
50
|
-
getActualSecretsPath
|
|
51
|
-
} = require('../utils/secrets-path');
|
|
21
|
+
const { loadSecrets } = require('./secrets-load');
|
|
52
22
|
const {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const pathsUtil = require('../utils/paths');
|
|
60
|
-
const { ensureSecureFilePermissions } = require('../utils/secure-file-permissions');
|
|
61
|
-
const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
|
|
62
|
-
const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
|
|
63
|
-
const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
|
|
64
|
-
const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
|
|
65
|
-
const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
|
|
23
|
+
resolveKvReferences,
|
|
24
|
+
generateEnvContent,
|
|
25
|
+
generateEnvFile,
|
|
26
|
+
parseEnvContentToMap,
|
|
27
|
+
mergeEnvMapIntoContent
|
|
28
|
+
} = require('./secrets-env-content');
|
|
66
29
|
const {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} = require('
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Generates a canonical secret name from an environment variable key.
|
|
73
|
-
* Converts to lowercase, replaces non-alphanumeric characters with hyphens,
|
|
74
|
-
* collapses consecutive hyphens, and trims leading/trailing hyphens.
|
|
75
|
-
*
|
|
76
|
-
* @function getCanonicalSecretName
|
|
77
|
-
* @param {string} key - Environment variable key (e.g., JWT_SECRET)
|
|
78
|
-
* @returns {string} Canonical secret name (e.g., jwt-secret)
|
|
79
|
-
*/
|
|
80
|
-
function getCanonicalSecretName(key) {
|
|
81
|
-
if (!key || typeof key !== 'string') {
|
|
82
|
-
return '';
|
|
83
|
-
}
|
|
84
|
-
// Insert hyphens before capital letters (camelCase -> kebab-case)
|
|
85
|
-
// Then convert to lowercase and replace non-alphanumeric with hyphens
|
|
86
|
-
const withHyphens = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
87
|
-
const lower = withHyphens.toLowerCase();
|
|
88
|
-
const hyphenated = lower.replace(/[^a-z0-9]/g, '-');
|
|
89
|
-
const collapsed = hyphenated.replace(/-+/g, '-');
|
|
90
|
-
return collapsed.replace(/^-+|-+$/g, '');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Decrypts encrypted values in secrets object
|
|
95
|
-
* Checks for secure:// prefix and decrypts using encryption key from config
|
|
96
|
-
*
|
|
97
|
-
* @async
|
|
98
|
-
* @function decryptSecretsObject
|
|
99
|
-
* @param {Object} secrets - Secrets object with potentially encrypted values
|
|
100
|
-
* @returns {Promise<Object>} Secrets object with decrypted values
|
|
101
|
-
* @throws {Error} If decryption fails or encryption key is missing
|
|
102
|
-
*/
|
|
103
|
-
async function decryptSecretsObject(secrets) {
|
|
104
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
105
|
-
return secrets;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
109
|
-
if (!encryptionKey) {
|
|
110
|
-
// No encryption key set, check if any values are encrypted
|
|
111
|
-
const hasEncrypted = Object.values(secrets).some(value => isEncrypted(value));
|
|
112
|
-
if (hasEncrypted) {
|
|
113
|
-
throw new Error('Encrypted secrets found but no encryption key configured. Run "aifabrix secure --secrets-encryption <key>" to set encryption key.');
|
|
114
|
-
}
|
|
115
|
-
// No encrypted values, return as-is
|
|
116
|
-
return secrets;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const decryptedSecrets = {};
|
|
120
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
121
|
-
if (isEncrypted(value)) {
|
|
122
|
-
try {
|
|
123
|
-
decryptedSecrets[key] = decryptSecret(value, encryptionKey);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
throw new Error(`Failed to decrypt secret '${key}': ${error.message}`);
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
decryptedSecrets[key] = value;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return decryptedSecrets;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Loads secrets with cascading lookup
|
|
137
|
-
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
138
|
-
* When aifabrix-secrets (or secrets-path) is set in config.yaml and that file exists, it is used as base; user's file (local) is strongest and overrides project for same key. Otherwise user's file first, then aifabrix-secrets as fallback.
|
|
139
|
-
* Automatically decrypts values with secure:// prefix
|
|
140
|
-
*
|
|
141
|
-
* @async
|
|
142
|
-
* @function loadSecrets
|
|
143
|
-
* @param {string} [secretsPath] - Path to secrets file (optional, for explicit override)
|
|
144
|
-
* @param {string} [appName] - Application name (optional, for backward compatibility)
|
|
145
|
-
* @returns {Promise<Object>} Loaded secrets object with decrypted values (may be empty after bootstrap file create)
|
|
146
|
-
* @throws {Error} If explicit secretsPath is set but file is missing or invalid
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* const secrets = await loadSecrets('../../secrets.local.yaml');
|
|
150
|
-
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* // When config.yaml has aifabrix-secrets: ./secrets.local.yaml, project file is base;
|
|
154
|
-
* // ~/.aifabrix/secrets.local.yaml overrides project for same key (local strongest).
|
|
155
|
-
* const secrets = await loadSecrets(undefined, 'myapp');
|
|
156
|
-
*/
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Merges config file secrets into user secrets (user wins). Returns null if path missing or config empty.
|
|
160
|
-
* @param {Object} userSecrets - User secrets object
|
|
161
|
-
* @param {string} resolvedConfigPath - Absolute path to config secrets file
|
|
162
|
-
* @returns {Object|null} Merged secrets or null
|
|
163
|
-
*/
|
|
164
|
-
function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
165
|
-
if (!fs.existsSync(resolvedConfigPath)) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
ensureSecureFilePermissions(resolvedConfigPath);
|
|
169
|
-
let configSecrets;
|
|
170
|
-
try {
|
|
171
|
-
configSecrets = readYamlAtPath(resolvedConfigPath);
|
|
172
|
-
} catch (loadError) {
|
|
173
|
-
throw new Error(`Failed to load secrets file ${resolvedConfigPath}: ${loadError.message}`);
|
|
174
|
-
}
|
|
175
|
-
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const merged = { ...userSecrets };
|
|
179
|
-
for (const key of Object.keys(configSecrets)) {
|
|
180
|
-
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
181
|
-
merged[key] = configSecrets[key];
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return merged;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Loads config secrets path, merges with user secrets (user/master wins, public fills missing).
|
|
189
|
-
* User/master = getPrimaryUserSecretsLocalPath() (config dir; same file as loadPrimaryUserSecrets).
|
|
190
|
-
* Public = aifabrix-secrets path from config. Used by loadSecrets cascade.
|
|
191
|
-
* When the effective shared-secrets target (after remote-dev resolution) is an http(s) URL,
|
|
192
|
-
* fetches shared secrets from API (never persisted to disk).
|
|
193
|
-
*
|
|
194
|
-
* @async
|
|
195
|
-
* @returns {Promise<Object|null>} Merged secrets object or null
|
|
196
|
-
*/
|
|
197
|
-
async function loadMergedConfigAndUserSecrets() {
|
|
198
|
-
const { loadRemoteSharedSecrets, mergeUserWithRemoteSecrets } = require('../utils/remote-secrets-loader');
|
|
199
|
-
const remoteDevAuth = require('../utils/remote-dev-auth');
|
|
200
|
-
const userSecrets = loadPrimaryUserSecrets();
|
|
201
|
-
const hasKeys = (obj) => obj && Object.keys(obj).length > 0;
|
|
202
|
-
const userOrNull = () => (hasKeys(userSecrets) ? userSecrets : null);
|
|
203
|
-
try {
|
|
204
|
-
const configSecretsPath = await config.getSecretsPath();
|
|
205
|
-
if (!configSecretsPath) {
|
|
206
|
-
return userOrNull();
|
|
207
|
-
}
|
|
208
|
-
const effectiveShared = await remoteDevAuth.resolveSharedSecretsEndpoint(configSecretsPath);
|
|
209
|
-
if (remoteDevAuth.isRemoteSecretsUrl(effectiveShared)) {
|
|
210
|
-
const remoteSecrets = await loadRemoteSharedSecrets();
|
|
211
|
-
const merged = mergeUserWithRemoteSecrets(userSecrets, remoteSecrets);
|
|
212
|
-
return hasKeys(merged) ? merged : userOrNull();
|
|
213
|
-
}
|
|
214
|
-
const resolvedConfigPath = path.isAbsolute(configSecretsPath)
|
|
215
|
-
? configSecretsPath
|
|
216
|
-
: path.resolve(process.cwd(), configSecretsPath);
|
|
217
|
-
const merged = mergeUserWithConfigFile(userSecrets, resolvedConfigPath);
|
|
218
|
-
return merged !== null ? merged : userOrNull();
|
|
219
|
-
} catch (error) {
|
|
220
|
-
if (error.message && error.message.startsWith('Failed to load secrets file')) {
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* @returns {string[]}
|
|
229
|
-
*/
|
|
230
|
-
function collectBuilderSecretsYamlPaths() {
|
|
231
|
-
const projectRoot = pathsUtil.getProjectRoot();
|
|
232
|
-
const candidates = [];
|
|
233
|
-
if (projectRoot) {
|
|
234
|
-
candidates.push(path.join(projectRoot, 'builder', 'secrets.local.yaml'));
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
const alt = path.join(pathsUtil.getBuilderRoot(), 'secrets.local.yaml');
|
|
238
|
-
if (!candidates.length || path.resolve(candidates[0]) !== path.resolve(alt)) {
|
|
239
|
-
candidates.push(alt);
|
|
240
|
-
}
|
|
241
|
-
} catch {
|
|
242
|
-
/* ignore */
|
|
243
|
-
}
|
|
244
|
-
return candidates;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Merge `builder/secrets.local.yaml` from project root and from {@link pathsUtil.getBuilderRoot} when distinct.
|
|
249
|
-
* @param {Object|null|undefined} merged
|
|
250
|
-
* @returns {Object|null|undefined}
|
|
251
|
-
*/
|
|
252
|
-
function mergeBuilderSecretsLocalFiles(merged) {
|
|
253
|
-
try {
|
|
254
|
-
const seen = new Set();
|
|
255
|
-
let out = merged;
|
|
256
|
-
for (const builderPath of collectBuilderSecretsYamlPaths()) {
|
|
257
|
-
if (!builderPath || seen.has(path.resolve(builderPath))) {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
seen.add(path.resolve(builderPath));
|
|
261
|
-
if (fs.existsSync(builderPath)) {
|
|
262
|
-
ensureSecureFilePermissions(builderPath);
|
|
263
|
-
const builderSecrets = mergeUserWithConfigFile(out || {}, builderPath);
|
|
264
|
-
if (builderSecrets) out = builderSecrets;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return out;
|
|
268
|
-
} catch {
|
|
269
|
-
return merged;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Loads merged secrets using config/user cascade, builder file merge, and default fallback.
|
|
275
|
-
* @async
|
|
276
|
-
* @returns {Promise<Object>} Merged secrets object (not decrypted)
|
|
277
|
-
*/
|
|
278
|
-
async function loadSecretsWithFallbacks() {
|
|
279
|
-
let merged = await loadMergedConfigAndUserSecrets();
|
|
280
|
-
if (!merged || Object.keys(merged).length === 0) {
|
|
281
|
-
merged = loadPrimaryUserSecrets();
|
|
282
|
-
if (Object.keys(merged).length === 0) {
|
|
283
|
-
merged = loadUserSecrets();
|
|
284
|
-
}
|
|
285
|
-
merged = await applyCanonicalSecretsOverride(merged);
|
|
286
|
-
}
|
|
287
|
-
merged = mergeBuilderSecretsLocalFiles(merged);
|
|
288
|
-
if (Object.keys(merged).length === 0) {
|
|
289
|
-
merged = loadDefaultSecrets();
|
|
290
|
-
}
|
|
291
|
-
return merged;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function loadSecrets(secretsPath, _appName) {
|
|
295
|
-
if (secretsPath) {
|
|
296
|
-
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
297
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
298
|
-
throw new Error(`Secrets file not found: ${resolvedPath}`);
|
|
299
|
-
}
|
|
300
|
-
ensureSecureFilePermissions(resolvedPath);
|
|
301
|
-
const explicitSecrets = readYamlAtPath(resolvedPath);
|
|
302
|
-
if (!explicitSecrets || typeof explicitSecrets !== 'object') {
|
|
303
|
-
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
304
|
-
}
|
|
305
|
-
return await decryptSecretsObject(explicitSecrets);
|
|
306
|
-
}
|
|
307
|
-
let mergedSecrets = await loadSecretsWithFallbacks();
|
|
308
|
-
if (!mergedSecrets || Object.keys(mergedSecrets).length === 0) {
|
|
309
|
-
ensurePrimaryUserSecretsFileExists();
|
|
310
|
-
mergedSecrets = await loadSecretsWithFallbacks();
|
|
311
|
-
}
|
|
312
|
-
return await decryptSecretsObject(mergedSecrets || {});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Resolves kv:// references in environment template
|
|
317
|
-
* Replaces kv://keyName with actual values from secrets
|
|
318
|
-
*
|
|
319
|
-
* @async
|
|
320
|
-
* @function resolveKvReferences
|
|
321
|
-
* @param {string} envTemplate - Environment template content
|
|
322
|
-
* @param {Object} secrets - Secrets object from loadSecrets()
|
|
323
|
-
* @param {string} [environment='local'] - Environment context (docker/local)
|
|
324
|
-
* @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
|
|
325
|
-
* @param {string} [secretsFilePaths.userPath] - User's secrets file path
|
|
326
|
-
* @param {string|null} [secretsFilePaths.buildPath] - App's aifabrix-secrets file path (from config.yaml, if configured)
|
|
327
|
-
* @param {string} [appName] - Application name (optional, for error messages)
|
|
328
|
-
* @returns {Promise<string>} Resolved environment content
|
|
329
|
-
* @throws {Error} If kv:// reference cannot be resolved
|
|
330
|
-
*
|
|
331
|
-
* @example
|
|
332
|
-
* const resolved = await resolveKvReferences(template, secrets, 'local');
|
|
333
|
-
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
334
|
-
*/
|
|
335
|
-
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null, scopedKv = null) {
|
|
336
|
-
const os = require('os');
|
|
337
|
-
|
|
338
|
-
// Get developer-id for port variables (local and docker: *_PUBLIC_PORT = base + devId*100)
|
|
339
|
-
let developerId = null;
|
|
340
|
-
try {
|
|
341
|
-
developerId = await config.getDeveloperId();
|
|
342
|
-
} catch {
|
|
343
|
-
// ignore, buildEnvVarMap will use default
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const envKey = String(environment || 'local').toLowerCase();
|
|
347
|
-
const mapContext = envKey === 'docker' || envKey === 'local' ? envKey : 'local';
|
|
348
|
-
|
|
349
|
-
let envVars = await buildEnvVarMap(mapContext, os, developerId);
|
|
350
|
-
if (!envVars || Object.keys(envVars).length === 0) {
|
|
351
|
-
// Fallback to local environment variables if requested environment does not exist
|
|
352
|
-
envVars = await buildEnvVarMap('local', os, developerId);
|
|
353
|
-
}
|
|
354
|
-
const resolved = interpolateEnvVars(envTemplate, envVars);
|
|
355
|
-
const missing = collectMissingSecrets(resolved, secrets, scopedKv);
|
|
356
|
-
if (missing.length > 0) {
|
|
357
|
-
const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
|
|
358
|
-
const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
|
|
359
|
-
throw new Error(`Missing secrets: ${missing.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
|
|
360
|
-
}
|
|
361
|
-
return replaceKvInContent(resolved, secrets, envVars, scopedKv);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Resolve run env key and effective env-scoped kv/redis behavior for generateEnvContent.
|
|
366
|
-
*
|
|
367
|
-
* @async
|
|
368
|
-
* @param {string} appPath - Builder application directory
|
|
369
|
-
* @param {Object} [options] - generateEnvContent options; may set runEnvKey
|
|
370
|
-
* @returns {Promise<{ runEnvKey: string, effective: boolean }>}
|
|
371
|
-
*/
|
|
372
|
-
async function buildScopedKvContext(appPath, options = {}) {
|
|
373
|
-
let runEnvKey;
|
|
374
|
-
if (options.runEnvKey !== undefined && options.runEnvKey !== null) {
|
|
375
|
-
runEnvKey = String(options.runEnvKey).toLowerCase();
|
|
376
|
-
} else if (typeof config.getCurrentEnvironment === 'function') {
|
|
377
|
-
runEnvKey = String((await config.getCurrentEnvironment()) || 'dev').toLowerCase();
|
|
378
|
-
} else {
|
|
379
|
-
runEnvKey = 'dev';
|
|
380
|
-
}
|
|
381
|
-
const userCfg =
|
|
382
|
-
typeof config.getConfig === 'function'
|
|
383
|
-
? await config.getConfig()
|
|
384
|
-
: { useEnvironmentScopedResources: false };
|
|
385
|
-
const useGate = Boolean(userCfg.useEnvironmentScopedResources);
|
|
386
|
-
const appFlag = readAppEnvironmentScopedFlagForAppPath(appPath);
|
|
387
|
-
const effective = computeEffectiveEnvironmentScopedResources(useGate, appFlag, runEnvKey);
|
|
388
|
-
return { runEnvKey, effective };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Redis/DB service endpoints for docker env interpolation.
|
|
393
|
-
* @returns {Promise<{ redisHost: string, redisPort: number, dbHost: string, dbPort: number }>}
|
|
394
|
-
*/
|
|
395
|
-
async function getDockerRedisDbEndpoints() {
|
|
396
|
-
const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
|
|
397
|
-
const hosts = await getEnvHosts('docker');
|
|
398
|
-
const localhostOverride = getLocalhostOverride('docker');
|
|
399
|
-
const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);
|
|
400
|
-
const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
|
|
401
|
-
const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
|
|
402
|
-
const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
|
|
403
|
-
return { redisHost, redisPort, dbHost, dbPort };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Config inputs for declarative url:// expansion (keeps expandDeclarativeUrlsIfPresent small).
|
|
408
|
-
* @param {string} appPath
|
|
409
|
-
* @returns {Promise<Object>}
|
|
410
|
-
*/
|
|
411
|
-
async function loadDeclarativeUrlExpandInputs(appPath) {
|
|
412
|
-
const userCfg = await config.getConfig();
|
|
413
|
-
let remoteServer = null;
|
|
414
|
-
try {
|
|
415
|
-
const rs = await config.getRemoteServer();
|
|
416
|
-
if (rs && String(rs).trim()) {
|
|
417
|
-
remoteServer = String(rs).trim();
|
|
418
|
-
}
|
|
419
|
-
} catch {
|
|
420
|
-
remoteServer = null;
|
|
421
|
-
}
|
|
422
|
-
let developerIdRaw = null;
|
|
423
|
-
try {
|
|
424
|
-
developerIdRaw = await config.getDeveloperId();
|
|
425
|
-
} catch {
|
|
426
|
-
developerIdRaw = null;
|
|
427
|
-
}
|
|
428
|
-
let infraTlsEnabled = false;
|
|
429
|
-
try {
|
|
430
|
-
infraTlsEnabled = await config.getTlsEnabled();
|
|
431
|
-
} catch {
|
|
432
|
-
infraTlsEnabled = false;
|
|
433
|
-
}
|
|
434
|
-
return {
|
|
435
|
-
userCfg,
|
|
436
|
-
remoteServer,
|
|
437
|
-
developerIdRaw,
|
|
438
|
-
infraTlsEnabled,
|
|
439
|
-
appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* After kv:// resolution, expand url:// references when application config exists.
|
|
445
|
-
* @param {string} resolved
|
|
446
|
-
* @param {string} appName
|
|
447
|
-
* @param {string} appPath
|
|
448
|
-
* @param {string|null} variablesPath
|
|
449
|
-
* @param {string} environment
|
|
450
|
-
* @param {boolean} envOnly
|
|
451
|
-
* @returns {Promise<string>}
|
|
452
|
-
*/
|
|
453
|
-
async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variablesPath, environment, envOnly) {
|
|
454
|
-
if (envOnly || !variablesPath) {
|
|
455
|
-
return resolved;
|
|
456
|
-
}
|
|
457
|
-
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
|
|
458
|
-
await loadDeclarativeUrlExpandInputs(appPath);
|
|
459
|
-
resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
|
|
460
|
-
if (!resolved.includes('url://')) {
|
|
461
|
-
return resolved;
|
|
462
|
-
}
|
|
463
|
-
return expandDeclarativeUrlsInEnvContent(resolved, {
|
|
464
|
-
profile: environment === 'docker' ? 'docker' : 'local',
|
|
465
|
-
currentAppKey: appName,
|
|
466
|
-
variablesPath,
|
|
467
|
-
useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
|
|
468
|
-
appEnvironmentScopedResources: appScopedFlag,
|
|
469
|
-
remoteServer,
|
|
470
|
-
developerIdRaw,
|
|
471
|
-
traefik: Boolean(userCfg.traefik),
|
|
472
|
-
infraTlsEnabled
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** Docker env transformations: ports, infra endpoints, PORT. */
|
|
477
|
-
async function applyDockerTransformations(resolved, variablesPath) {
|
|
478
|
-
resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
|
|
479
|
-
resolved = await rewriteInfraEndpoints(resolved, 'docker');
|
|
480
|
-
const { redisHost, redisPort, dbHost, dbPort } = await getDockerRedisDbEndpoints();
|
|
481
|
-
let dockerEnv = await getBaseDockerEnv();
|
|
482
|
-
dockerEnv = applyDockerEnvOverride(dockerEnv);
|
|
483
|
-
const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
|
|
484
|
-
const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
|
|
485
|
-
const appDoc = loadVariablesFromPath(variablesPath);
|
|
486
|
-
await mergeDockerManifestPublishedPort(envVars, appDoc);
|
|
487
|
-
envVars.REDIS_HOST = redisHost;
|
|
488
|
-
envVars.REDIS_PORT = String(redisPort);
|
|
489
|
-
envVars.DB_HOST = dbHost;
|
|
490
|
-
envVars.DB_PORT = String(dbPort);
|
|
491
|
-
envVars.PORT = String(containerPort);
|
|
492
|
-
resolved = interpolateEnvVars(resolved, envVars);
|
|
493
|
-
resolved = rewriteDockerManifestPublicPortEnvLine(resolved, envVars, appDoc);
|
|
494
|
-
return updatePortForDocker(resolved, variablesPath);
|
|
495
|
-
}
|
|
496
|
-
/** Environment-specific transformations to resolved content. */
|
|
497
|
-
async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
|
|
498
|
-
if (environment === 'docker') return applyDockerTransformations(resolved, variablesPath);
|
|
499
|
-
if (environment === 'local') return adjustLocalEnvPortsInContent(resolved, variablesPath);
|
|
500
|
-
return resolved;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Generate .env content from template and secrets (no disk write).
|
|
505
|
-
* When options.envOnly is true, variablesPath is null (no application config).
|
|
506
|
-
*
|
|
507
|
-
* @param {string} appName - Application name
|
|
508
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
509
|
-
* @param {string} [environment='local'] - Environment context
|
|
510
|
-
* @param {boolean} [force=false] - Generate missing secret keys
|
|
511
|
-
* @param {Object} [options] - Optional: appPath, envOnly (env-only mode uses only env.template)
|
|
512
|
-
* @returns {Promise<string>} Resolved env content
|
|
513
|
-
*/
|
|
514
|
-
async function generateEnvContent(appName, secretsPath, environment = 'local', force = false, options = {}) {
|
|
515
|
-
const appPath = (options && options.appPath) || pathsUtil.getBuilderPath(appName);
|
|
516
|
-
const templatePath = path.join(appPath, 'env.template');
|
|
517
|
-
const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
|
|
518
|
-
const template = loadEnvTemplate(templatePath);
|
|
519
|
-
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
520
|
-
if (force) {
|
|
521
|
-
const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
|
|
522
|
-
await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
|
|
523
|
-
}
|
|
524
|
-
const secrets = await loadSecrets(secretsPath, appName);
|
|
525
|
-
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
526
|
-
const scopedKv = { envKey: runEnvKey, effective };
|
|
527
|
-
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, scopedKv);
|
|
528
|
-
resolved = await expandDeclarativeUrlsIfPresent(
|
|
529
|
-
resolved,
|
|
530
|
-
appName,
|
|
531
|
-
appPath,
|
|
532
|
-
variablesPath,
|
|
533
|
-
environment,
|
|
534
|
-
Boolean(options.envOnly)
|
|
535
|
-
);
|
|
536
|
-
resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
|
|
537
|
-
if (effective) {
|
|
538
|
-
const idx = redisDbIndexForScopedRunEnv(runEnvKey);
|
|
539
|
-
resolved = applyRedisDbIndexToEnvContent(resolved, idx);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return resolved;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Parses .env file content into a key-to-value map.
|
|
547
|
-
* Only includes lines that look like KEY=value (first = separates key and value).
|
|
548
|
-
*
|
|
549
|
-
* @function parseEnvContentToMap
|
|
550
|
-
* @param {string} content - Raw .env file content
|
|
551
|
-
* @returns {Object.<string, string>} Map of variable name to value
|
|
552
|
-
*/
|
|
553
|
-
function parseEnvContentToMap(content) {
|
|
554
|
-
if (!content || typeof content !== 'string') {
|
|
555
|
-
return {};
|
|
556
|
-
}
|
|
557
|
-
const map = {};
|
|
558
|
-
const lines = content.split(/\r?\n/);
|
|
559
|
-
for (const line of lines) {
|
|
560
|
-
const trimmed = line.trim();
|
|
561
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const eq = trimmed.indexOf('=');
|
|
565
|
-
if (eq > 0) {
|
|
566
|
-
const key = trimmed.substring(0, eq).trim();
|
|
567
|
-
const value = trimmed.substring(eq + 1);
|
|
568
|
-
map[key] = value;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return map;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Merges new .env content with existing .env: newly resolved content wins for keys it
|
|
576
|
-
* defines (so project secrets take effect when re-running). Keys only in existing .env
|
|
577
|
-
* are appended so manual additions are kept.
|
|
578
|
-
*
|
|
579
|
-
* @function mergeEnvContentPreservingExisting
|
|
580
|
-
* @param {string} newContent - Newly generated .env content (from template + loadSecrets)
|
|
581
|
-
* @param {Object.<string, string>} existingMap - Existing key-to-value map from current .env
|
|
582
|
-
* @returns {string} Merged content: new values for keys in newContent, plus extra existing keys
|
|
583
|
-
*/
|
|
584
|
-
function mergeEnvContentPreservingExisting(newContent, existingMap) {
|
|
585
|
-
const lines = newContent.split(/\r?\n/);
|
|
586
|
-
const newKeys = new Set();
|
|
587
|
-
const out = [];
|
|
588
|
-
for (const line of lines) {
|
|
589
|
-
const trimmed = line.trim();
|
|
590
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
591
|
-
out.push(line);
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
const eq = trimmed.indexOf('=');
|
|
595
|
-
if (eq > 0) {
|
|
596
|
-
const key = trimmed.substring(0, eq).trim();
|
|
597
|
-
newKeys.add(key);
|
|
598
|
-
}
|
|
599
|
-
out.push(line);
|
|
600
|
-
}
|
|
601
|
-
if (existingMap && Object.keys(existingMap).length > 0) {
|
|
602
|
-
for (const key of Object.keys(existingMap)) {
|
|
603
|
-
if (!newKeys.has(key)) {
|
|
604
|
-
out.push(`${key}=${existingMap[key]}`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return out.join('\n');
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Merges a key-value map into existing .env file content, preserving comments and blank lines.
|
|
613
|
-
* For each KEY=value line in existing content, replaces value with newMap[KEY] when the key exists
|
|
614
|
-
* in newMap. Appends any keys from newMap that did not appear in the file.
|
|
615
|
-
*
|
|
616
|
-
* @function mergeEnvMapIntoContent
|
|
617
|
-
* @param {string} existingContent - Full existing .env file content
|
|
618
|
-
* @param {Object.<string, string>} newMap - New key-to-value map (e.g. from resolved or run env)
|
|
619
|
-
* @returns {string} Merged content with comments preserved
|
|
620
|
-
*/
|
|
621
|
-
function mergeEnvMapIntoContent(existingContent, newMap) {
|
|
622
|
-
if (!newMap || Object.keys(newMap).length === 0) {
|
|
623
|
-
return typeof existingContent === 'string' ? existingContent : '';
|
|
624
|
-
}
|
|
625
|
-
const lines = (existingContent || '').split(/\r?\n/);
|
|
626
|
-
const seen = new Set();
|
|
627
|
-
const out = [];
|
|
628
|
-
for (const line of lines) {
|
|
629
|
-
const trimmed = line.trim();
|
|
630
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
631
|
-
out.push(line);
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
const eq = trimmed.indexOf('=');
|
|
635
|
-
if (eq > 0) {
|
|
636
|
-
const key = trimmed.substring(0, eq).trim();
|
|
637
|
-
seen.add(key);
|
|
638
|
-
out.push(Object.prototype.hasOwnProperty.call(newMap, key) ? `${key}=${newMap[key]}` : line);
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
out.push(line);
|
|
642
|
-
}
|
|
643
|
-
for (const key of Object.keys(newMap)) {
|
|
644
|
-
if (!seen.has(key)) out.push(`${key}=${newMap[key]}`);
|
|
645
|
-
}
|
|
646
|
-
return out.join('\n');
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Resolves content to write for .env: merges with existing file when present.
|
|
651
|
-
* @param {string} resolved - Newly generated content
|
|
652
|
-
* @param {string} pathToPreserve - Path to existing .env to merge from (or null)
|
|
653
|
-
* @returns {string} Content to write
|
|
654
|
-
*/
|
|
655
|
-
function resolveEnvContentToWrite(resolved, pathToPreserve) {
|
|
656
|
-
if (!pathToPreserve || !fs.existsSync(pathToPreserve)) return resolved;
|
|
657
|
-
const existingContent = fs.readFileSync(pathToPreserve, 'utf8');
|
|
658
|
-
const existingMap = parseEnvContentToMap(existingContent);
|
|
659
|
-
return mergeEnvContentPreservingExisting(resolved, existingMap);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Generates and writes .env file. Newly resolved values win over existing .env; extra vars in existing .env are kept.
|
|
664
|
-
* When options.envOnly is true, only env.template is used; .env is written to options.appPath.
|
|
665
|
-
* @async
|
|
666
|
-
* @function generateEnvFile
|
|
667
|
-
* @param {string} appName - Name of the application
|
|
668
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
669
|
-
* @param {string} [environment='local'] - Environment context ('local' or 'docker')
|
|
670
|
-
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
671
|
-
* @param {Object} [options] - Optional: appPath, envOnly, skipOutputPath, preserveFromPath
|
|
672
|
-
* @returns {Promise<string>} Path to generated .env file
|
|
673
|
-
*/
|
|
674
|
-
async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, options = {}) {
|
|
675
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
676
|
-
const appPath = opts.appPath || pathsUtil.getBuilderPath(appName);
|
|
677
|
-
const envOnly = !!opts.envOnly;
|
|
678
|
-
const variablesPath = envOnly ? null : resolveApplicationConfigPath(appPath);
|
|
679
|
-
const envPath = path.join(appPath, '.env');
|
|
680
|
-
|
|
681
|
-
if (envOnly) {
|
|
682
|
-
const templatePath = path.join(appPath, 'env.template');
|
|
683
|
-
if (!fs.existsSync(templatePath)) {
|
|
684
|
-
throw new Error(`env.template not found at ${templatePath}. Resolve requires env.template in the app directory.`);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
|
|
689
|
-
const preservePath = opts.preserveFromPath !== undefined && opts.preserveFromPath !== null ? opts.preserveFromPath : null;
|
|
690
|
-
const pathToPreserve = preservePath !== null ? preservePath : envPath;
|
|
691
|
-
const toWrite = resolveEnvContentToWrite(resolved, pathToPreserve);
|
|
692
|
-
fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
|
|
693
|
-
|
|
694
|
-
if (!opts.skipOutputPath) {
|
|
695
|
-
const { processEnvVariables } = require('../utils/env-copy');
|
|
696
|
-
await processEnvVariables(envPath, variablesPath, appName, secretsPath);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
return envPath;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Writes admin env key-value pairs to content; encrypts values when encryption key is set.
|
|
704
|
-
* @async
|
|
705
|
-
* @param {Object.<string, string>} adminObj - Key-value object (e.g. POSTGRES_PASSWORD, ...)
|
|
706
|
-
* @returns {Promise<string>} .env-style content (plaintext or secure:// for secrets)
|
|
707
|
-
*/
|
|
708
|
-
async function formatAdminSecretsContent(adminObj) {
|
|
709
|
-
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
710
|
-
const { encryptSecret } = require('../utils/secrets-encryption');
|
|
711
|
-
const lines = ['# Infrastructure Admin Credentials'];
|
|
712
|
-
for (const [k, v] of Object.entries(adminObj)) {
|
|
713
|
-
const value = (v === null || v === undefined) ? '' : String(v).replace(/\n/g, ' ').trim();
|
|
714
|
-
const valueToWrite = encryptionKey ? encryptSecret(value, encryptionKey) : value;
|
|
715
|
-
lines.push(`${k}=${valueToWrite}`);
|
|
716
|
-
}
|
|
717
|
-
return lines.join('\n');
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
async function loadSecretsOrBootstrapForAdmin(secretsPath) {
|
|
721
|
-
try {
|
|
722
|
-
return await loadSecrets(secretsPath);
|
|
723
|
-
} catch (error) {
|
|
724
|
-
const defaultSecretsPath = secretsPath || path.join(pathsUtil.getAifabrixHome(), 'secrets.yaml');
|
|
725
|
-
if (!fs.existsSync(defaultSecretsPath)) {
|
|
726
|
-
logger.log('Creating default secrets file...');
|
|
727
|
-
await createDefaultSecrets(defaultSecretsPath);
|
|
728
|
-
return await loadSecrets(secretsPath);
|
|
729
|
-
}
|
|
730
|
-
throw error;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function getInfraDefaultsMergedForAdmin() {
|
|
735
|
-
try {
|
|
736
|
-
return mergeInfraParameterDefaultsForCli(getInfraParameterCatalog().data, {});
|
|
737
|
-
} catch {
|
|
738
|
-
return {};
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function buildLocalAdminSecretsObject(secrets, infraDefaults) {
|
|
743
|
-
const raw = secrets['postgres-passwordKeyVault'];
|
|
744
|
-
const relaxed = readRelaxedCatalogDefaults();
|
|
745
|
-
const postgresPassword =
|
|
746
|
-
(raw && String(raw).trim()) ||
|
|
747
|
-
infraDefaults.adminPassword ||
|
|
748
|
-
relaxed.adminPassword ||
|
|
749
|
-
'';
|
|
750
|
-
const pgAdminEmail = infraDefaults.adminEmail || relaxed.adminEmail || '';
|
|
751
|
-
return {
|
|
752
|
-
POSTGRES_PASSWORD: postgresPassword,
|
|
753
|
-
PGADMIN_DEFAULT_EMAIL: pgAdminEmail,
|
|
754
|
-
PGADMIN_DEFAULT_PASSWORD: postgresPassword,
|
|
755
|
-
REDIS_HOST: 'local:redis:6379:0:',
|
|
756
|
-
REDIS_COMMANDER_USER: 'admin',
|
|
757
|
-
REDIS_COMMANDER_PASSWORD: postgresPassword
|
|
758
|
-
};
|
|
759
|
-
}
|
|
30
|
+
formatAdminSecretsContent,
|
|
31
|
+
generateAdminSecretsEnv
|
|
32
|
+
} = require('./secrets-admin-env');
|
|
33
|
+
const { getCanonicalSecretName } = require('./secrets-names');
|
|
760
34
|
|
|
761
|
-
/** Generates admin secrets for infrastructure (beside config.yaml, typically ~/.aifabrix/admin-secrets.env). Defaults from infra.parameter.yaml `defaults`. */
|
|
762
|
-
async function generateAdminSecretsEnv(secretsPath) {
|
|
763
|
-
const secrets = await loadSecretsOrBootstrapForAdmin(secretsPath);
|
|
764
|
-
const infraDefaults = getInfraDefaultsMergedForAdmin();
|
|
765
|
-
const adminObj = buildLocalAdminSecretsObject(secrets, infraDefaults);
|
|
766
|
-
const aifabrixDir = pathsUtil.getAifabrixSystemDir();
|
|
767
|
-
const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
768
|
-
if (!fs.existsSync(aifabrixDir)) {
|
|
769
|
-
fs.mkdirSync(aifabrixDir, { recursive: true, mode: 0o700 });
|
|
770
|
-
}
|
|
771
|
-
const adminSecrets = await formatAdminSecretsContent(adminObj);
|
|
772
|
-
fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
|
|
773
|
-
return adminEnvPath;
|
|
774
|
-
}
|
|
775
35
|
module.exports = {
|
|
776
36
|
loadSecrets,
|
|
777
37
|
resolveKvReferences,
|