@aifabrix/builder 2.44.5 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/.cursor/rules/cli-layout.mdc +8 -4
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/README.md +15 -23
  4. package/integration/hubspot-test/README.md +2 -0
  5. package/integration/hubspot-test/test.js +5 -3
  6. package/jest.projects.js +104 -2
  7. package/lib/api/controller-health.api.js +49 -0
  8. package/lib/api/dimension-values.api.js +82 -0
  9. package/lib/api/dimensions.api.js +114 -0
  10. package/lib/api/external-systems.api.js +1 -0
  11. package/lib/api/integration-clients.api.js +168 -0
  12. package/lib/api/types/dimension-values.types.js +28 -0
  13. package/lib/api/types/dimensions.types.js +31 -0
  14. package/lib/api/types/integration-clients.types.js +45 -0
  15. package/lib/api/validation-runner.js +46 -25
  16. package/lib/app/deploy-config.js +11 -1
  17. package/lib/app/deploy-status-display.js +3 -3
  18. package/lib/app/deploy.js +36 -14
  19. package/lib/app/display.js +15 -11
  20. package/lib/app/helpers.js +3 -3
  21. package/lib/app/index.js +3 -3
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +7 -6
  24. package/lib/app/restart-display.js +126 -0
  25. package/lib/app/rotate-secret.js +7 -6
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +58 -19
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +148 -74
  32. package/lib/app/show-display.js +7 -0
  33. package/lib/app/show.js +87 -5
  34. package/lib/build/index.js +83 -49
  35. package/lib/cli/doctor-check.js +117 -0
  36. package/lib/cli/index.js +8 -2
  37. package/lib/cli/infra-guided.js +460 -0
  38. package/lib/cli/installation-log-command.js +73 -0
  39. package/lib/cli/setup-app.js +31 -3
  40. package/lib/cli/setup-auth.js +98 -27
  41. package/lib/cli/setup-dev-path-commands.js +50 -3
  42. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  43. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  44. package/lib/cli/setup-infra.js +132 -118
  45. package/lib/cli/setup-integration-client.js +182 -0
  46. package/lib/cli/setup-parameters.js +21 -2
  47. package/lib/cli/setup-platform.js +102 -0
  48. package/lib/cli/setup-secrets.js +18 -6
  49. package/lib/cli/setup-utility-resolve.js +132 -0
  50. package/lib/cli/setup-utility.js +143 -84
  51. package/lib/commands/app-logs.js +81 -33
  52. package/lib/commands/auth-config.js +116 -18
  53. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  54. package/lib/commands/datasource-capability-output.js +29 -0
  55. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  56. package/lib/commands/datasource-capability.js +411 -0
  57. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  58. package/lib/commands/datasource.js +53 -13
  59. package/lib/commands/dev-down.js +3 -3
  60. package/lib/commands/dev-infra-gate.js +32 -0
  61. package/lib/commands/dev-init.js +13 -7
  62. package/lib/commands/dimension-value.js +179 -0
  63. package/lib/commands/dimension.js +330 -0
  64. package/lib/commands/integration-client.js +430 -0
  65. package/lib/commands/login-device.js +65 -30
  66. package/lib/commands/login.js +21 -10
  67. package/lib/commands/parameters-validate.js +78 -13
  68. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  69. package/lib/commands/repair-datasource-keys.js +10 -5
  70. package/lib/commands/repair-datasource.js +19 -7
  71. package/lib/commands/repair-env-template.js +4 -1
  72. package/lib/commands/repair-openapi-sync.js +172 -0
  73. package/lib/commands/repair-persist.js +102 -0
  74. package/lib/commands/repair-rbac-extract.js +27 -0
  75. package/lib/commands/repair-rbac-migrate.js +186 -0
  76. package/lib/commands/repair-rbac.js +214 -31
  77. package/lib/commands/repair-system-alignment.js +246 -0
  78. package/lib/commands/repair-system-permissions.js +168 -0
  79. package/lib/commands/repair.js +120 -338
  80. package/lib/commands/secure.js +1 -1
  81. package/lib/commands/setup-modes.js +468 -0
  82. package/lib/commands/setup-prompts.js +421 -0
  83. package/lib/commands/setup.js +254 -0
  84. package/lib/commands/teardown.js +277 -0
  85. package/lib/commands/up-common.js +113 -19
  86. package/lib/commands/up-dataplane.js +44 -19
  87. package/lib/commands/up-miso.js +18 -18
  88. package/lib/commands/upload.js +111 -23
  89. package/lib/commands/wizard-core-helpers.js +14 -11
  90. package/lib/commands/wizard-core.js +6 -5
  91. package/lib/commands/wizard-dataplane.js +2 -2
  92. package/lib/commands/wizard-entity-selection.js +4 -3
  93. package/lib/commands/wizard-headless.js +2 -1
  94. package/lib/commands/wizard.js +2 -1
  95. package/lib/constants/infra-compose-service-names.js +40 -0
  96. package/lib/core/audit-logger.js +1 -34
  97. package/lib/core/config-admin-email.js +56 -0
  98. package/lib/core/config-normalize.js +60 -0
  99. package/lib/core/config-registered-controller-urls.js +54 -0
  100. package/lib/core/config.js +33 -50
  101. package/lib/core/env-reader.js +16 -3
  102. package/lib/core/secrets-admin-env.js +101 -0
  103. package/lib/core/secrets-ensure-infra.js +34 -1
  104. package/lib/core/secrets-ensure.js +88 -66
  105. package/lib/core/secrets-env-content.js +428 -0
  106. package/lib/core/secrets-env-declarative-expand.js +170 -0
  107. package/lib/core/secrets-env-write.js +29 -1
  108. package/lib/core/secrets-load.js +252 -0
  109. package/lib/core/secrets-names.js +32 -0
  110. package/lib/core/secrets.js +17 -757
  111. package/lib/datasource/capability/basic-exposure.js +76 -0
  112. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  113. package/lib/datasource/capability/capability-key.js +34 -0
  114. package/lib/datasource/capability/capability-resolve.js +172 -0
  115. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  116. package/lib/datasource/capability/copy-operations.js +348 -0
  117. package/lib/datasource/capability/copy-test-payload.js +139 -0
  118. package/lib/datasource/capability/create-operations.js +235 -0
  119. package/lib/datasource/capability/dimension-operations.js +151 -0
  120. package/lib/datasource/capability/dimension-validate.js +219 -0
  121. package/lib/datasource/capability/json-pointer.js +31 -0
  122. package/lib/datasource/capability/reference-rewrite.js +51 -0
  123. package/lib/datasource/capability/relate-operations.js +325 -0
  124. package/lib/datasource/capability/relate-validate.js +219 -0
  125. package/lib/datasource/capability/remove-operations.js +275 -0
  126. package/lib/datasource/capability/run-capability-copy.js +152 -0
  127. package/lib/datasource/capability/run-capability-diff.js +135 -0
  128. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  129. package/lib/datasource/capability/run-capability-edit.js +377 -0
  130. package/lib/datasource/capability/run-capability-relate.js +193 -0
  131. package/lib/datasource/capability/run-capability-remove.js +105 -0
  132. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  133. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  134. package/lib/datasource/list.js +136 -23
  135. package/lib/datasource/log-viewer.js +2 -4
  136. package/lib/datasource/unified-validation-run.js +51 -16
  137. package/lib/datasource/validate.js +53 -1
  138. package/lib/deployment/deploy-poll-ui.js +60 -0
  139. package/lib/deployment/deployer-status.js +29 -3
  140. package/lib/deployment/deployer.js +48 -30
  141. package/lib/deployment/environment.js +7 -2
  142. package/lib/deployment/poll-interval.js +72 -0
  143. package/lib/deployment/push.js +11 -9
  144. package/lib/external-system/deploy.js +9 -2
  145. package/lib/external-system/download.js +61 -32
  146. package/lib/external-system/sync-deploy-manifest.js +33 -0
  147. package/lib/infrastructure/index.js +49 -19
  148. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  149. package/lib/internal/node-fs.js +2 -0
  150. package/lib/parameters/infra-kv-discovery.js +29 -4
  151. package/lib/parameters/infra-parameter-catalog.js +6 -3
  152. package/lib/parameters/infra-parameter-validate.js +67 -19
  153. package/lib/resolvers/datasource-resolver.js +53 -0
  154. package/lib/resolvers/dimension-file.js +52 -0
  155. package/lib/resolvers/manifest-resolver.js +133 -0
  156. package/lib/schema/application-schema.json +4 -0
  157. package/lib/schema/external-datasource.schema.json +183 -53
  158. package/lib/schema/external-system.schema.json +23 -10
  159. package/lib/schema/infra.parameter.yaml +26 -1
  160. package/lib/schema/wizard-config.schema.json +1 -1
  161. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  162. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  163. package/lib/utils/app-config-resolver.js +24 -1
  164. package/lib/utils/app-run-containers.js +2 -2
  165. package/lib/utils/applications-config-defaults.js +206 -0
  166. package/lib/utils/auth-config-validator.js +2 -12
  167. package/lib/utils/bash-secret-env.js +59 -0
  168. package/lib/utils/cli-secrets-error-format.js +78 -0
  169. package/lib/utils/cli-test-layout-chalk.js +31 -9
  170. package/lib/utils/cli-utils.js +4 -36
  171. package/lib/utils/compose-generate-docker-compose.js +111 -6
  172. package/lib/utils/compose-generator.js +17 -8
  173. package/lib/utils/controller-url.js +50 -7
  174. package/lib/utils/datasource-test-run-display.js +8 -0
  175. package/lib/utils/dev-hosts-helper.js +3 -2
  176. package/lib/utils/dev-init-ssh-merge.js +2 -1
  177. package/lib/utils/docker-build.js +17 -9
  178. package/lib/utils/docker-reload-mount.js +127 -0
  179. package/lib/utils/env-copy.js +99 -14
  180. package/lib/utils/env-template.js +5 -1
  181. package/lib/utils/external-readme.js +71 -2
  182. package/lib/utils/external-system-local-test-tty.js +3 -2
  183. package/lib/utils/external-system-readiness-core.js +45 -12
  184. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  185. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  186. package/lib/utils/external-system-readiness-display.js +10 -1
  187. package/lib/utils/file-upload.js +40 -3
  188. package/lib/utils/health-check-db-init.js +107 -0
  189. package/lib/utils/health-check-public-warn.js +69 -0
  190. package/lib/utils/health-check-url.js +28 -10
  191. package/lib/utils/health-check.js +139 -107
  192. package/lib/utils/help-builder.js +5 -1
  193. package/lib/utils/image-name.js +34 -7
  194. package/lib/utils/infra-optional-service-flags.js +69 -0
  195. package/lib/utils/installation-log-core.js +282 -0
  196. package/lib/utils/installation-log-record.js +237 -0
  197. package/lib/utils/installation-log.js +123 -0
  198. package/lib/utils/integration-file-backup.js +74 -0
  199. package/lib/utils/log-redaction.js +105 -0
  200. package/lib/utils/manifest-location.js +164 -0
  201. package/lib/utils/manifest-source-emit.js +162 -0
  202. package/lib/utils/mutagen-install.js +30 -3
  203. package/lib/utils/paths.js +308 -76
  204. package/lib/utils/postgres-wipe.js +212 -0
  205. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  206. package/lib/utils/remote-dev-auth.js +21 -5
  207. package/lib/utils/remote-docker-env.js +9 -1
  208. package/lib/utils/remote-secrets-loader.js +49 -4
  209. package/lib/utils/resolve-docker-image-ref.js +9 -3
  210. package/lib/utils/run-cli-flags.js +29 -0
  211. package/lib/utils/secrets-ancestor-paths.js +47 -0
  212. package/lib/utils/secrets-canonical.js +10 -3
  213. package/lib/utils/secrets-helpers.js +17 -10
  214. package/lib/utils/secrets-kv-refs.js +42 -0
  215. package/lib/utils/secrets-kv-scope.js +19 -2
  216. package/lib/utils/secrets-materialize-local.js +134 -0
  217. package/lib/utils/secrets-path.js +26 -13
  218. package/lib/utils/secrets-utils.js +20 -10
  219. package/lib/utils/system-builder-root.js +42 -0
  220. package/lib/utils/url-declarative-public-base.js +80 -12
  221. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  222. package/lib/utils/url-declarative-resolve-build.js +24 -388
  223. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  224. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  225. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  226. package/lib/utils/url-declarative-resolve.js +47 -7
  227. package/lib/utils/url-declarative-runtime-base-path.js +52 -0
  228. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  229. package/lib/utils/urls-local-registry-scan.js +103 -0
  230. package/lib/utils/urls-local-registry.js +158 -76
  231. package/lib/utils/validation-poll-ui.js +81 -0
  232. package/lib/utils/validation-run-poll.js +29 -5
  233. package/lib/utils/with-muted-logger.js +53 -0
  234. package/package.json +3 -1
  235. package/templates/applications/dataplane/application.yaml +5 -1
  236. package/templates/applications/dataplane/rbac.yaml +10 -10
  237. package/templates/applications/keycloak/env.template +8 -6
  238. package/templates/applications/miso-controller/application.yaml +9 -0
  239. package/templates/applications/miso-controller/env.template +27 -29
  240. package/templates/applications/miso-controller/rbac.yaml +9 -9
  241. package/templates/external-system/README.md.hbs +83 -123
  242. package/.npmrc.token +0 -1
  243. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  244. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  245. package/.nyc_output/processinfo/index.json +0 -1
  246. package/lib/api/service-users.api.js +0 -150
  247. package/lib/api/types/service-users.types.js +0 -65
  248. package/lib/cli/setup-service-user.js +0 -187
  249. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Derive controller URLs stored in config (default + device token keys).
