@aifabrix/builder 2.60.0 → 2.62.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 (177) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/api/applications.api.js +22 -2
  14. package/lib/api/bootstrap-snapshot.api.js +115 -0
  15. package/lib/api/configuration.api.js +131 -0
  16. package/lib/api/system-secrets.api.js +72 -0
  17. package/lib/app/deploy.js +14 -5
  18. package/lib/app/run-docker-fallback.js +6 -1
  19. package/lib/app/run-env-compose.js +5 -6
  20. package/lib/app/run-helpers.js +2 -1
  21. package/lib/app/run-parameter-sync.js +142 -0
  22. package/lib/app/run.js +6 -4
  23. package/lib/build/docker-build-args.js +5 -2
  24. package/lib/build/index.js +3 -2
  25. package/lib/build/standard-docker-build.js +6 -2
  26. package/lib/channels/channel-artifact-generator.js +13 -3
  27. package/lib/cli/infra-guided-footers.js +10 -8
  28. package/lib/cli/infra-guided.js +30 -10
  29. package/lib/cli/setup-app.js +5 -0
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  32. package/lib/cli/setup-platform.js +2 -0
  33. package/lib/commands/auth-config.js +2 -0
  34. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  35. package/lib/commands/datasource-capability.js +4 -2
  36. package/lib/commands/env-secret-context.js +113 -0
  37. package/lib/commands/env-secret-list.js +200 -0
  38. package/lib/commands/env-secret-push-confirm.js +54 -0
  39. package/lib/commands/env-secret-push-run.js +227 -0
  40. package/lib/commands/env-secret-push.js +168 -0
  41. package/lib/commands/login.js +4 -0
  42. package/lib/commands/repair-datasource-apply.js +5 -1
  43. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  44. package/lib/commands/repair-datasource-legacy.js +94 -0
  45. package/lib/commands/repair-datasource-run.js +1 -0
  46. package/lib/commands/setup-image-refresh.js +26 -0
  47. package/lib/commands/setup-modes.js +8 -9
  48. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  49. package/lib/commands/setup-prompts.js +2 -180
  50. package/lib/commands/setup.js +3 -0
  51. package/lib/commands/up-builder-api.js +24 -8
  52. package/lib/commands/up-common.js +16 -1
  53. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  54. package/lib/commands/up-dataplane-credentials.js +21 -0
  55. package/lib/commands/up-dataplane.js +37 -18
  56. package/lib/commands/up-integration-server.js +112 -0
  57. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  58. package/lib/commands/verify-operations-steps.js +13 -1
  59. package/lib/commands/wizard-config-normalizer.js +12 -5
  60. package/lib/commands/wizard-core.js +5 -157
  61. package/lib/commands/wizard-file-saving.js +163 -0
  62. package/lib/core/admin-secrets-env-overlay.js +8 -3
  63. package/lib/core/env-platform-expand.js +5 -1
  64. package/lib/core/environment-access-policy.js +121 -0
  65. package/lib/core/environment-mode-policy.js +177 -0
  66. package/lib/core/local-env-overrides.js +149 -0
  67. package/lib/core/secrets-ensure.js +4 -2
  68. package/lib/core/secrets-env-content.js +17 -10
  69. package/lib/core/secrets-env-write.js +11 -4
  70. package/lib/core/secrets-load.js +4 -2
  71. package/lib/datasource/binary-documents-validator.js +190 -0
  72. package/lib/datasource/capability/capability-manifest.js +1 -0
  73. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  74. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  75. package/lib/datasource/capability/upsert-operations.js +138 -0
  76. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  77. package/lib/datasource/validate.js +12 -5
  78. package/lib/deployment/installation/index.js +14 -1
  79. package/lib/deployment/installation/infra-catalog.js +3 -0
  80. package/lib/deployment/installation/local-environment-stage.js +285 -0
  81. package/lib/deployment/installation/local-installation.js +76 -0
  82. package/lib/deployment/installation/stage-input.js +5 -1
  83. package/lib/generator/builders.js +3 -1
  84. package/lib/generator/index.js +3 -0
  85. package/lib/generator/split-variables.js +2 -2
  86. package/lib/lifecycle/product-model.js +4 -3
  87. package/lib/lifecycle/report-display.js +3 -2
  88. package/lib/parameters/database-adoption.js +125 -0
  89. package/lib/parameters/infra-parameter-catalog.js +1 -1
  90. package/lib/parameters/physical-database-naming.js +136 -0
  91. package/lib/parameters/physical-secret-name.js +194 -0
  92. package/lib/parameters/rsa-secret-values.js +165 -0
  93. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  94. package/lib/programmatic/builder-help-governance.js +1 -1
  95. package/lib/programmatic/builder-help.js +1 -1
  96. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  97. package/lib/programmatic/run-channel-package.js +3 -1
  98. package/lib/programmatic/workspace-context.js +37 -2
  99. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  100. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  101. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  102. package/lib/role-assistant/test-runner-workhub.js +28 -19
  103. package/lib/schema/application-schema.json +14 -2
  104. package/lib/schema/external-datasource.schema.json +23 -3
  105. package/lib/schema/infra.parameter.yaml +440 -56
  106. package/lib/schema/infrastructure-schema.json +213 -120
  107. package/lib/utils/compose-generate-docker-compose.js +9 -14
  108. package/lib/utils/compose-generator.js +4 -7
  109. package/lib/utils/compose-miso-env.js +2 -2
  110. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  111. package/lib/utils/datasource-binary-evidence.js +92 -0
  112. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  113. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  114. package/lib/utils/datasource-test-run-display.js +8 -2
  115. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  116. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  117. package/lib/utils/declarative-url-ports.js +3 -0
  118. package/lib/utils/env-copy.js +11 -10
  119. package/lib/utils/env-environment-file-paths.js +105 -0
  120. package/lib/utils/environment-scoped-resources.js +48 -14
  121. package/lib/utils/external-system-system-test-tty.js +3 -2
  122. package/lib/utils/image-tags.js +2 -2
  123. package/lib/utils/paths-system-builder-keys.js +7 -1
  124. package/lib/utils/paths.js +7 -2
  125. package/lib/utils/platform-kv-ref.js +1 -1
  126. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  127. package/lib/utils/prepare-local-data-mount.js +58 -0
  128. package/lib/utils/redis-env-scope.js +21 -1
  129. package/lib/utils/registry-auth-sources.js +154 -0
  130. package/lib/utils/registry-credentials.js +131 -14
  131. package/lib/utils/secrets-generator.js +30 -21
  132. package/lib/utils/secrets-helpers.js +5 -8
  133. package/lib/utils/secrets-kv-scope.js +127 -28
  134. package/lib/utils/secrets-materialize-local.js +9 -0
  135. package/lib/utils/secrets-missing-error.js +35 -1
  136. package/lib/utils/system-secret-mapping.js +125 -0
  137. package/lib/utils/test-log-writer.js +2 -1
  138. package/lib/utils/token-manager.js +14 -8
  139. package/lib/utils/url-public-path-prefix.js +6 -10
  140. package/lib/validation/external-manifest-validator.js +5 -0
  141. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  142. package/lib/validation/validate-external-file.js +5 -1
  143. package/package.json +6 -4
  144. package/templates/agent-kit/agent-kit.yaml +1 -1
  145. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  146. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  147. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  148. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  149. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  150. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  151. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  152. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  153. package/templates/agent-kit/skills/shared/status.md +3 -12
  154. package/templates/applications/builder-api/application.yaml +1 -1
  155. package/templates/applications/builder-api/env.template +5 -1
  156. package/templates/applications/dataplane/application.yaml +2 -2
  157. package/templates/applications/dataplane/env.template +10 -12
  158. package/templates/applications/integration-server/application.yaml +91 -0
  159. package/templates/applications/integration-server/env.template +215 -0
  160. package/templates/applications/keycloak/application.yaml +6 -1
  161. package/templates/applications/mirrored-templates.json +52 -0
  162. package/templates/applications/miso-controller/application.yaml +116 -2
  163. package/templates/applications/miso-controller/env.template +62 -17
  164. package/templates/applications/miso-controller/rbac.yaml +17 -0
  165. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  166. package/templates/marketplace/createUiDefinition.json +52 -4
  167. package/templates/marketplace/main.json +88 -10
  168. package/templates/python/docker-compose.hbs +1 -1
  169. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  170. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  171. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  172. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  173. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  174. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  175. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  176. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  177. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -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
