@aifabrix/builder 2.40.0 → 2.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +29 -0
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-auth.js +1 -0
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +19 -4
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/auth-status.js +36 -3
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +13 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +49 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validate.js +1 -1
- package/lib/validation/validator.js +65 -0
- package/package.json +4 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/external-system/external-system.json.hbs +1 -16
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder – Ensure secrets in configured store
|
|
3
|
+
*
|
|
4
|
+
* Ensures missing secret keys exist in the correct store (file path, remote API, or
|
|
5
|
+
* user secrets file). New values are encrypted when writing to file and
|
|
6
|
+
* secrets-encryption is set. Remote write tries API first; on failure falls back
|
|
7
|
+
* to user file with a warning.
|
|
8
|
+
*
|
|
9
|
+
* @fileoverview Central ensure-secrets service for zero-touch install
|
|
10
|
+
* @author AI Fabrix Team
|
|
11
|
+
* @version 2.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const config = require('./config');
|
|
18
|
+
const pathsUtil = require('../utils/paths');
|
|
19
|
+
const logger = require('../utils/logger');
|
|
20
|
+
const { isRemoteSecretsUrl, getRemoteDevAuth } = require('../utils/remote-dev-auth');
|
|
21
|
+
const devApi = require('../api/dev.api');
|
|
22
|
+
const {
|
|
23
|
+
findMissingSecretKeys,
|
|
24
|
+
generateSecretValue,
|
|
25
|
+
loadExistingSecrets,
|
|
26
|
+
appendSecretsToFile,
|
|
27
|
+
saveSecretsFile
|
|
28
|
+
} = require('../utils/secrets-generator');
|
|
29
|
+
const { encryptSecret } = require('../utils/secrets-encryption');
|
|
30
|
+
const { loadEnvTemplate } = require('../utils/secrets-helpers');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Expand leading ~ to home directory.
|
|
34
|
+
* @param {string} filePath - Path that may start with ~
|
|
35
|
+
* @returns {string} Resolved path
|
|
36
|
+
*/
|
|
37
|
+
function expandTilde(filePath) {
|
|
38
|
+
if (!filePath || typeof filePath !== 'string') return filePath;
|
|
39
|
+
if (filePath === '~') return os.homedir();
|
|
40
|
+
if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
|
|
41
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
42
|
+
}
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve write target from config.
|
|
48
|
+
* - File path → that path (expand ~)
|
|
49
|
+
* - http(s) URL → remote (fallback: user file)
|
|
50
|
+
* - No config → user file
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<{ type: 'file'|'remote', filePath?: string, serverUrl?: string, clientCertPem?: string }>}
|
|
53
|
+
*/
|
|
54
|
+
async function resolveWriteTarget() {
|
|
55
|
+
const secretsPath = await config.getSecretsPath();
|
|
56
|
+
const userFilePath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
57
|
+
|
|
58
|
+
if (!secretsPath) {
|
|
59
|
+
return { type: 'file', filePath: userFilePath };
|
|
60
|
+
}
|
|
61
|
+
if (isRemoteSecretsUrl(secretsPath)) {
|
|
62
|
+
const auth = await getRemoteDevAuth();
|
|
63
|
+
return {
|
|
64
|
+
type: 'remote',
|
|
65
|
+
filePath: userFilePath,
|
|
66
|
+
serverUrl: secretsPath.replace(/\/+$/, ''),
|
|
67
|
+
clientCertPem: auth ? auth.clientCertPem : null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const filePath = path.isAbsolute(secretsPath)
|
|
71
|
+
? secretsPath
|
|
72
|
+
: path.resolve(process.cwd(), expandTilde(secretsPath));
|
|
73
|
+
return { type: 'file', filePath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load existing secrets from the resolved target (file or remote).
|
|
78
|
+
*
|
|
79
|
+
* @param {{ type: string, filePath?: string, serverUrl?: string, clientCertPem?: string }} target
|
|
80
|
+
* @returns {Promise<Object>} Existing secrets key-value object
|
|
81
|
+
*/
|
|
82
|
+
async function loadExistingFromTarget(target) {
|
|
83
|
+
if (target.type === 'file' && target.filePath) {
|
|
84
|
+
return loadExistingSecrets(target.filePath);
|
|
85
|
+
}
|
|
86
|
+
if (target.type === 'remote' && target.serverUrl && target.clientCertPem) {
|
|
87
|
+
try {
|
|
88
|
+
const items = await devApi.listSecrets(target.serverUrl, target.clientCertPem);
|
|
89
|
+
if (!Array.isArray(items)) return {};
|
|
90
|
+
const obj = {};
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
if (item && typeof item.name === 'string' && item.value !== undefined) {
|
|
93
|
+
obj[item.name] = String(item.value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return obj;
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (target.type === 'remote' && target.filePath) {
|
|
102
|
+
return loadExistingSecrets(target.filePath);
|
|
103
|
+
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Write a single secret to file, optionally encrypting the value.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} filePath - Resolved file path
|
|
111
|
+
* @param {string} key - Secret key
|
|
112
|
+
* @param {string} value - Plain value
|
|
113
|
+
* @param {string|null} encryptionKey - Config secrets-encryption key or null
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
async function writeSecretToFile(filePath, key, value, encryptionKey) {
|
|
117
|
+
const dir = path.dirname(filePath);
|
|
118
|
+
if (!fs.existsSync(dir)) {
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
120
|
+
}
|
|
121
|
+
let valueToWrite = value;
|
|
122
|
+
if (encryptionKey && value !== '') {
|
|
123
|
+
try {
|
|
124
|
+
valueToWrite = encryptSecret(value, encryptionKey);
|
|
125
|
+
} catch {
|
|
126
|
+
// Keep plaintext if encryption fails
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
appendSecretsToFile(filePath, { [key]: valueToWrite });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Compute value for a key (suggested or generated).
|
|
134
|
+
* @param {string} key - Secret key
|
|
135
|
+
* @param {Object} suggested - Map of suggested values
|
|
136
|
+
* @param {boolean} emptyForCredentials - Use empty string if true
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
function valueForKey(key, suggested, emptyForCredentials) {
|
|
140
|
+
if (key in suggested) return String(suggested[key]);
|
|
141
|
+
return emptyForCredentials ? '' : generateSecretValue(key);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add secrets via remote API; on failure write to local file (with encryption if configured).
|
|
146
|
+
* @param {Object} target - Resolved target (remote)
|
|
147
|
+
* @param {string[]} toAdd - Keys to add
|
|
148
|
+
* @param {Object} suggested - Suggested values
|
|
149
|
+
* @param {string[]} added - Array to push added keys to
|
|
150
|
+
* @param {string|null} encryptionKey - Encryption key for file fallback or null
|
|
151
|
+
* @returns {Promise<string[]>}
|
|
152
|
+
*/
|
|
153
|
+
async function addSecretsRemote(target, toAdd, suggested, added, encryptionKey) {
|
|
154
|
+
const emptyForCredentials = false;
|
|
155
|
+
for (const key of toAdd) {
|
|
156
|
+
const value = valueForKey(key, suggested, emptyForCredentials);
|
|
157
|
+
try {
|
|
158
|
+
await devApi.addSecret(target.serverUrl, target.clientCertPem, { key, value });
|
|
159
|
+
added.push(key);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logger.warn(`Remote secret "${key}" failed (${err.message}); writing to local file.`);
|
|
162
|
+
await writeSecretToFile(target.filePath, key, value, encryptionKey);
|
|
163
|
+
added.push(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return added;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Add secrets to file (with optional encryption).
|
|
171
|
+
* @param {string} filePath - File path
|
|
172
|
+
* @param {string[]} toAdd - Keys to add
|
|
173
|
+
* @param {Object} suggested - Suggested values
|
|
174
|
+
* @param {boolean} emptyForCredentials - Use empty for new values
|
|
175
|
+
* @param {string|null} encryptionKey - Encryption key or null
|
|
176
|
+
* @param {string[]} added - Array to push added keys to
|
|
177
|
+
* @returns {Promise<string[]>}
|
|
178
|
+
*/
|
|
179
|
+
async function addSecretsToFile(filePath, toAdd, suggested, emptyForCredentials, encryptionKey, added) {
|
|
180
|
+
for (const key of toAdd) {
|
|
181
|
+
const value = valueForKey(key, suggested, emptyForCredentials);
|
|
182
|
+
await writeSecretToFile(filePath, key, value, encryptionKey);
|
|
183
|
+
added.push(key);
|
|
184
|
+
}
|
|
185
|
+
if (added.length > 0) {
|
|
186
|
+
logger.log(`✓ Ensured ${added.length} secret key(s): ${added.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
return added;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Ensure a list of secret keys exists in the configured store.
|
|
193
|
+
* Only adds keys that are missing or empty. Uses generateSecretValue for new
|
|
194
|
+
* values unless emptyValuesForCredentials is true (then empty string).
|
|
195
|
+
*
|
|
196
|
+
* @async
|
|
197
|
+
* @function ensureSecretsForKeys
|
|
198
|
+
* @param {string[]} keys - Secret keys to ensure
|
|
199
|
+
* @param {Object} [options] - Options
|
|
200
|
+
* @param {boolean} [options.emptyValuesForCredentials=false] - Use empty string for new values
|
|
201
|
+
* @param {Object} [options.suggestedValues] - Optional map key -> value for specific keys
|
|
202
|
+
* @returns {Promise<string[]>} Keys that were added (new or backfilled)
|
|
203
|
+
* @throws {Error} If config or file write fails
|
|
204
|
+
*/
|
|
205
|
+
async function ensureSecretsForKeys(keys, options = {}) {
|
|
206
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const emptyForCredentials = Boolean(options.emptyValuesForCredentials);
|
|
210
|
+
const suggested = options.suggestedValues && typeof options.suggestedValues === 'object'
|
|
211
|
+
? options.suggestedValues
|
|
212
|
+
: {};
|
|
213
|
+
|
|
214
|
+
const target = options._targetOverride || await resolveWriteTarget();
|
|
215
|
+
const existing = await loadExistingFromTarget(target);
|
|
216
|
+
const toAdd = keys.filter((k) => {
|
|
217
|
+
const v = existing[k];
|
|
218
|
+
const missingOrEmpty = v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
|
|
219
|
+
return missingOrEmpty && !KEYS_ALLOWED_EMPTY.has(k);
|
|
220
|
+
});
|
|
221
|
+
if (toAdd.length === 0) return [];
|
|
222
|
+
|
|
223
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
224
|
+
const added = [];
|
|
225
|
+
|
|
226
|
+
if (target.type === 'remote' && target.serverUrl && target.clientCertPem) {
|
|
227
|
+
return addSecretsRemote(target, toAdd, suggested, added, encryptionKey);
|
|
228
|
+
}
|
|
229
|
+
return addSecretsToFile(target.filePath, toAdd, suggested, emptyForCredentials, encryptionKey, added);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Ensure secrets referenced in an env template exist in the configured store.
|
|
234
|
+
* Reads template from path or uses content if content is provided (string with kv://).
|
|
235
|
+
*
|
|
236
|
+
* @async
|
|
237
|
+
* @function ensureSecretsFromEnvTemplate
|
|
238
|
+
* @param {string} envTemplatePathOrContent - Path to env.template or template content
|
|
239
|
+
* @param {Object} [options] - Options
|
|
240
|
+
* @param {boolean} [options.emptyValuesForCredentials=false] - Use empty string for new values
|
|
241
|
+
* @returns {Promise<string[]>} Keys that were added
|
|
242
|
+
* @throws {Error} If template cannot be read or ensure fails
|
|
243
|
+
*/
|
|
244
|
+
async function ensureSecretsFromEnvTemplate(envTemplatePathOrContent, options = {}) {
|
|
245
|
+
let template;
|
|
246
|
+
const input = typeof envTemplatePathOrContent === 'string' ? envTemplatePathOrContent : '';
|
|
247
|
+
const looksLikePath = input.length > 0 && !input.includes('\n') && !input.includes('kv://');
|
|
248
|
+
if (looksLikePath && fs.existsSync(input)) {
|
|
249
|
+
template = loadEnvTemplate(input);
|
|
250
|
+
} else if (input.includes('kv://')) {
|
|
251
|
+
template = input;
|
|
252
|
+
} else if (looksLikePath) {
|
|
253
|
+
const err = new Error(`env.template not found: ${input}`);
|
|
254
|
+
err.code = 'ENOENT';
|
|
255
|
+
throw err;
|
|
256
|
+
} else {
|
|
257
|
+
throw new Error('env.template path or content is required');
|
|
258
|
+
}
|
|
259
|
+
let target;
|
|
260
|
+
if (options.preferredFilePath && typeof options.preferredFilePath === 'string') {
|
|
261
|
+
const filePath = path.isAbsolute(options.preferredFilePath)
|
|
262
|
+
? options.preferredFilePath
|
|
263
|
+
: path.resolve(process.cwd(), options.preferredFilePath);
|
|
264
|
+
target = { type: 'file', filePath };
|
|
265
|
+
} else {
|
|
266
|
+
target = await resolveWriteTarget();
|
|
267
|
+
}
|
|
268
|
+
const existing = await loadExistingFromTarget(target);
|
|
269
|
+
const missingKeys = findMissingSecretKeys(template, existing);
|
|
270
|
+
return ensureSecretsForKeys(missingKeys, { ...options, _targetOverride: target });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Infra secret keys used by createDefaultSecrets / keyvault.md for up-infra.
|
|
275
|
+
* Includes miso-controller DB keys so ensureMisoInitScript can read from store.
|
|
276
|
+
*
|
|
277
|
+
* postgres-passwordKeyVault: Postgres superuser/admin password for local Docker Postgres,
|
|
278
|
+
* PgAdmin, and Redis Commander (see generateAdminSecretsEnv in lib/core/secrets.js).
|
|
279
|
+
*
|
|
280
|
+
* @type {string[]}
|
|
281
|
+
*/
|
|
282
|
+
const INFRA_SECRET_KEYS = [
|
|
283
|
+
'postgres-passwordKeyVault',
|
|
284
|
+
'redis-passwordKeyVault',
|
|
285
|
+
'redis-url',
|
|
286
|
+
'keycloak-admin-passwordKeyVault',
|
|
287
|
+
'keycloak-server-url',
|
|
288
|
+
'databases-miso-controller-0-passwordKeyVault',
|
|
289
|
+
'databases-miso-controller-0-urlKeyVault'
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Keys that are valid when empty (e.g. no Redis password in local dev).
|
|
294
|
+
* We do not backfill these when empty, so the file is not appended with a duplicate key.
|
|
295
|
+
*
|
|
296
|
+
* @type {Set<string>}
|
|
297
|
+
*/
|
|
298
|
+
const KEYS_ALLOWED_EMPTY = new Set(['redis-passwordKeyVault']);
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Write one key-value to a file store (load, merge, optionally encrypt, save).
|
|
302
|
+
* @param {string} filePath - Secrets file path
|
|
303
|
+
* @param {string} key - Secret key
|
|
304
|
+
* @param {string} strValue - Plain value
|
|
305
|
+
* @returns {Promise<void>}
|
|
306
|
+
*/
|
|
307
|
+
async function writeSecretToStoreFile(filePath, key, strValue) {
|
|
308
|
+
const existing = loadExistingSecrets(filePath);
|
|
309
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
310
|
+
let valueToWrite = strValue;
|
|
311
|
+
if (encryptionKey && strValue !== '') {
|
|
312
|
+
try {
|
|
313
|
+
valueToWrite = encryptSecret(strValue, encryptionKey);
|
|
314
|
+
} catch {
|
|
315
|
+
// Keep plaintext if encryption fails
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
existing[key] = valueToWrite;
|
|
319
|
+
const dir = path.dirname(filePath);
|
|
320
|
+
if (!fs.existsSync(dir)) {
|
|
321
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
322
|
+
}
|
|
323
|
+
saveSecretsFile(filePath, existing);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Write a single secret to the configured store (overwrites if key exists).
|
|
328
|
+
* Used when syncing e.g. postgres-passwordKeyVault after --adminPwd override.
|
|
329
|
+
*
|
|
330
|
+
* @async
|
|
331
|
+
* @function setSecretInStore
|
|
332
|
+
* @param {string} key - Secret key
|
|
333
|
+
* @param {string} value - Plain value
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
async function setSecretInStore(key, value) {
|
|
337
|
+
if (!key || typeof key !== 'string' || value === undefined) return;
|
|
338
|
+
const target = await resolveWriteTarget();
|
|
339
|
+
const strValue = typeof value === 'string' ? value : String(value);
|
|
340
|
+
if (target.type === 'remote' && target.serverUrl && target.clientCertPem) {
|
|
341
|
+
try {
|
|
342
|
+
await devApi.addSecret(target.serverUrl, target.clientCertPem, { key, value: strValue });
|
|
343
|
+
} catch (err) {
|
|
344
|
+
logger.warn(`Could not sync secret "${key}" to remote store: ${err.message}`);
|
|
345
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
346
|
+
await writeSecretToFile(target.filePath, key, strValue, encryptionKey);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await writeSecretToStoreFile(target.filePath, key, strValue);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Ensure infra secrets exist in the configured store. Call before ensureAdminSecrets or startInfra.
|
|
355
|
+
*
|
|
356
|
+
* @async
|
|
357
|
+
* @function ensureInfraSecrets
|
|
358
|
+
* @param {Object} [options] - Options
|
|
359
|
+
* @param {string} [options.adminPwd] - Override for postgres-passwordKeyVault when creating new secrets
|
|
360
|
+
* @returns {Promise<string[]>} Keys that were added
|
|
361
|
+
*/
|
|
362
|
+
async function ensureInfraSecrets(options = {}) {
|
|
363
|
+
const suggested = {};
|
|
364
|
+
if (options.adminPwd && typeof options.adminPwd === 'string' && options.adminPwd.trim() !== '') {
|
|
365
|
+
suggested['postgres-passwordKeyVault'] = options.adminPwd.trim();
|
|
366
|
+
}
|
|
367
|
+
return ensureSecretsForKeys(INFRA_SECRET_KEYS, { suggestedValues: suggested });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
ensureSecretsForKeys,
|
|
372
|
+
ensureSecretsFromEnvTemplate,
|
|
373
|
+
ensureInfraSecrets,
|
|
374
|
+
setSecretInStore,
|
|
375
|
+
resolveWriteTarget,
|
|
376
|
+
loadExistingFromTarget,
|
|
377
|
+
INFRA_SECRET_KEYS
|
|
378
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve .env in memory and write only to envOutputPath or temp (no builder/ or integration/).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Single .env write for run flow (plan 66)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if env content has a non-empty value for a given key (KEY=value line).
|
|
17
|
+
* Returns false when value is empty or an unresolved kv:// reference.
|
|
18
|
+
* @param {string} content - .env-style content
|
|
19
|
+
* @param {string} key - Variable name (e.g. NPM_TOKEN)
|
|
20
|
+
* @returns {boolean} True if key exists with a real value (non-empty, not kv://)
|
|
21
|
+
*/
|
|
22
|
+
function envContentHasKey(content, key) {
|
|
23
|
+
if (!content || typeof content !== 'string') return false;
|
|
24
|
+
const re = new RegExp(`^${key}=(.+)$`, 'm');
|
|
25
|
+
const m = content.match(re);
|
|
26
|
+
if (!m || !m[1]) return false;
|
|
27
|
+
const val = String(m[1]).trim();
|
|
28
|
+
return val.length > 0 && !val.startsWith('kv://');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get secret value trying common key variants (e.g. npm_token, NPM_TOKEN, npm-token).
|
|
33
|
+
* @param {Object} secrets - Loaded secrets object
|
|
34
|
+
* @param {string} preferred - Preferred key (e.g. 'NPM_TOKEN')
|
|
35
|
+
* @param {string} alternate - Alternate key (e.g. 'npm_token')
|
|
36
|
+
* @returns {string|null} Value or null
|
|
37
|
+
*/
|
|
38
|
+
function getSecretForEnvVar(secrets, preferred, alternate) {
|
|
39
|
+
if (!secrets || typeof secrets !== 'object') return null;
|
|
40
|
+
const keys = [preferred, alternate];
|
|
41
|
+
if (alternate.includes('_')) keys.push(alternate.replace('_', '-'));
|
|
42
|
+
for (const k of keys) {
|
|
43
|
+
const v = secrets[k];
|
|
44
|
+
if (v !== null && v !== undefined && String(v).trim() !== '') return String(v).trim();
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Inject NPM_TOKEN and PYPI_TOKEN from loaded secrets into content when missing.
|
|
51
|
+
* @param {string} content - .env-style content
|
|
52
|
+
* @param {string|null} secretsPath - Path to secrets file
|
|
53
|
+
* @param {string} appName - Application name
|
|
54
|
+
* @returns {Promise<string>} Content with tokens injected when possible
|
|
55
|
+
*/
|
|
56
|
+
async function injectRegistryTokens(content, secretsPath, appName) {
|
|
57
|
+
const secrets = require('./secrets');
|
|
58
|
+
try {
|
|
59
|
+
const loadedSecrets = await secrets.loadSecrets(secretsPath, appName);
|
|
60
|
+
let out = content || '';
|
|
61
|
+
if (!envContentHasKey(out, 'NPM_TOKEN')) {
|
|
62
|
+
const v = getSecretForEnvVar(loadedSecrets, 'NPM_TOKEN', 'npm_token');
|
|
63
|
+
if (v) out = out.trimEnd() + (out.endsWith('\n') ? '' : '\n') + `NPM_TOKEN=${v}\n`;
|
|
64
|
+
}
|
|
65
|
+
if (!envContentHasKey(out, 'PYPI_TOKEN')) {
|
|
66
|
+
const v = getSecretForEnvVar(loadedSecrets, 'PYPI_TOKEN', 'pypi_token');
|
|
67
|
+
if (v) out = out.trimEnd() + (out.endsWith('\n') ? '' : '\n') + `PYPI_TOKEN=${v}\n`;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
} catch {
|
|
71
|
+
return content || '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse .env-style content into a key-value map (excludes comments and empty lines).
|
|
77
|
+
* @param {string} content - .env-style content
|
|
78
|
+
* @returns {Object.<string, string>} Map of variable name to value
|
|
79
|
+
*/
|
|
80
|
+
function parseEnvContentToMap(content) {
|
|
81
|
+
const map = {};
|
|
82
|
+
if (!content || typeof content !== 'string') return map;
|
|
83
|
+
const lines = content.split(/\r?\n/);
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
87
|
+
const eq = trimmed.indexOf('=');
|
|
88
|
+
if (eq > 0) {
|
|
89
|
+
const key = trimmed.substring(0, eq).trim();
|
|
90
|
+
map[key] = trimmed.substring(eq + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve .env in memory and write only to envOutputPath or temp (no builder/ or integration/).
|
|
98
|
+
* Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing so shell/install/test have registry tokens.
|
|
99
|
+
*
|
|
100
|
+
* @async
|
|
101
|
+
* @function resolveAndWriteEnvFile
|
|
102
|
+
* @param {string} appName - Application name
|
|
103
|
+
* @param {Object} options - Options
|
|
104
|
+
* @param {string|null} [options.envOutputPath] - Absolute path to write .env (when set)
|
|
105
|
+
* @param {string} [options.environment='docker'] - Environment context ('local' or 'docker')
|
|
106
|
+
* @param {string|null} [options.secretsPath] - Path to secrets file (optional)
|
|
107
|
+
* @param {boolean} [options.force=false] - Generate missing secret keys
|
|
108
|
+
* @returns {Promise<string>} Path where .env was written (envOutputPath or temp file)
|
|
109
|
+
* @throws {Error} If generation fails
|
|
110
|
+
*/
|
|
111
|
+
async function resolveAndWriteEnvFile(appName, options = {}) {
|
|
112
|
+
const secrets = require('./secrets');
|
|
113
|
+
const envOutputPath = options.envOutputPath || null;
|
|
114
|
+
const environment = options.environment || 'docker';
|
|
115
|
+
const secretsPath = options.secretsPath || null;
|
|
116
|
+
const force = options.force === true;
|
|
117
|
+
|
|
118
|
+
let resolved = await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
119
|
+
resolved = await injectRegistryTokens(resolved, secretsPath, appName);
|
|
120
|
+
|
|
121
|
+
if (envOutputPath && typeof envOutputPath === 'string') {
|
|
122
|
+
const dir = path.dirname(envOutputPath);
|
|
123
|
+
if (!fs.existsSync(dir)) {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
125
|
+
}
|
|
126
|
+
fs.writeFileSync(envOutputPath, resolved, { mode: 0o600 });
|
|
127
|
+
return envOutputPath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const tmpDir = os.tmpdir();
|
|
131
|
+
const tmpPath = path.join(tmpDir, `aifabrix-${appName}-${Date.now()}.env`);
|
|
132
|
+
fs.writeFileSync(tmpPath, resolved, { mode: 0o600 });
|
|
133
|
+
return tmpPath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve app env (template + kv:// secrets) and return as key-value map.
|
|
138
|
+
* Used by build to pass NPM_TOKEN/PYPI_TOKEN as Docker build-args.
|
|
139
|
+
* Injects NPM_TOKEN/PYPI_TOKEN from secrets when missing (same as resolveAndWriteEnvFile).
|
|
140
|
+
*
|
|
141
|
+
* @async
|
|
142
|
+
* @function resolveAndGetEnvMap
|
|
143
|
+
* @param {string} appName - Application name
|
|
144
|
+
* @param {Object} [options] - Options (same as resolveAndWriteEnvFile)
|
|
145
|
+
* @returns {Promise<Object.<string, string>>} Map of variable name to value
|
|
146
|
+
*/
|
|
147
|
+
async function resolveAndGetEnvMap(appName, options = {}) {
|
|
148
|
+
const secrets = require('./secrets');
|
|
149
|
+
const environment = options.environment || 'docker';
|
|
150
|
+
const secretsPath = options.secretsPath || null;
|
|
151
|
+
const force = options.force === true;
|
|
152
|
+
let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
|
|
153
|
+
content = await injectRegistryTokens(content, secretsPath, appName);
|
|
154
|
+
return parseEnvContentToMap(content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { resolveAndWriteEnvFile, resolveAndGetEnvMap };
|