3
+ *
4
+ * @fileoverview Registered controller URL list for auth --set-controller pick mode
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Build sorted unique controller URLs from a config object.
13
+ * @param {Object} cfg - Parsed config.yaml object
14
+ * @param {(url: string) => string} normalizeControllerUrl - normalizer from config module
15
+ * @returns {string[]}
16
+ */
17
+ function buildRegisteredControllerUrlList(cfg, normalizeControllerUrl) {
18
+ const seen = new Set();
19
+ const add = (raw) => {
20
+ if (!raw || typeof raw !== 'string') return;
21
+ const trimmed = raw.trim();
22
+ if (!/^https?:\/\//i.test(trimmed)) return;
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(trimmed);
26
+ } catch {
27
+ return;
28
+ }
29
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;
30
+ seen.add(normalizeControllerUrl(trimmed));
31
+ };
32
+ if (cfg.controller) {
33
+ add(cfg.controller);
34
+ }
35
+ for (const key of Object.keys(cfg.device || {})) {
36
+ add(key);
37
+ }
38
+ return [...seen].sort((a, b) => a.localeCompare(b));
39
+ }
40
+
41
+ /**
42
+ * @param {() => Promise<Object>} getConfig - async config loader
43
+ * @param {(url: string) => string} normalizeControllerUrl
44
+ * @returns {Promise<string[]>}
45
+ */
46
+ async function getRegisteredControllerUrlsWithLoader(getConfig, normalizeControllerUrl) {
47
+ const cfg = await getConfig();
48
+ return buildRegisteredControllerUrlList(cfg, normalizeControllerUrl);
49
+ }
50
+
51
+ module.exports = {
52
+ buildRegisteredControllerUrlList,
53
+ getRegisteredControllerUrlsWithLoader
54
+ };
@@ -15,62 +15,15 @@ const os = require('os');
15
15
  const { encryptToken, decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
16
16
  const { ensureSecureFilePermissions, ensureSecureDirPermissions } = require('../utils/secure-file-permissions');
17
17
  const { getRuntimeConfigDir, getRuntimeConfigFile } = require('./config-runtime-paths');
18
+ const { getAdminEmailFromConfig, setAdminEmailInConfig } = require('./config-admin-email');
19
+ const { getRegisteredControllerUrlsWithLoader } = require('./config-registered-controller-urls');
20
+ const { normalizeControllerUrl, validateAndNormalizeDeveloperId } = require('./config-normalize');
18
21
  // Avoid importing paths.js here to prevent circular dependency; use shared runtime config dir helper.
19
22
  // Config location: AIFABRIX_CONFIG dirname → AIFABRIX_HOME (with ~/.aifabrix fallback when config lives there) → ~/.aifabrix
20
23
 
21
24
  // Cache for developer ID - loaded when getConfig() is first called
22
25
  let cachedDeveloperId = null;
23
26
 
24
- /**
25
- * Normalize controller URL for consistent storage and lookup
26
- * Removes trailing slashes and normalizes the URL format
27
- * @param {string} url - Controller URL to normalize
28
- * @returns {string} Normalized controller URL
29
- */
30
- function normalizeControllerUrl(url) {
31
- if (!url || typeof url !== 'string') {
32
- return url;
33
- }
34
- // Remove trailing slashes
35
- let normalized = url.trim().replace(/\/+$/, '');
36
- // Ensure it starts with http:// or https://
37
- if (!normalized.match(/^https?:\/\//)) {
38
- // If it doesn't start with protocol, assume http://
39
- normalized = `http://${normalized}`;
40
- }
41
- return normalized;
42
- }
43
-
44
- /**
45
- * Validate and normalize developer ID
46
- * @param {*} developerId - Developer ID value (can be string, number, undefined, or null)
47
- * @returns {string} Normalized developer ID as string
48
- * @throws {Error} If developer ID is invalid
49
- */
50
- function validateAndNormalizeDeveloperId(developerId) {
51
- const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
52
-
53
- if (typeof developerId === 'undefined' || developerId === null) {
54
- return '0';
55
- }
56
-
57
- if (typeof developerId === 'number') {
58
- if (developerId < 0 || !Number.isFinite(developerId)) {
59
- throw new Error('Developer ID must be a non-negative digit string or number');
60
- }
61
- return String(developerId);
62
- }
63
-
64
- if (typeof developerId === 'string') {
65
- if (!DEV_ID_DIGITS_REGEX.test(developerId)) {
66
- throw new Error('Developer ID must be a non-negative digit string or number');
67
- }
68
- return developerId;
69
- }
70
-
71
- throw new Error('Developer ID must be a non-negative digit string or number');
72
- }
73
-
74
27
  /**
75
28
  * Ensure default config values exist
76
29
  * @param {Object} config - Configuration object
@@ -258,6 +211,25 @@ async function getTlsEnabled() {
258
211
  return cfg.tlsEnabled === true;
259
212
  }
260
213
 
214
+ /**
215
+ * Admin email stored in `config.yaml` by `aifabrix setup` (Keycloak / pgAdmin). Empty when unset.
216
+ *
217
+ * @returns {Promise<string>}
218
+ */
219
+ async function getAdminEmail() {
220
+ return getAdminEmailFromConfig(getConfig);
221
+ }
222
+
223
+ /**
224
+ * Persist admin email to `config.yaml` (used on subsequent setup runs as the prompt default).
225
+ *
226
+ * @param {string} email
227
+ * @returns {Promise<void>}
228
+ */
229
+ async function setAdminEmail(email) {
230
+ return setAdminEmailInConfig(getConfig, saveConfig, email);
231
+ }
232
+
261
233
  /**
262
234
  * Whether Traefik is enabled (`traefik: true` in config; infra compose includes the proxy).
263
235
  * @returns {Promise<boolean>}
@@ -316,6 +288,14 @@ async function getControllerUrl() {
316
288
  return config.controller || null;
317
289
  }
318
290
 
291
+ /**
292
+ * Controller URLs from config: `controller` plus `device` token keys.
293
+ * @returns {Promise<string[]>}
294
+ */
295
+ async function getRegisteredControllerUrls() {
296
+ return getRegisteredControllerUrlsWithLoader(getConfig, normalizeControllerUrl);
297
+ }
298
+
319
299
  function isTokenExpired(expiresAt) {
320
300
  if (!expiresAt) return true;
321
301
  const expirationTime = new Date(expiresAt).getTime();
@@ -454,6 +434,8 @@ const exportsObj = {
454
434
  loadDeveloperId,
455
435
  getCurrentEnvironment,
456
436
  getTlsEnabled,
437
+ getAdminEmail,
438
+ setAdminEmail,
457
439
  getTraefikEnabled,
458
440
  setCurrentEnvironment,
459
441
  resolveEnvironment,
@@ -469,6 +451,7 @@ const exportsObj = {
469
451
  normalizeControllerUrl,
470
452
  setControllerUrl,
471
453
  getControllerUrl,
454
+ getRegisteredControllerUrls,
472
455
  get CONFIG_DIR() {
473
456
  return getRuntimeConfigDir();
474
457
  },
@@ -115,6 +115,19 @@ function detectSensitiveValue(key, value) {
115
115
  return false;
116
116
  }
117
117
 
118
+ /**
119
+ * Path segment after `kv://` for sensitive env vars; must match infra.parameter.yaml keys.
120
+ * API_KEY uses the shared miso-controller/dataplane catalog entry (not a flat `api-key` slug).
121
+ * @param {string} key - Environment variable name
122
+ * @returns {string}
123
+ */
124
+ function sensitiveKvPathSegmentFromEnvKey(key) {
125
+ if (key === 'API_KEY') {
126
+ return 'miso-controller-secrets-apiKeyVault';
127
+ }
128
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '-');
129
+ }
130
+
118
131
  /**
119
132
  * Convert existing .env variables to env.template format
120
133
  * @param {Object} existingEnv - Existing environment variables
@@ -128,7 +141,7 @@ function convertToEnvTemplate(existingEnv, requiredVars) {
128
141
  Object.entries(existingEnv).forEach(([key, value]) => {
129
142
  if (detectSensitiveValue(key, value)) {
130
143
  // Convert sensitive values to kv:// references
131
- convertedEnv[key] = `kv://${key.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
144
+ convertedEnv[key] = `kv://${sensitiveKvPathSegmentFromEnvKey(key)}`;
132
145
  } else {
133
146
  // Keep non-sensitive values as-is
134
147
  convertedEnv[key] = value;
@@ -148,8 +161,8 @@ function generateSecretsFromEnv(envVars) {
148
161
 
149
162
  Object.entries(envVars).forEach(([key, value]) => {
150
163
  if (detectSensitiveValue(key, value)) {
151
- // Use centralized resolver for canonical secret names
152
- const secretName = getCanonicalSecretName(key);
164
+ const secretName =
165
+ key === 'API_KEY' ? sensitiveKvPathSegmentFromEnvKey(key) : getCanonicalSecretName(key);
153
166
  secrets[secretName] = value;
154
167
  }
155
168
  });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Infrastructure admin-secrets.env generation (PG/Redis Commander defaults).
3
+ *
4
+ * @fileoverview Split from secrets.js for module size limits
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const logger = require('../utils/logger');
14
+ const config = require('./config');
15
+ const {
16
+ mergeInfraParameterDefaultsForCli,
17
+ getInfraParameterCatalog,
18
+ readRelaxedCatalogDefaults
19
+ } = require('../parameters/infra-parameter-catalog');
20
+ const { createDefaultSecrets } = require('../utils/secrets-generator');
21
+ const pathsUtil = require('../utils/paths');
22
+ const { loadSecrets } = require('./secrets-load');
23
+
24
+ /**
25
+ * Writes admin env key-value pairs to content; encrypts values when encryption key is set.
26
+ * @async
27
+ * @param {Object.<string, string>} adminObj - Key-value object (e.g. POSTGRES_PASSWORD, ...)
28
+ * @returns {Promise<string>} .env-style content (plaintext or secure:// for secrets)
29
+ */
30
+ async function formatAdminSecretsContent(adminObj) {
31
+ const encryptionKey = await config.getSecretsEncryptionKey();
32
+ const { encryptSecret } = require('../utils/secrets-encryption');
33
+ const lines = ['# Infrastructure Admin Credentials'];
34
+ for (const [k, v] of Object.entries(adminObj)) {
35
+ const value = (v === null || v === undefined) ? '' : String(v).replace(/\n/g, ' ').trim();
36
+ const valueToWrite = encryptionKey ? encryptSecret(value, encryptionKey) : value;
37
+ lines.push(`${k}=${valueToWrite}`);
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+
42
+ async function loadSecretsOrBootstrapForAdmin(secretsPath) {
43
+ try {
44
+ return await loadSecrets(secretsPath);
45
+ } catch (error) {
46
+ const defaultSecretsPath = secretsPath || path.join(pathsUtil.getAifabrixHome(), 'secrets.yaml');
47
+ if (!fs.existsSync(defaultSecretsPath)) {
48
+ logger.log('Creating default secrets file...');
49
+ await createDefaultSecrets(defaultSecretsPath);
50
+ return await loadSecrets(secretsPath);
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ function getInfraDefaultsMergedForAdmin() {
57
+ try {
58
+ return mergeInfraParameterDefaultsForCli(getInfraParameterCatalog().data, {});
59
+ } catch {
60
+ return {};
61
+ }
62
+ }
63
+
64
+ function buildLocalAdminSecretsObject(secrets, infraDefaults) {
65
+ const raw = secrets['postgres-passwordKeyVault'];
66
+ const relaxed = readRelaxedCatalogDefaults();
67
+ const postgresPassword =
68
+ (raw && String(raw).trim()) ||
69
+ infraDefaults.adminPassword ||
70
+ relaxed.adminPassword ||
71
+ '';
72
+ const pgAdminEmail = infraDefaults.adminEmail || relaxed.adminEmail || '';
73
+ return {
74
+ POSTGRES_PASSWORD: postgresPassword,
75
+ PGADMIN_DEFAULT_EMAIL: pgAdminEmail,
76
+ PGADMIN_DEFAULT_PASSWORD: postgresPassword,
77
+ REDIS_HOST: 'local:redis:6379:0:',
78
+ REDIS_COMMANDER_USER: 'admin',
79
+ REDIS_COMMANDER_PASSWORD: postgresPassword
80
+ };
81
+ }
82
+
83
+ /** Generates admin secrets for infrastructure (beside config.yaml, typically ~/.aifabrix/admin-secrets.env). Defaults from infra.parameter.yaml `defaults`. */
84
+ async function generateAdminSecretsEnv(secretsPath) {
85
+ const secrets = await loadSecretsOrBootstrapForAdmin(secretsPath);
86
+ const infraDefaults = getInfraDefaultsMergedForAdmin();
87
+ const adminObj = buildLocalAdminSecretsObject(secrets, infraDefaults);
88
+ const aifabrixDir = pathsUtil.getAifabrixSystemDir();
89
+ const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
90
+ if (!fs.existsSync(aifabrixDir)) {
91
+ fs.mkdirSync(aifabrixDir, { recursive: true, mode: 0o700 });
92
+ }
93
+ const adminSecrets = await formatAdminSecretsContent(adminObj);
94
+ fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
95
+ return adminEnvPath;
96
+ }
97
+
98
+ module.exports = {
99
+ formatAdminSecretsContent,
100
+ generateAdminSecretsEnv
101
+ };
@@ -70,8 +70,41 @@ function getInfraSecretKeysForUpInfra() {
70
70
  }
71
71
  }
72
72
 
73
+ /**
74
+ * Same merge as runtime `loadSecrets()` (primary `~/.aifabrix/secrets.local.yaml` plus `aifabrix-secrets` file or remote API).
75
+ * Ensures missing-key checks treat remote `--shared` keys as already satisfied.
76
+ *
77
+ * @returns {Promise<Object.<string, string>>}
78
+ */
79
+ async function loadMergedSecretsForEnsureMissingCheck() {
80
+ const { loadSecrets } = require('./secrets-load');
81
+ return loadSecrets(undefined);
82
+ }
83
+
84
+ /**
85
+ * Default on for writes to the primary user secrets file: consult full {@link loadSecrets} merge so remote
86
+ * `--shared` keys are not duplicated locally. Opt out with `useMergedSecretsForMissingKeys: false`.
87
+ *
88
+ * @param {{ useMergedSecretsForMissingKeys?: boolean }} options
89
+ * @param {{ type?: string, filePath?: string }} target
90
+ * @returns {boolean}
91
+ */
92
+ function shouldUseMergedSecretsForMissingKeys(options, target) {
93
+ if (options.useMergedSecretsForMissingKeys === false) return false;
94
+ if (options.useMergedSecretsForMissingKeys === true) return true;
95
+ if (!target || target.type !== 'file' || !target.filePath) return false;
96
+ try {
97
+ const primary = pathsUtil.getPrimaryUserSecretsLocalPath();
98
+ return path.resolve(target.filePath) === path.resolve(primary);
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
73
104
  module.exports = {
74
105
  buildInfraPlaceholderContext,
75
106
  isSecretKeyAllowedEmpty,
76
- getInfraSecretKeysForUpInfra
107
+ getInfraSecretKeysForUpInfra,
108
+ loadMergedSecretsForEnsureMissingCheck,
109
+ shouldUseMergedSecretsForMissingKeys
77
110
  };
@@ -1,10 +1,11 @@
1
1
  /**
2
- * AI Fabrix Builder – Ensure secrets in configured store
2
+ * AI Fabrix Builder – Ensure secrets in the primary user store
3
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.
4
+ * Automated flows (resolve, run, wizard, up-infra backfill) **only** write to
5
+ * `~/.aifabrix/secrets.local.yaml` (or the path from `getPrimaryUserSecretsLocalPath`).
6
+ * The shared store (`aifabrix-secrets` file or remote API) is **read** via `loadSecrets()`;
7
+ * writing there is **human-only** through `aifabrix secret set --shared` (see `commands/secrets-set.js`).
8
+ * New values are encrypted when secrets-encryption is set.
8
9
  *
9
10
  * @fileoverview Central ensure-secrets service for zero-touch install
10
11
  * @author AI Fabrix Team
@@ -17,6 +18,7 @@ const os = require('os');
17
18
  const config = require('./config');
18
19
  const pathsUtil = require('../utils/paths');
19
20
  const logger = require('../utils/logger');
21
+ const { formatSuccessLine, metadata } = require('../utils/cli-test-layout-chalk');
20
22
  const remoteDevAuth = require('../utils/remote-dev-auth');
21
23
  const devApi = require('../api/dev.api');
22
24
  const {
@@ -31,7 +33,9 @@ const { loadEnvTemplate } = require('../utils/secrets-helpers');
31
33
  const {
32
34
  buildInfraPlaceholderContext,
33
35
  isSecretKeyAllowedEmpty,
34
- getInfraSecretKeysForUpInfra
36
+ getInfraSecretKeysForUpInfra,
37
+ loadMergedSecretsForEnsureMissingCheck,
38
+ shouldUseMergedSecretsForMissingKeys
35
39
  } = require('./secrets-ensure-infra');
36
40
  const { syncLiteralKvSecretsFromCliOverrides } = require('./secrets-infra-placeholder-sync');
37
41
 
@@ -59,10 +63,12 @@ function expandTilde(filePath) {
59
63
  }
60
64
 
61
65
  /**
62
- * Resolve write target from config.
66
+ * Resolve the **configured** shared store for inspection/validation (e.g. `secret validate` without a path).
67
+ * This is the `aifabrix-secrets` target (file or remote API), not the default for automated writes.
68
+ *
63
69
  * - File path → that path (expand ~)
64
- * - http(s) URL → remote (fallback: user file)
65
- * - No config → user file
70
+ * - http(s) URL → remote
71
+ * - No config → primary user secrets file
66
72
  *
67
73
  * @returns {Promise<{ type: 'file'|'remote', filePath?: string, serverUrl?: string|null, secretsEndpointUrl?: string, clientCertPem?: string|null, serverCaPem?: string|null }>}
68
74
  */
@@ -91,6 +97,16 @@ async function resolveWriteTarget() {
91
97
  return { type: 'file', filePath };
92
98
  }
93
99
 
100
+ /**
101
+ * Default write target for **automated** ensure / `setSecretInStore` (resolve, run, wizard, infra).
102
+ * Never the shared store; use `aifabrix secret set --shared` to write shared.
103
+ *
104
+ * @returns {Promise<{ type: 'file', filePath: string }>}
105
+ */
106
+ async function resolvePrimaryUserWriteTarget() {
107
+ return { type: 'file', filePath: pathsUtil.getPrimaryUserSecretsLocalPath() };
108
+ }
109
+
94
110
  /**
95
111
  * Load existing secrets from the resolved target (file or remote).
96
112
  *
@@ -166,34 +182,36 @@ function valueForKey(key, suggested, emptyForCredentials, placeholderContext) {
166
182
  }
167
183
 
168
184
  /**
169
- * Add secrets via remote API; on failure write to local file (with encryption if configured).
170
- * @param {Object} target - Resolved target (remote)
171
- * @param {string[]} toAdd - Keys to add
172
- * @param {Object} suggested - Suggested values
173
- * @param {string[]} added - Array to push added keys to
174
- * @param {string|null} encryptionKey - Encryption key for file fallback or null
175
- * @returns {Promise<string[]>}
185
+ * Short label for logs (file path or remote secrets URL).
186
+ * @param {{ type?: string, filePath?: string, secretsEndpointUrl?: string }|null} target
187
+ * @returns {string}
176
188
  */
177
- async function addSecretsRemote(target, toAdd, suggested, added, encryptionKey, placeholderContext) {
178
- const emptyForCredentials = false;
179
- for (const key of toAdd) {
180
- const value = valueForKey(key, suggested, emptyForCredentials, placeholderContext);
181
- try {
182
- await devApi.addSecret(
183
- target.serverUrl,
184
- target.clientCertPem,
185
- { key, value },
186
- target.serverCaPem || undefined,
187
- target.secretsEndpointUrl
188
- );
189
- added.push(key);
190
- } catch (err) {
191
- logger.warn(`Remote secret "${key}" failed (${err.message}); writing to local file.`);
192
- await writeSecretToFile(target.filePath, key, value, encryptionKey);
193
- added.push(key);
194
- }
189
+ function summarizeSecretsStoreForLog(target) {
190
+ if (!target || typeof target !== 'object') return 'secrets store';
191
+ if (target.type === 'remote' && typeof target.secretsEndpointUrl === 'string' && target.secretsEndpointUrl.trim()) {
192
+ return target.secretsEndpointUrl.trim();
195
193
  }
196
- return added;
194
+ if (typeof target.filePath === 'string' && target.filePath.trim()) {
195
+ return target.filePath.trim();
196
+ }
197
+ return 'secrets store';
198
+ }
199
+
200
+ /**
201
+ * Log that new values were written for keys that were missing or empty.
202
+ * @param {string[]} added
203
+ * @param {{ type?: string, filePath?: string, secretsEndpointUrl?: string }|null} target
204
+ */
205
+ function logNewSecretValuesWritten(added, target) {
206
+ if (!Array.isArray(added) || added.length === 0) return;
207
+ const where = summarizeSecretsStoreForLog(target);
208
+ logger.log(
209
+ formatSuccessLine(
210
+ `Wrote ${added.length} new secret value(s) to ${where}. ` +
211
+ 'These keys were missing or empty; values are now stored.'
212
+ )
213
+ );
214
+ logger.log(metadata(` Keys: ${added.join(', ')}`));
197
215
  }
198
216
 
199
217
  /**
@@ -223,9 +241,7 @@ async function addSecretsToFile(batch) {
223
241
  await writeSecretToFile(filePath, key, value, encryptionKey);
224
242
  added.push(key);
225
243
  }
226
- if (added.length > 0) {
227
- logger.log(`✔ Ensured ${added.length} secret key(s): ${added.join(', ')}`);
228
- }
244
+ logNewSecretValuesWritten(added, { type: 'file', filePath });
229
245
  return added;
230
246
  }
231
247
 
@@ -240,6 +256,9 @@ async function addSecretsToFile(batch) {
240
256
  * @param {Object} [options] - Options
241
257
  * @param {boolean} [options.emptyValuesForCredentials=false] - Use empty string for new values
242
258
  * @param {Object} [options.suggestedValues] - Optional map key -> value for specific keys
259
+ * @param {boolean} [options.useMergedSecretsForMissingKeys] - When true, treat keys present in the full
260
+ * `loadSecrets()` merge (including remote `--shared`) as satisfied. When false, only the target store file
261
+ * is consulted. When omitted and the target file is the primary user secrets path, merged secrets are used (default).
243
262
  * @returns {Promise<string[]>} Keys that were added (new or backfilled)
244
263
  * @throws {Error} If config or file write fails
245
264
  */
@@ -253,8 +272,11 @@ async function ensureSecretsForKeys(keys, options = {}) {
253
272
  : {};
254
273
  const placeholderContext = options.placeholderContext;
255
274
 
256
- const target = options._targetOverride || await resolveWriteTarget();
257
- const existing = await loadExistingFromTarget(target);
275
+ const target = options._targetOverride || (await resolvePrimaryUserWriteTarget());
276
+ let existing = await loadExistingFromTarget(target);
277
+ if (shouldUseMergedSecretsForMissingKeys(options, target)) {
278
+ existing = await loadMergedSecretsForEnsureMissingCheck();
279
+ }
258
280
  const toAdd = keys.filter((k) => {
259
281
  const v = existing[k];
260
282
  const missingOrEmpty = v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
@@ -265,9 +287,6 @@ async function ensureSecretsForKeys(keys, options = {}) {
265
287
  const encryptionKey = await config.getSecretsEncryptionKey();
266
288
  const added = [];
267
289
 
268
- if (target.type === 'remote' && target.serverUrl && target.clientCertPem) {
269
- return addSecretsRemote(target, toAdd, suggested, added, encryptionKey, placeholderContext);
270
- }
271
290
  return addSecretsToFile({
272
291
  filePath: target.filePath,
273
292
  toAdd,
@@ -288,6 +307,8 @@ async function ensureSecretsForKeys(keys, options = {}) {
288
307
  * @param {string} envTemplatePathOrContent - Path to env.template or template content
289
308
  * @param {Object} [options] - Options
290
309
  * @param {boolean} [options.emptyValuesForCredentials=false] - Use empty string for new values
310
+ * @param {boolean} [options.useMergedSecretsForMissingKeys] - Same semantics as {@link ensureSecretsForKeys};
311
+ * defaults to merged lookup when the write target is the primary user secrets file
291
312
  * @returns {Promise<string[]>} Keys that were added
292
313
  * @throws {Error} If template cannot be read or ensure fails
293
314
  */
@@ -313,9 +334,12 @@ async function ensureSecretsFromEnvTemplate(envTemplatePathOrContent, options =
313
334
  : path.resolve(process.cwd(), options.preferredFilePath);
314
335
  target = { type: 'file', filePath };
315
336
  } else {
316
- target = await resolveWriteTarget();
337
+ target = await resolvePrimaryUserWriteTarget();
338
+ }
339
+ let existing = await loadExistingFromTarget(target);
340
+ if (shouldUseMergedSecretsForMissingKeys(options, target)) {
341
+ existing = await loadMergedSecretsForEnsureMissingCheck();
317
342
  }
318
- const existing = await loadExistingFromTarget(target);
319
343
  const missingKeys = findMissingSecretKeys(template, existing);
320
344
  return ensureSecretsForKeys(missingKeys, { ...options, _targetOverride: target });
321
345
  }
@@ -358,29 +382,16 @@ async function writeSecretToStoreFile(filePath, key, strValue) {
358
382
  */
359
383
  async function setSecretInStore(key, value) {
360
384
  if (!key || typeof key !== 'string' || value === undefined) return;
361
- const target = await resolveWriteTarget();
362
385
  const strValue = typeof value === 'string' ? value : String(value);
363
- if (target.type === 'remote' && target.serverUrl && target.clientCertPem) {
364
- try {
365
- await devApi.addSecret(
366
- target.serverUrl,
367
- target.clientCertPem,
368
- { key, value: strValue },
369
- target.serverCaPem || undefined,
370
- target.secretsEndpointUrl
371
- );
372
- } catch (err) {
373
- logger.warn(`Could not sync secret "${key}" to remote store: ${err.message}`);
374
- const encryptionKey = await config.getSecretsEncryptionKey();
375
- await writeSecretToFile(target.filePath, key, strValue, encryptionKey);
376
- }
377
- return;
378
- }
379
- await writeSecretToStoreFile(target.filePath, key, strValue);
386
+ const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
387
+ await writeSecretToStoreFile(userPath, key, strValue);
380
388
  }
381
389
 
382
390
  /**
383
- * Ensure infra secrets exist in the configured store. Call before ensureAdminSecrets or startInfra.
391
+ * Ensure infra secrets exist before ensureAdminSecrets or startInfra.
392
+ * Writes go to the primary user secrets file only (same as all **`ensureSecretsForKeys`** / **`setSecretInStore`** paths).
393
+ * **`kv://` resolution** merges the shared store via **`loadSecrets()`**. To publish a key to shared, use
394
+ * **`aifabrix secret set --shared`**.
384
395
  *
385
396
  * @async
386
397
  * @function ensureInfraSecrets
@@ -398,11 +409,21 @@ async function setSecretInStore(key, value) {
398
409
  async function ensureInfraSecrets(options = {}) {
399
410
  const keys = getInfraSecretKeysForUpInfra();
400
411
  const placeholderContext = buildInfraPlaceholderContext(options);
401
- const added = await ensureSecretsForKeys(keys, { placeholderContext });
412
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
413
+ const added = await ensureSecretsForKeys(keys, {
414
+ placeholderContext,
415
+ _targetOverride: { type: 'file', filePath: userSecretsPath }
416
+ });
417
+
418
+ async function writeInfraPlaceholderLiteralToUserLocal(key, value) {
419
+ const strValue = typeof value === 'string' ? value : String(value);
420
+ await writeSecretToStoreFile(userSecretsPath, key, strValue);
421
+ }
422
+
402
423
  await syncLiteralKvSecretsFromCliOverrides(
403
424
  options,
404
425
  placeholderContext,
405
- setSecretInStore,
426
+ writeInfraPlaceholderLiteralToUserLocal,
406
427
  infraParameterCatalogModule
407
428
  );
408
429
  return added;
@@ -414,6 +435,7 @@ module.exports = {
414
435
  ensureInfraSecrets,
415
436
  setSecretInStore,
416
437
  resolveWriteTarget,
438
+ resolvePrimaryUserWriteTarget,
417
439
  loadExistingFromTarget,
418
440
  getInfraSecretKeysForUpInfra,
419
441
  isSecretKeyAllowedEmpty,