@aifabrix/builder 2.61.2 → 2.63.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.
Files changed (90) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/api/onboarding-status.api.js +117 -0
  4. package/lib/app/deploy.js +14 -5
  5. package/lib/app/register.js +24 -2
  6. package/lib/app/run-env-compose.js +44 -8
  7. package/lib/app/run-env-recovery.js +1 -0
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/app/run.js +6 -4
  10. package/lib/build/standard-docker-build.js +4 -0
  11. package/lib/channels/channel-artifact-generator.js +13 -3
  12. package/lib/cli/infra-guided-footers.js +10 -8
  13. package/lib/cli/infra-guided.js +60 -12
  14. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  15. package/lib/cli/setup-platform.js +20 -3
  16. package/lib/commands/auth-config.js +2 -0
  17. package/lib/commands/login.js +4 -0
  18. package/lib/commands/repair-datasource-apply.js +3 -1
  19. package/lib/commands/repair-datasource-legacy.js +94 -0
  20. package/lib/commands/setup-image-refresh.js +26 -0
  21. package/lib/commands/setup-modes.js +9 -2
  22. package/lib/commands/setup-onboarding-readiness.js +83 -0
  23. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  24. package/lib/commands/setup.js +35 -10
  25. package/lib/commands/up-builder-api.js +24 -8
  26. package/lib/commands/up-common.js +16 -1
  27. package/lib/commands/up-dataplane-bootstrap.js +149 -16
  28. package/lib/commands/up-dataplane-credentials.js +21 -0
  29. package/lib/commands/up-dataplane.js +26 -34
  30. package/lib/commands/up-integration-server.js +112 -0
  31. package/lib/commands/wizard-config-normalizer.js +5 -1
  32. package/lib/core/admin-secrets-env-overlay.js +8 -3
  33. package/lib/core/environment-access-policy.js +121 -0
  34. package/lib/core/environment-mode-policy.js +177 -0
  35. package/lib/core/local-env-overrides.js +149 -0
  36. package/lib/core/secrets-ensure.js +4 -2
  37. package/lib/core/secrets-env-content.js +12 -9
  38. package/lib/core/secrets-env-write.js +1 -1
  39. package/lib/datasource/capability/capability-manifest.js +1 -0
  40. package/lib/deployment/installation/index.js +14 -1
  41. package/lib/deployment/installation/infra-catalog.js +3 -0
  42. package/lib/deployment/installation/local-environment-stage.js +285 -0
  43. package/lib/deployment/installation/local-installation.js +76 -0
  44. package/lib/deployment/installation/stage-input.js +5 -1
  45. package/lib/generator/builders.js +3 -1
  46. package/lib/generator/index.js +32 -9
  47. package/lib/generator/split-variables.js +2 -2
  48. package/lib/parameters/database-adoption.js +125 -0
  49. package/lib/parameters/physical-database-naming.js +136 -0
  50. package/lib/parameters/physical-secret-name.js +194 -0
  51. package/lib/parameters/rsa-secret-values.js +165 -0
  52. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  53. package/lib/programmatic/run-channel-package.js +3 -1
  54. package/lib/programmatic/workspace-context.js +37 -2
  55. package/lib/schema/infra.parameter.yaml +61 -27
  56. package/lib/schema/infrastructure-schema.json +213 -120
  57. package/lib/utils/compose-generate-docker-compose.js +2 -2
  58. package/lib/utils/compose-generator.js +4 -7
  59. package/lib/utils/compose-miso-env.js +2 -2
  60. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  61. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  62. package/lib/utils/declarative-url-ports.js +3 -0
  63. package/lib/utils/env-environment-file-paths.js +105 -0
  64. package/lib/utils/environment-scoped-resources.js +48 -14
  65. package/lib/utils/log-redaction.js +4 -1
  66. package/lib/utils/paths-system-builder-keys.js +7 -1
  67. package/lib/utils/paths.js +7 -2
  68. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  69. package/lib/utils/redis-env-scope.js +21 -1
  70. package/lib/utils/registry-auth-sources.js +154 -0
  71. package/lib/utils/registry-credentials.js +131 -14
  72. package/lib/utils/secrets-generator.js +30 -21
  73. package/lib/utils/secrets-helpers.js +5 -7
  74. package/lib/utils/secrets-kv-scope.js +127 -28
  75. package/lib/utils/secrets-materialize-local.js +9 -0
  76. package/lib/utils/secrets-missing-error.js +35 -1
  77. package/lib/utils/token-manager.js +14 -8
  78. package/lib/utils/url-public-path-prefix.js +6 -10
  79. package/package.json +4 -2
  80. package/templates/README.md +6 -0
  81. package/templates/applications/builder-api/application.yaml +1 -1
  82. package/templates/applications/dataplane/application.yaml +2 -2
  83. package/templates/applications/dataplane/env.template +1 -1
  84. package/templates/applications/integration-server/application.yaml +91 -0
  85. package/templates/applications/integration-server/env.template +215 -0
  86. package/templates/applications/mirrored-templates.json +52 -0
  87. package/templates/applications/miso-controller/application.yaml +43 -1
  88. package/templates/applications/miso-controller/env.template +45 -39
  89. package/templates/marketplace/createUiDefinition.json +52 -4
  90. package/templates/marketplace/main.json +85 -7
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Where a registry credential comes from when none is stored.
3
+ *
4
+ * Builder is registry-agnostic: Docker Hub, GHCR and ACR are all a host, a username and a
5
+ * password, and an individual identity, a registry token and an admin key all fit that
6
+ * shape. The only special case is that an `*.azurecr.io` host can also be authenticated by
7
+ * the Azure CLI, which is worth trying before asking a person to paste a secret.
8
+ *
9
+ * Order: the stored pair (handled by the caller), then `az acr login` for ACR, then a
10
+ * prompt. A non-interactive run never prompts — it fails naming the host and how to
11
+ * configure it, because a hung prompt in CI is worse than a clear failure.
12
+ *
13
+ * @fileoverview Credential source strategy for registries without a stored pair
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ /** Azure Container Registry login server suffix. */
19
+ const ACR_HOST_SUFFIX = '.azurecr.io';
20
+
21
+ /**
22
+ * @param {string} host - Registry host
23
+ * @returns {boolean} True for an Azure Container Registry login server
24
+ */
25
+ function isAzureContainerRegistryHost(host) {
26
+ return String(host || '').trim().toLowerCase().endsWith(ACR_HOST_SUFFIX);
27
+ }
28
+
29
+ /**
30
+ * Message for a run that cannot ask for credentials.
31
+ *
32
+ * @param {string} host - Registry host
33
+ * @returns {string}
34
+ */
35
+ function nonInteractiveCredentialMessage(host) {
36
+ const acrHint = isAzureContainerRegistryHost(host)
37
+ ? ` For Azure Container Registry you can instead run "az login" so "az acr login --name ${String(host).split('.')[0]}" succeeds.`
38
+ : '';
39
+ return (
40
+ `No credentials for ${host} and nothing to ask: this run is not interactive. ` +
41
+ `Store a pair with "aifabrix dev set-registry ${host} --username <username> --password-stdin".${acrHint}`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Build the fallback `ensureRegistryLogin` uses when no pair is stored.
47
+ *
48
+ * Dependencies are injected so the chain can be tested without a terminal, an Azure CLI or
49
+ * a Docker daemon.
50
+ *
51
+ * @param {Object} [deps] - Injectable dependencies
52
+ * @param {boolean} [deps.interactive] - Whether a person can be asked; defaults to TTY
53
+ * @param {Function} [deps.acrLogin] - async (host) => void, defaults to `az acr login`
54
+ * @param {Function} [deps.promptCredentials] - async (host) => { username, password }
55
+ * @param {Function} [deps.saveCredentials] - async (host, username, password) => void
56
+ * @param {Function} [deps.dockerLogin] - async (host, username, password) => void
57
+ * @returns {(host: string) => Promise<'acr'|'prompt'>} Fallback for ensureRegistryLogin
58
+ */
59
+ /**
60
+ * An error that names the host it is about.
61
+ *
62
+ * Callers read `registryHost` off it to report which registry refused, so the property is
63
+ * attached in one place rather than at each throw.
64
+ *
65
+ * @param {string} message - Message
66
+ * @param {string} host - Registry host
67
+ * @returns {Error}
68
+ */
69
+ function hostError(message, host) {
70
+ const err = new Error(message);
71
+ err.registryHost = host;
72
+ return err;
73
+ }
74
+
75
+ function buildRegistryAuthFallback(deps = {}) {
76
+ const interactive = deps.interactive === undefined
77
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY)
78
+ : Boolean(deps.interactive);
79
+
80
+ /**
81
+ * Try `az acr login` for an ACR host.
82
+ *
83
+ * Extracted so the fallback itself stays readable. A failure here is deliberately not
84
+ * fatal when a prompt is still possible: an expired or absent `az login` is an ordinary
85
+ * state, and a stored pair or an answered prompt can still authenticate the host.
86
+ *
87
+ * @param {string} host - Registry host
88
+ * @returns {Promise<boolean>} True when ACR login succeeded
89
+ */
90
+ async function tryAcrLogin(host) {
91
+ if (!isAzureContainerRegistryHost(host)) {
92
+ return false;
93
+ }
94
+ const acrLogin = deps.acrLogin
95
+ || ((target) => require('../deployment/push').authenticateACR(target));
96
+ try {
97
+ await acrLogin(host);
98
+ return true;
99
+ } catch (error) {
100
+ if (!interactive) {
101
+ throw hostError(`${nonInteractiveCredentialMessage(host)} (az acr login failed: ${error.message})`, host);
102
+ }
103
+ return false;
104
+ }
105
+ }
106
+
107
+ return async function registryAuthFallback(host) {
108
+ if (await tryAcrLogin(host)) {
109
+ return 'acr';
110
+ }
111
+ if (!interactive) {
112
+ throw hostError(nonInteractiveCredentialMessage(host), host);
113
+ }
114
+
115
+ const promptCredentials = deps.promptCredentials || promptForRegistryCredentials;
116
+ const { username, password } = await promptCredentials(host);
117
+ if (!username || !password) {
118
+ throw new Error(`Registry credentials for ${host} are required: both a username and a password.`);
119
+ }
120
+
121
+ const saveCredentials = deps.saveCredentials
122
+ || ((target, user, pass) => require('./registry-credentials').saveRegistryCredentials(target, user, pass));
123
+ // Store before logging in, so the answer is not asked for again on the next command
124
+ // even if the login itself fails for an unrelated reason.
125
+ await saveCredentials(host, username, password);
126
+
127
+ const dockerLogin = deps.dockerLogin
128
+ || ((target, user, pass) => require('../deployment/push').authenticateExternalRegistry(target, user, pass));
129
+ await dockerLogin(host, username, password);
130
+ return 'prompt';
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Ask for a registry username and password. The password is never echoed.
136
+ *
137
+ * @param {string} host - Registry host
138
+ * @returns {Promise<{ username: string, password: string }>}
139
+ */
140
+ async function promptForRegistryCredentials(host) {
141
+ const inquirer = require('inquirer');
142
+ return inquirer.prompt([
143
+ { type: 'input', name: 'username', message: `Username for ${host}:` },
144
+ { type: 'password', name: 'password', mask: '*', message: `Password or token for ${host}:` }
145
+ ]);
146
+ }
147
+
148
+ module.exports = {
149
+ ACR_HOST_SUFFIX,
150
+ isAzureContainerRegistryHost,
151
+ nonInteractiveCredentialMessage,
152
+ buildRegistryAuthFallback,
153
+ promptForRegistryCredentials
154
+ };
@@ -1,9 +1,19 @@
1
1
  /**
2
2
  * Host-scoped container registry credentials in the primary user secrets file.
3
3
  *
4
- * Keys are exactly `<registry>-username` and `<registry>-password` in the user's
5
- * `secrets.local.yaml` (never the shared store). Values are encrypted through the existing
6
- * local-secret writer and reach Docker only via `docker login --password-stdin`.
4
+ * Keys are `registry-<slug>-username` and `registry-<slug>-password` in the user's
5
+ * `secrets.local.yaml` (never the shared store), where the slug is the host with every
6
+ * character outside `[a-z0-9]` replaced by `-`. The vault naming contract accepts
7
+ * alphanumerics and dashes only, so the earlier `<host>-username` form — which keeps the
8
+ * dots of `myreg.azurecr.io` — was local-only by accident rather than by design. The
9
+ * `registry-` prefix keeps a host that begins with an application key distinguishable from
10
+ * that application's own secrets in the same flat file.
11
+ *
12
+ * Pairs written under the old keys are still read, so an existing `dev set-registry` does
13
+ * not silently stop working, and removal clears both forms.
14
+ *
15
+ * Values are encrypted through the existing local-secret writer and reach Docker only via
16
+ * `docker login --password-stdin`.
7
17
  *
8
18
  * @fileoverview Registry credential storage, lookup, removal, and per-command Docker login
9
19
  * @author AI Fabrix Team
@@ -25,12 +35,41 @@ const { normalizeRegistryHost } = require('./config-registry-preference');
25
35
  const authenticatedHosts = new Set();
26
36
 
27
37
  /**
38
+ * Host reduced to the vault naming charset: lowercase alphanumerics and dashes.
39
+ *
40
+ * @param {string} host - Normalized registry host
41
+ * @returns {string} Slug safe for a secret key
42
+ */
43
+ function registryKeySlug(host) {
44
+ return String(host)
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9]+/g, '-')
47
+ .replace(/^-+|-+$/g, '');
48
+ }
49
+
50
+ /**
51
+ * Secret keys for a registry, including the previous form so an existing pair still loads.
52
+ *
53
+ * `hostKey` records which host owns the slug. Two hosts can normalise to one slug
54
+ * (`a.b-c` and `a-b.c` both give `a-b-c`), and silently reusing another registry's
55
+ * credentials is worse than refusing, so a write compares against it.
56
+ *
28
57
  * @param {string} registry - Registry host
29
- * @returns {{ host: string, usernameKey: string, passwordKey: string }}
58
+ * @returns {{ host: string, slug: string, usernameKey: string, passwordKey: string,
59
+ * hostKey: string, legacyUsernameKey: string, legacyPasswordKey: string }}
30
60
  */
31
61
  function registryCredentialKeys(registry) {
32
62
  const host = normalizeRegistryHost(registry);
33
- return { host, usernameKey: `${host}-username`, passwordKey: `${host}-password` };
63
+ const slug = registryKeySlug(host);
64
+ return {
65
+ host,
66
+ slug,
67
+ usernameKey: `registry-${slug}-username`,
68
+ passwordKey: `registry-${slug}-password`,
69
+ hostKey: `registry-${slug}-host`,
70
+ legacyUsernameKey: `${host}-username`,
71
+ legacyPasswordKey: `${host}-password`
72
+ };
34
73
  }
35
74
 
36
75
  function hasValue(value) {
@@ -40,7 +79,18 @@ function hasValue(value) {
40
79
  function readRawPair(keys) {
41
80
  const file = pathsUtil.getPrimaryUserSecretsLocalPath();
42
81
  const raw = loadExistingSecrets(file);
43
- return { file, username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
82
+ const current = { username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
83
+ if (hasValue(current.username) || hasValue(current.password)) {
84
+ return { file, ...current, legacy: false, owner: raw[keys.hostKey] };
85
+ }
86
+ // Written before the keys were namespaced; read it rather than prompting again.
87
+ return {
88
+ file,
89
+ username: raw[keys.legacyUsernameKey],
90
+ password: raw[keys.legacyPasswordKey],
91
+ legacy: true,
92
+ owner: undefined
93
+ };
44
94
  }
45
95
 
46
96
  function partialPairError(keys) {
@@ -106,13 +156,36 @@ function removeKeysFromFile(file, keyNames) {
106
156
  return true;
107
157
  }
108
158
 
159
+ /**
160
+ * Refuse to overwrite a different host's credentials when two hosts share a slug.
161
+ *
162
+ * @param {Object} keys - From {@link registryCredentialKeys}
163
+ * @param {Object} snapshot - From readRawPair
164
+ * @throws {Error} When the slug already belongs to another host
165
+ */
166
+ function assertNoSlugCollision(keys, snapshot) {
167
+ const owner = typeof snapshot.owner === 'string' ? snapshot.owner.trim() : '';
168
+ if (!owner || owner === keys.host) {
169
+ return;
170
+ }
171
+ throw new Error(
172
+ `Registry key collision: "${keys.host}" and "${owner}" both reduce to "${keys.slug}", ` +
173
+ `so storing this pair would overwrite the credentials for ${owner}. ` +
174
+ `Remove the other registry first with "aifabrix dev set-registry ${owner} --clear-credentials".`
175
+ );
176
+ }
177
+
109
178
  function restorePair(keys, snapshot) {
179
+ // Always clear what a save writes, then put back what the snapshot actually held — which
180
+ // may be under the old keys, since a pair written before namespacing is read from there.
110
181
  const restore = {};
111
- const remove = [];
112
- for (const [key, value] of [[keys.usernameKey, snapshot.username], [keys.passwordKey, snapshot.password]]) {
113
- if (value === undefined) remove.push(key);
114
- else restore[key] = value;
115
- }
182
+ const [usernameKey, passwordKey] = snapshot.legacy
183
+ ? [keys.legacyUsernameKey, keys.legacyPasswordKey]
184
+ : [keys.usernameKey, keys.passwordKey];
185
+ if (snapshot.username !== undefined) restore[usernameKey] = snapshot.username;
186
+ if (snapshot.password !== undefined) restore[passwordKey] = snapshot.password;
187
+ const remove = [keys.usernameKey, keys.passwordKey, keys.hostKey]
188
+ .filter((key) => !Object.prototype.hasOwnProperty.call(restore, key));
116
189
  removeKeysFromFile(snapshot.file, remove);
117
190
  mergeSecretsIntoFile(snapshot.file, restore);
118
191
  }
@@ -130,14 +203,21 @@ async function saveRegistryCredentials(registry, username, password) {
130
203
  throw new Error('Registry username and password must both be non-empty');
131
204
  }
132
205
  const snapshot = readRawPair(keys);
206
+ assertNoSlugCollision(keys, snapshot);
133
207
  await config.ensureSecretsEncryptionKey();
134
208
  try {
135
209
  await saveLocalSecret(keys.usernameKey, String(username));
136
210
  await saveLocalSecret(keys.passwordKey, String(password));
211
+ // Plain, not through saveLocalSecret: the host is not a secret, and encrypting it would
212
+ // make every later read compare a ciphertext against the host and report a collision.
213
+ mergeSecretsIntoFile(snapshot.file, { [keys.hostKey]: keys.host });
137
214
  } catch (error) {
138
215
  restorePair(keys, snapshot);
139
216
  throw new Error(`Could not save registry credentials for ${keys.host}; previous values were restored (${error.message})`);
140
217
  }
218
+ // The pair now lives under the namespaced keys; leaving the old one behind would keep a
219
+ // stale password readable and let a later read pick whichever it found first.
220
+ removeKeysFromFile(snapshot.file, [keys.legacyUsernameKey, keys.legacyPasswordKey]);
141
221
  authenticatedHosts.delete(keys.host);
142
222
  return { host: keys.host, rollback: () => restorePair(keys, snapshot) };
143
223
  }
@@ -150,12 +230,24 @@ async function saveRegistryCredentials(registry, username, password) {
150
230
  function removeRegistryCredentials(registry) {
151
231
  const keys = registryCredentialKeys(registry);
152
232
  authenticatedHosts.delete(keys.host);
153
- return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [keys.usernameKey, keys.passwordKey]);
233
+ return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [
234
+ keys.usernameKey,
235
+ keys.passwordKey,
236
+ keys.hostKey,
237
+ keys.legacyUsernameKey,
238
+ keys.legacyPasswordKey
239
+ ]);
154
240
  }
155
241
 
156
242
  /**
157
- * Authenticates Docker to the registry once per process: stored pair first, then the optional
158
- * fallback (for example Azure CLI for ACR). The password is sent only on stdin.
243
+ * Authenticates Docker to the registry once per process: stored pair first, then the
244
+ * optional fallback. The password is sent only on stdin.
245
+ *
246
+ * There is deliberately no default fallback. Most pulls are of public images and must stay
247
+ * anonymous; asking for a credential there would prompt for something the pull does not
248
+ * need. A caller that genuinely requires a credential passes one — see
249
+ * {@link ensureRegistryLoginWithSources}.
250
+ *
159
251
  * @param {string} registry - Registry host
160
252
  * @param {Object} [options] - Options
161
253
  * @param {Function} [options.fallback] - async (host) => void when no pair is stored
@@ -180,6 +272,29 @@ async function ensureRegistryLogin(registry, options = {}) {
180
272
  return 'none';
181
273
  }
182
274
 
275
+ /**
276
+ * Authenticate a registry that a command cannot proceed without.
277
+ *
278
+ * Adds the credential-source chain on top of {@link ensureRegistryLogin}: the stored pair,
279
+ * then `az acr login` for an `*.azurecr.io` host, then a prompt. A non-interactive run
280
+ * fails naming the host instead of hanging on a question nobody can answer.
281
+ *
282
+ * @param {string} registry - Registry host
283
+ * @param {Object} [sources] - Overrides for {@link buildRegistryAuthFallback}
284
+ * @returns {Promise<'cached'|'credentials'|'acr'|'prompt'>}
285
+ */
286
+ async function ensureRegistryLoginWithSources(registry, sources = {}) {
287
+ const { buildRegistryAuthFallback } = require('./registry-auth-sources');
288
+ const fallback = buildRegistryAuthFallback(sources);
289
+ let outcome = null;
290
+ const result = await ensureRegistryLogin(registry, {
291
+ fallback: async(host) => {
292
+ outcome = await fallback(host);
293
+ }
294
+ });
295
+ return result === 'fallback' ? outcome : result;
296
+ }
297
+
183
298
  /**
184
299
  * Registry host of an image reference, or '' for Docker Hub style refs.
185
300
  * @param {string} imageRef - Image reference (`host/repo:tag`, `repo:tag`, ...)
@@ -224,12 +339,14 @@ function resetRegistryLoginCache(registry) {
224
339
  }
225
340
 
226
341
  module.exports = {
342
+ registryKeySlug,
227
343
  registryCredentialKeys,
228
344
  getRegistryCredentialStatus,
229
345
  loadRegistryCredentials,
230
346
  saveRegistryCredentials,
231
347
  removeRegistryCredentials,
232
348
  ensureRegistryLogin,
349
+ ensureRegistryLoginWithSources,
233
350
  ensureLoginForImageRefs,
234
351
  registryHostFromImageRef,
235
352
  resetRegistryLoginCache
@@ -16,6 +16,7 @@ const yaml = require('js-yaml');
16
16
  const crypto = require('crypto');
17
17
  const logger = require('./logger');
18
18
  const pathsUtil = require('./paths');
19
+ const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
19
20
  const { mergeFlatSecretsYamlPreservingComments } = require('./secrets-yaml-preserve');
20
21
  const {
21
22
  generateDatabasePasswordValueForKey,
@@ -165,32 +166,40 @@ function generateDbUrlValue(key) {
165
166
  * @param {Record<string, string>|undefined} placeholderContext - Merged infra.parameter.yaml defaults + CLI overrides
166
167
  * @returns {string|undefined} Value if catalog handled key; undefined to use legacy rules
167
168
  */
168
- function tryGenerateSecretValueFromCatalog(key, placeholderContext) {
169
- let catalogEntry;
169
+ /** Generators whose value comes straight from the catalog entry. */
170
+ const CATALOG_DIRECT_GENERATORS = Object.freeze([
171
+ 'randomBytes32', 'randomAlphanumeric', 'password', 'emptyString', 'emptyAllowed', 'literal'
172
+ ]);
173
+
174
+ /**
175
+ * The catalog entry for a key, or nothing when the catalog cannot be read.
176
+ *
177
+ * @param {string} key - Secret key
178
+ * @returns {Object|undefined}
179
+ */
180
+ function catalogEntryForKey(key) {
170
181
  try {
171
- catalogEntry = infraParameterCatalogModule().getInfraParameterCatalog().findEntryForKey(key);
182
+ return infraParameterCatalogModule().getInfraParameterCatalog().findEntryForKey(key);
172
183
  } catch (err) {
173
184
  logger.warn(`infra parameter catalog unavailable (${err.message}); using legacy generators.`);
174
185
  return undefined;
175
186
  }
187
+ }
188
+
189
+ function tryGenerateSecretValueFromCatalog(key, placeholderContext) {
190
+ const catalogEntry = catalogEntryForKey(key);
176
191
  if (!catalogEntry || !catalogEntry.generator) return undefined;
177
192
  const g = catalogEntry.generator.type;
193
+ if (g === 'rsaKeyPair') {
194
+ return require('../parameters/rsa-secret-values').generateRsaValueFromCatalog(key, catalogEntry);
195
+ }
178
196
  if (g === 'databasePassword') {
179
- const v = generateDbPasswordValue(key);
180
- return v !== null ? v : undefined;
197
+ return generateDbPasswordValue(key) ?? undefined;
181
198
  }
182
199
  if (g === 'databaseUrl') {
183
- const v = generateDbUrlValue(key);
184
- return v !== null ? v : undefined;
185
- }
186
- if (
187
- g === 'randomBytes32' ||
188
- g === 'randomAlphanumeric' ||
189
- g === 'password' ||
190
- g === 'emptyString' ||
191
- g === 'emptyAllowed' ||
192
- g === 'literal'
193
- ) {
200
+ return generateDbUrlValue(key) ?? undefined;
201
+ }
202
+ if (CATALOG_DIRECT_GENERATORS.includes(g)) {
194
203
  return infraParameterCatalogModule().generateValueFromCatalogEntry(key, catalogEntry, crypto, placeholderContext);
195
204
  }
196
205
  return undefined;
@@ -411,15 +420,16 @@ async function generateMissingSecrets(envTemplate, secretsPath) {
411
420
  return [];
412
421
  }
413
422
 
414
- const newSecrets = {};
423
+ const newSecrets = generateRsaSecretValues(missingKeys, existingSecrets);
415
424
  for (const key of missingKeys) {
416
- newSecrets[key] = generateSecretValue(key);
425
+ if (!(key in newSecrets)) newSecrets[key] = generateSecretValue(key);
417
426
  }
418
427
 
419
428
  appendSecretsToFile(resolvedPath, newSecrets);
420
429
 
421
- logger.log(`✔ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
422
- return missingKeys;
430
+ const generatedKeys = Object.keys(newSecrets);
431
+ logger.log(`✔ Generated ${generatedKeys.length} missing secret key(s): ${generatedKeys.join(', ')}`);
432
+ return generatedKeys;
423
433
  }
424
434
 
425
435
  /**
@@ -479,4 +489,3 @@ module.exports = {
479
489
  generateMissingSecrets,
480
490
  createDefaultSecrets
481
491
  };
482
-
@@ -136,7 +136,7 @@ function getValueByPath(secrets, pathStr) {
136
136
  return getValueByNestedPath(secrets, pathStr.split('/'));
137
137
  }
138
138
 
139
- const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies } =
139
+ const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies, prepareScopedKv } =
140
140
  require('./secrets-kv-scope').createScopedKvHelpers(getValueByPath);
141
141
 
142
142
  function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
@@ -155,8 +155,7 @@ function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
155
155
  * @returns {string[]} Array of missing kv://<path> references (unique)
156
156
  */
157
157
  function collectMissingSecrets(content, secrets, scopedKv = null) {
158
- const effective = Boolean(scopedKv && scopedKv.effective && scopedKv.envKey);
159
- const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
158
+ const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
160
159
  const seen = new Set();
161
160
  const missing = [];
162
161
  for (const line of content.split('\n')) {
@@ -168,7 +167,7 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
168
167
  const pathStr = match[1];
169
168
  if (seen.has(pathStr)) continue;
170
169
  seen.add(pathStr);
171
- const value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
170
+ const value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
172
171
  if ((value === undefined || value === null) && !isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
173
172
  missing.push(`kv://${pathStr}`);
174
173
  }
@@ -216,8 +215,7 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
216
215
  * @returns {string} Content with kv:// references replaced
217
216
  */
218
217
  function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
219
- const effective = Boolean(scopedKv && scopedKv.effective && scopedKv.envKey);
220
- const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
218
+ const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
221
219
  const lines = content.split('\n');
222
220
  const result = lines.map(line => {
223
221
  if (isCommentOrEmptyLine(line)) return line;
@@ -225,7 +223,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
225
223
  if (!scanLine.trim()) return line;
226
224
  const suffix = line.slice(scanLine.length);
227
225
  const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
228
- let value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
226
+ let value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
229
227
  if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
230
228
  value = '';
231
229
  }