-
@@ -55,7 +55,6 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
55
55
  const EMPTY_ALLOWED_KV_FALLBACK = new Set([
56
56
  'redis-passwordKeyVault',
57
57
  'azure-openaiapi-urlKeyVault',
58
- 'secrets-azureOpenaiApiKeyVault',
59
58
  'miso-controller-secrets-apiKeyVault'
60
59
  ]);
61
60
 
@@ -137,7 +136,7 @@ function getValueByPath(secrets, pathStr) {
137
136
  return getValueByNestedPath(secrets, pathStr.split('/'));
138
137
  }
139
138
 
140
- const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies } =
139
+ const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies, prepareScopedKv } =
141
140
  require('./secrets-kv-scope').createScopedKvHelpers(getValueByPath);
142
141
 
143
142
  function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
@@ -156,8 +155,7 @@ function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
156
155
  * @returns {string[]} Array of missing kv://<path> references (unique)
157
156
  */
158
157
  function collectMissingSecrets(content, secrets, scopedKv = null) {
159
- const effective = Boolean(scopedKv && scopedKv.effective && scopedKv.envKey);
160
- const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
158
+ const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
161
159
  const seen = new Set();
162
160
  const missing = [];
163
161
  for (const line of content.split('\n')) {
@@ -169,7 +167,7 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
169
167
  const pathStr = match[1];
170
168
  if (seen.has(pathStr)) continue;
171
169
  seen.add(pathStr);
172
- const value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
170
+ const value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
173
171
  if ((value === undefined || value === null) && !isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
174
172
  missing.push(`kv://${pathStr}`);
175
173
  }
@@ -217,8 +215,7 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
217
215
  * @returns {string} Content with kv:// references replaced
218
216
  */
219
217
  function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
220
- const effective = Boolean(scopedKv && scopedKv.effective && scopedKv.envKey);
221
- const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
218
+ const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
222
219
  const lines = content.split('\n');
223
220
  const result = lines.map(line => {
224
221
  if (isCommentOrEmptyLine(line)) return line;
@@ -226,7 +223,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
226
223
  if (!scanLine.trim()) return line;
227
224
  const suffix = line.slice(scanLine.length);
228
225
  const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
229
- let value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
226
+ let value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
230
227
  if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
231
228
  value = '';
232
229
  }
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * Env-scoped kv:// resolution helpers (prefixed keys + in-memory aliases).
3
3
  *
4
+ * Two regimes share this module:
5
+ *
6
+ * - `dev` and `miso` are **lenient** (plan 117). A `<env>-<key>` secret wins when present,
7
+ * and an absent one falls back to the unprefixed key. Existing installations are written
8
+ * that way, so removing the fallback would break every one of them.
9
+ * - `tst` and `pro` are **strict** (plan 210.0). Only `<env>-<key>` is read. An absent key
10
+ * is absent — it never falls back to the unprefixed value and it is never generated.
11
+ *
12
+ * The strict rule is the whole point: these environments share one Postgres, one Redis and
13
+ * one secrets file with `dev`. A fallback there would hand production whatever `dev` happens
14
+ * to hold, which is worse than failing, because it looks like it worked. Strictness is
15
+ * therefore deliberately independent of `useEnvironmentScopedResources` and of the
16
+ * per-application `environmentScopedResources` flag: an installation that forgot to set
17
+ * either must not silently resolve `pro` against `dev`'s secrets.
18
+ *
4
19
  * @fileoverview Split from secrets-helpers for file size limits
5
20
  * @author AI Fabrix Team
6
21
  * @version 1.0.0
@@ -8,6 +23,25 @@
8
23
 
9
24
  'use strict';
10
25
 
26
+ /**
27
+ * Environments where the `<env>-` prefix is mandatory rather than preferred.
28
+ *
29
+ * Not the same list as `physical-database-naming.PHYSICAL_NAME_ENVIRONMENTS`: that one is the
30
+ * Postgres naming vocabulary and includes `miso` and `dev`, which resolve leniently here.
31
+ */
32
+ const STRICT_SCOPED_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
33
+
34
+ /**
35
+ * @param {string} envKey - Run environment key
36
+ * @returns {boolean} True when the prefix is mandatory for this environment
37
+ */
38
+ function isStrictScopedEnvironment(envKey) {
39
+ if (!envKey || typeof envKey !== 'string') {
40
+ return false;
41
+ }
42
+ return STRICT_SCOPED_ENVIRONMENTS.includes(envKey.trim().toLowerCase());
43
+ }
44
+
11
45
  /**
12
46
  * Treat undefined/null and whitespace-only strings as no value so prefixed slots
13
47
  * cannot shadow unprefixed secrets with empty placeholders (plan 117 scoped KV).
@@ -25,9 +59,80 @@ function isKvSlotEmpty(v) {
25
59
  return false;
26
60
  }
27
61
 
62
+ /**
63
+ * The secret key a strict environment actually requires.
64
+ *
65
+ * **Not simply `<env>-<key>`.** Miso's naming rule has three cases, and only the middle one
66
+ * is a bare environment prefix:
67
+ *
68
+ * ```text
69
+ * system <key> unprefixed, deliberately
70
+ * environment-scoped pro-<key>
71
+ * application-scoped pro-<app>-<key>
72
+ * ```
73
+ *
74
+ * They are not distinguishable from the key alone, so the name comes from the catalog entry's
75
+ * own vault-name pattern rather than being built here. A database password is therefore
76
+ * `pro-dataplane-databases-0-passwordKeyVault` — the application segment before the key — and
77
+ * `keycloak-*` and `acr-*` keep the names the Marketplace template writes.
78
+ *
79
+ * @param {string} envKey - Run environment key
80
+ * @param {string} pathStr - Unprefixed kv path
81
+ * @returns {string}
82
+ */
83
+ function strictScopedKeyName(envKey, pathStr) {
84
+ const { resolveStrictSecretName } = require('../parameters/physical-secret-name');
85
+ return resolveStrictSecretName(pathStr, String(envKey).toLowerCase());
86
+ }
87
+
88
+ /**
89
+ * Alias every unprefixed key to its `<env>-` name so a lenient lookup finds it.
90
+ *
91
+ * Never called for a strict environment: aliasing there would reintroduce the fallback
92
+ * this module exists to remove.
93
+ *
94
+ * @param {Object} secrets
95
+ * @param {string} envKey
96
+ * @returns {Object}
97
+ */
98
+ function mergeSecretsWithPrefixedCopies(secrets, envKey) {
99
+ if (!secrets || typeof secrets !== 'object' || !envKey) {
100
+ return secrets;
101
+ }
102
+ const prefix = `${String(envKey).toLowerCase()}-`;
103
+ const merged = { ...secrets };
104
+ for (const [k, v] of Object.entries(secrets)) {
105
+ if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
106
+ const pk = prefix + k;
107
+ if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
108
+ merged[pk] = v;
109
+ }
110
+ }
111
+ return merged;
112
+ }
113
+
114
+ /**
115
+ * Resolve the scoped-kv regime and the secrets map to read through.
116
+ *
117
+ * One helper so every caller applies the same rule; a caller that recomputed `effective`
118
+ * itself could leave a strict environment resolving leniently.
119
+ *
120
+ * @param {Object} secrets - Merged secrets map
121
+ * @param {{ effective?: boolean, envKey?: string }|null} scopedKv
122
+ * @returns {{ secretsMap: Object, envKey: string|undefined, effective: boolean, strict: boolean }}
123
+ */
124
+ function prepareScopedKv(secrets, scopedKv) {
125
+ const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : undefined;
126
+ const strict = isStrictScopedEnvironment(envKey);
127
+ const effective = strict || Boolean(scopedKv && scopedKv.effective && envKey);
128
+ const secretsMap =
129
+ effective && !strict ? mergeSecretsWithPrefixedCopies(secrets, envKey) : secrets;
130
+ return { secretsMap, envKey, effective, strict };
131
+ }
132
+
28
133
  /**
29
134
  * @param {Function} getValueByPath - From secrets-helpers
30
- * @returns {{ getValueByPathWithEnvScope: Function, mergeSecretsWithPrefixedCopies: Function }}
135
+ * @returns {Object} Scoped kv helpers bound to that lookup
31
136
  */
32
137
  function createScopedKvHelpers(getValueByPath) {
33
138
  /**
@@ -38,40 +143,34 @@ function createScopedKvHelpers(getValueByPath) {
38
143
  * @returns {*}
39
144
  */
40
145
  function getValueByPathWithEnvScope(secrets, pathStr, envKey, effective) {
41
- if (!effective || !envKey) {
146
+ const strict = isStrictScopedEnvironment(envKey);
147
+ if (!strict && (!effective || !envKey)) {
42
148
  return getValueByPath(secrets, pathStr);
43
149
  }
44
- const prefix = `${String(envKey).toLowerCase()}-`;
45
- const prefixedPath = prefix + pathStr;
46
- const fromPrefixed = getValueByPath(secrets, prefixedPath);
150
+ const fromPrefixed = getValueByPath(secrets, strictScopedKeyName(envKey, pathStr));
151
+ if (strict) {
152
+ // No fallback. Undefined rather than the empty value so the caller's
153
+ // allowed-empty rule still decides whether absence is an error.
154
+ return isKvSlotEmpty(fromPrefixed) ? undefined : fromPrefixed;
155
+ }
47
156
  if (!isKvSlotEmpty(fromPrefixed)) {
48
157
  return fromPrefixed;
49
158
  }
50
159
  return getValueByPath(secrets, pathStr);
51
160
  }
52
161
 
53
- /**
54
- * @param {Object} secrets
55
- * @param {string} envKey
56
- * @returns {Object}
57
- */
58
- function mergeSecretsWithPrefixedCopies(secrets, envKey) {
59
- if (!secrets || typeof secrets !== 'object' || !envKey) {
60
- return secrets;
61
- }
62
- const prefix = `${String(envKey).toLowerCase()}-`;
63
- const merged = { ...secrets };
64
- for (const [k, v] of Object.entries(secrets)) {
65
- if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
66
- const pk = prefix + k;
67
- if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
68
- merged[pk] = v;
69
- }
70
- }
71
- return merged;
72
- }
73
-
74
- return { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies };
162
+ return {
163
+ getValueByPathWithEnvScope,
164
+ mergeSecretsWithPrefixedCopies,
165
+ prepareScopedKv
166
+ };
75
167
  }
76
168
 
77
- module.exports = { createScopedKvHelpers };
169
+ module.exports = {
170
+ STRICT_SCOPED_ENVIRONMENTS,
171
+ isStrictScopedEnvironment,
172
+ strictScopedKeyName,
173
+ mergeSecretsWithPrefixedCopies,
174
+ prepareScopedKv,
175
+ createScopedKvHelpers
176
+ };
@@ -17,6 +17,7 @@ const secretsUtils = require('./secrets-utils');
17
17
  const { loadConfiguredSharedSecretsStore } = require('./remote-secrets-loader');
18
18
  const { decryptSecretsObject } = require('../core/secrets-load');
19
19
  const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
20
+ const { isStrictScopedEnvironment } = require('./secrets-kv-scope');
20
21
  const {
21
22
  resolveKvRefValue,
22
23
  mergeSecretsWithPrefixedCopies,
@@ -98,6 +99,14 @@ async function materializeResolvedKvSecretsToUserLocal(templateContent, mergedSe
98
99
  return [];
99
100
  }
100
101
 
102
+ // This writes back under the UNPREFIXED key. In a strict environment that would mint an
103
+ // unprefixed copy of a tst or pro secret which dev would then resolve, quietly undoing the
104
+ // isolation. Nothing is lost by skipping: a strict environment resolves only `<env>-<key>`,
105
+ // so every value it resolved was already stored under the name it will be read by.
106
+ if (isStrictScopedEnvironment(scopedKv && scopedKv.envKey)) {
107
+ return [];
108
+ }
109
+
101
110
  try {
102
111
  const { effective, secretsMap, localMap } = buildScopedMaps(mergedSecrets, scopedKv);
103
112
  const sharedScoped = await resolveSharedScopedMapForMaterialize(scopedKv, effective);
@@ -10,6 +10,7 @@
10
10
 
11
11
  const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
12
12
  const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
13
+ const { isStrictScopedEnvironment, strictScopedKeyName } = require('./secrets-kv-scope');
13
14
 
14
15
  /**
15
16
  * @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
@@ -117,6 +118,35 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
117
118
  return lines;
118
119
  }
119
120
 
121
+ /**
122
+ * Name the key a strict environment actually needs.
123
+ *
124
+ * The template references `kv://<key>`, but `tst` and `pro` only read `<env>-<key>`, so an
125
+ * error quoting the template reference alone would send someone to create the one name that
126
+ * will not be used. Both are printed: the reference so the template line is findable, and
127
+ * the required key so `aifabrix secret set` gets the right argument.
128
+ *
129
+ * @param {string[]} missing - kv:// refs
130
+ * @param {{ envKey?: string }|null} [scopedKv]
131
+ * @returns {string[]}
132
+ */
133
+ function buildStrictScopedKeyHints(missing, scopedKv) {
134
+ const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : null;
135
+ if (!envKey || !isStrictScopedEnvironment(envKey)) {
136
+ return [];
137
+ }
138
+ const env = String(envKey).toLowerCase();
139
+ const lines = missing.map(
140
+ (ref) => `Required-key: ${strictScopedKeyName(env, kvRefToSecretSetKey(ref))}`
141
+ );
142
+ lines.push(
143
+ `Scoped-env-hint: ${env} reads only ${env}-prefixed secrets. The unprefixed key is ` +
144
+ `not a fallback and is never generated for ${env}, so another environment's value ` +
145
+ 'cannot be used by accident.'
146
+ );
147
+ return lines;
148
+ }
149
+
120
150
  /**
121
151
  * @param {Object} opts
122
152
  * @param {string[]} opts.missing
@@ -125,10 +155,11 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
125
155
  * @param {string} [opts.envTemplatePath]
126
156
  * @param {string} [opts.envTemplate]
127
157
  * @param {Object} [opts.secrets]
158
+ * @param {{ envKey?: string }|null} [opts.scopedKv]
128
159
  * @returns {string}
129
160
  */
130
161
  function buildMissingSecretsErrorMessage(opts) {
131
- const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets } = opts;
162
+ const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets, scopedKv } = opts;
132
163
  const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
133
164
  const lines = [`Missing secrets: ${missing.join(', ')}`];
134
165
  if (fileInfo) lines.push(fileInfo.trim());
@@ -138,6 +169,9 @@ function buildMissingSecretsErrorMessage(opts) {
138
169
  if (templateLine) lines.push(`Env-template-line: ${ref}|${templateLine}`);
139
170
  }
140
171
  if (appName) lines.push(`App-name: ${appName}`);
172
+ for (const hint of buildStrictScopedKeyHints(missing, scopedKv)) {
173
+ lines.push(hint);
174
+ }
141
175
  if (hasStaleOAuthTemplatePaths(missing) && appName) {
142
176
  lines.push(`Stale-template-hint: oauth-leftover | repair ${appName}`);
143
177
  }