@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,428 @@
1
+ /**
2
+ * kv:// resolution, declarative URL expansion, and .env generation (content + file write).
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 { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
+ const config = require('./config');
15
+ const {
16
+ interpolateEnvVars,
17
+ collectMissingSecrets,
18
+ formatMissingSecretsFileInfo,
19
+ replaceKvInContent,
20
+ loadEnvTemplate,
21
+ adjustLocalEnvPortsInContent,
22
+ rewriteInfraEndpoints
23
+ } = require('../utils/secrets-helpers');
24
+ const { buildEnvVarMap } = require('../utils/env-map');
25
+ const { resolveServicePortsInEnvContent } = require('../utils/secrets-url');
26
+ const { materializeResolvedKvSecretsToUserLocal } = require('../utils/secrets-materialize-local');
27
+ const {
28
+ updatePortForDocker,
29
+ getBaseDockerEnv,
30
+ applyDockerEnvOverride,
31
+ getContainerPortFromDockerEnv
32
+ } = require('./secrets-docker-env');
33
+ const { getContainerPortFromPath, loadVariablesFromPath } = require('../utils/port-resolver');
34
+ const secretsEnsure = require('./secrets-ensure');
35
+ const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
36
+ const pathsUtil = require('../utils/paths');
37
+ const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
38
+ const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
39
+ const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
40
+ const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
41
+ const {
42
+ mergeDockerManifestPublishedPort,
43
+ rewriteDockerManifestPublicPortEnvLine
44
+ } = require('../utils/docker-manifest-public-port');
45
+ const { loadSecrets } = require('./secrets-load');
46
+
47
+ /**
48
+ * Resolves kv:// references in environment template
49
+ * Replaces kv://keyName with actual values from secrets
50
+ *
51
+ * @async
52
+ * @param {string} envTemplate - Environment template content
53
+ * @param {Object} secrets - Secrets object from loadSecrets()
54
+ * @param {string} [environment='local'] - Environment context (docker/local)
55
+ * @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
56
+ * @param {string} [appName] - Application name (optional, for error messages)
57
+ * @returns {Promise<string>} Resolved environment content
58
+ * @throws {Error} If kv:// reference cannot be resolved
59
+ */
60
+ async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null, scopedKv = null) {
61
+ const os = require('os');
62
+
63
+ let developerId = null;
64
+ try {
65
+ developerId = await config.getDeveloperId();
66
+ } catch {
67
+ /* ignore */
68
+ }
69
+
70
+ const envKey = String(environment || 'local').toLowerCase();
71
+ const mapContext = envKey === 'docker' || envKey === 'local' ? envKey : 'local';
72
+
73
+ let envVars = await buildEnvVarMap(mapContext, os, developerId);
74
+ if (!envVars || Object.keys(envVars).length === 0) {
75
+ envVars = await buildEnvVarMap('local', os, developerId);
76
+ }
77
+ const resolved = interpolateEnvVars(envTemplate, envVars);
78
+ const missing = collectMissingSecrets(resolved, secrets, scopedKv);
79
+ if (missing.length > 0) {
80
+ const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
81
+ const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
82
+ throw new Error(`Missing secrets: ${missing.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
83
+ }
84
+ return replaceKvInContent(resolved, secrets, envVars, scopedKv);
85
+ }
86
+
87
+ /**
88
+ * Resolve run env key and effective env-scoped kv/redis behavior for generateEnvContent.
89
+ *
90
+ * @async
91
+ * @param {string} appPath - Builder application directory
92
+ * @param {Object} [options] - generateEnvContent options; may set runEnvKey
93
+ * @returns {Promise<{ runEnvKey: string, effective: boolean }>}
94
+ */
95
+ async function buildScopedKvContext(appPath, options = {}) {
96
+ let runEnvKey;
97
+ if (options.runEnvKey !== undefined && options.runEnvKey !== null) {
98
+ runEnvKey = String(options.runEnvKey).toLowerCase();
99
+ } else if (typeof config.getCurrentEnvironment === 'function') {
100
+ runEnvKey = String((await config.getCurrentEnvironment()) || 'dev').toLowerCase();
101
+ } else {
102
+ runEnvKey = 'dev';
103
+ }
104
+ const userCfg =
105
+ typeof config.getConfig === 'function'
106
+ ? await config.getConfig()
107
+ : { useEnvironmentScopedResources: false };
108
+ const useGate = Boolean(userCfg.useEnvironmentScopedResources);
109
+ const appFlag = readAppEnvironmentScopedFlagForAppPath(appPath);
110
+ const effective = computeEffectiveEnvironmentScopedResources(useGate, appFlag, runEnvKey);
111
+ return { runEnvKey, effective };
112
+ }
113
+
114
+ /**
115
+ * Redis/DB service endpoints for docker env interpolation.
116
+ * @returns {Promise<{ redisHost: string, redisPort: number, dbHost: string, dbPort: number }>}
117
+ */
118
+ async function getDockerRedisDbEndpoints() {
119
+ const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
120
+ const hosts = await getEnvHosts('docker');
121
+ const localhostOverride = getLocalhostOverride('docker');
122
+ const redisHost = getServiceHost(hosts.REDIS_HOST, 'docker', 'redis', localhostOverride);
123
+ const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
124
+ const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
125
+ const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
126
+ return { redisHost, redisPort, dbHost, dbPort };
127
+ }
128
+
129
+ /** Docker env transformations: ports, infra endpoints, PORT. */
130
+ async function applyDockerTransformations(resolved, variablesPath) {
131
+ resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
132
+ resolved = await rewriteInfraEndpoints(resolved, 'docker');
133
+ const { redisHost, redisPort, dbHost, dbPort } = await getDockerRedisDbEndpoints();
134
+ let dockerEnv = await getBaseDockerEnv();
135
+ dockerEnv = applyDockerEnvOverride(dockerEnv);
136
+ const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
137
+ const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
138
+ const appDoc = loadVariablesFromPath(variablesPath);
139
+ await mergeDockerManifestPublishedPort(envVars, appDoc);
140
+ envVars.REDIS_HOST = redisHost;
141
+ envVars.REDIS_PORT = String(redisPort);
142
+ envVars.DB_HOST = dbHost;
143
+ envVars.DB_PORT = String(dbPort);
144
+ envVars.PORT = String(containerPort);
145
+ resolved = interpolateEnvVars(resolved, envVars);
146
+ resolved = rewriteDockerManifestPublicPortEnvLine(resolved, envVars, appDoc);
147
+ return updatePortForDocker(resolved, variablesPath);
148
+ }
149
+
150
+ /** Environment-specific transformations to resolved content. */
151
+ async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
152
+ if (environment === 'docker') return applyDockerTransformations(resolved, variablesPath);
153
+ if (environment === 'local') return adjustLocalEnvPortsInContent(resolved, variablesPath);
154
+ return resolved;
155
+ }
156
+
157
+ /**
158
+ * Generate .env content from template and secrets (no disk write).
159
+ * When options.envOnly is true, variablesPath is null (no application config).
160
+ *
161
+ * @param {string} appName - Application name
162
+ * @param {string} [secretsPath] - Path to secrets file (optional)
163
+ * @param {string} [environment='local'] - Environment context
164
+ * @param {boolean} [force=false] - Generate missing secret keys
165
+ * @param {Object} [options] - Optional: appPath, envOnly (env-only mode uses only env.template); skipMaterializeKvToLocal skips persisting resolved kv to ~/.aifabrix/secrets.local.yaml. Materialization runs only when no explicit secretsPath is passed (default loadSecrets cascade uses ~/.aifabrix/secrets.local.yaml).
166
+ * @returns {Promise<string>} Resolved env content
167
+ */
168
+ async function generateEnvContent(appName, secretsPath, environment = 'local', force = false, options = {}) {
169
+ const appPath = (options && options.appPath) || pathsUtil.getBuilderPath(appName);
170
+ const templatePath = path.join(appPath, 'env.template');
171
+ const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
172
+ const template = loadEnvTemplate(templatePath);
173
+ const secretsPaths = await getActualSecretsPath(secretsPath, appName);
174
+ if (force) {
175
+ const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
176
+ await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
177
+ }
178
+ const secrets = await loadSecrets(secretsPath, appName);
179
+ const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
180
+ const scopedKv = { envKey: runEnvKey, effective };
181
+ let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, scopedKv);
182
+ if (!secretsPath) {
183
+ await materializeResolvedKvSecretsToUserLocal(template, secrets, scopedKv, options);
184
+ }
185
+ resolved = await expandDeclarativeUrlsIfPresent(
186
+ resolved,
187
+ appName,
188
+ appPath,
189
+ variablesPath,
190
+ environment,
191
+ Boolean(options.envOnly)
192
+ );
193
+ resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
194
+ if (effective) {
195
+ const idx = redisDbIndexForScopedRunEnv(runEnvKey);
196
+ resolved = applyRedisDbIndexToEnvContent(resolved, idx);
197
+ }
198
+
199
+ return resolved;
200
+ }
201
+
202
+ /**
203
+ * Parses .env file content into a key-to-value map.
204
+ * Only includes lines that look like KEY=value (first = separates key and value).
205
+ *
206
+ * @param {string} content - Raw .env file content
207
+ * @returns {Object.<string, string>} Map of variable name to value
208
+ */
209
+ function parseEnvContentToMap(content) {
210
+ if (!content || typeof content !== 'string') {
211
+ return {};
212
+ }
213
+ const map = {};
214
+ const lines = content.split(/\r?\n/);
215
+ for (const line of lines) {
216
+ const trimmed = line.trim();
217
+ if (!trimmed || trimmed.startsWith('#')) {
218
+ continue;
219
+ }
220
+ const eq = trimmed.indexOf('=');
221
+ if (eq > 0) {
222
+ const key = trimmed.substring(0, eq).trim();
223
+ const value = trimmed.substring(eq + 1);
224
+ map[key] = value;
225
+ }
226
+ }
227
+ return map;
228
+ }
229
+
230
+ /**
231
+ * Merges new .env content with existing .env: newly resolved content wins for keys it
232
+ * defines (so project secrets take effect when re-running). Keys only in existing .env
233
+ * are appended so manual additions are kept.
234
+ *
235
+ * @param {string} newContent - Newly generated .env content (from template + loadSecrets)
236
+ * @param {Object.<string, string>} existingMap - Existing key-to-value map from current .env
237
+ * @returns {string} Merged content: new values for keys in newContent, plus extra existing keys
238
+ */
239
+ function mergeEnvContentPreservingExisting(newContent, existingMap) {
240
+ const lines = newContent.split(/\r?\n/);
241
+ const newKeys = new Set();
242
+ const out = [];
243
+ for (const line of lines) {
244
+ const trimmed = line.trim();
245
+ if (!trimmed || trimmed.startsWith('#')) {
246
+ out.push(line);
247
+ continue;
248
+ }
249
+ const eq = trimmed.indexOf('=');
250
+ if (eq > 0) {
251
+ const key = trimmed.substring(0, eq).trim();
252
+ newKeys.add(key);
253
+ }
254
+ out.push(line);
255
+ }
256
+ if (existingMap && Object.keys(existingMap).length > 0) {
257
+ for (const key of Object.keys(existingMap)) {
258
+ if (!newKeys.has(key)) {
259
+ out.push(`${key}=${existingMap[key]}`);
260
+ }
261
+ }
262
+ }
263
+ return out.join('\n');
264
+ }
265
+
266
+ /**
267
+ * Merges a key-value map into existing .env file content, preserving comments and blank lines.
268
+ * For each KEY=value line in existing content, replaces value with newMap[KEY] when the key exists
269
+ * in newMap. By default appends keys from newMap that did not appear in the file; set
270
+ * `appendMissingFromNewMap: false` to only update keys already present (e.g. `--reload` into a
271
+ * resolve-generated file so run-only keys like DB_0_NAME are not tacked on the end).
272
+ *
273
+ * @param {string} existingContent - Full existing .env file content
274
+ * @param {Object.<string, string>} newMap - New key-to-value map (e.g. from resolved or run env)
275
+ * @param {Object} [options] - Merge options
276
+ * @param {boolean} [options.appendMissingFromNewMap=true] - When false, do not append keys only in newMap
277
+ * @returns {string} Merged content with comments preserved
278
+ */
279
+ function collectSeenKeysAndMergeEnvLines(lines, newMap) {
280
+ const seen = new Set();
281
+ const out = [];
282
+ for (const line of lines) {
283
+ const trimmed = line.trim();
284
+ if (!trimmed || trimmed.startsWith('#')) {
285
+ out.push(line);
286
+ continue;
287
+ }
288
+ const eq = trimmed.indexOf('=');
289
+ if (eq > 0) {
290
+ const key = trimmed.substring(0, eq).trim();
291
+ seen.add(key);
292
+ out.push(Object.prototype.hasOwnProperty.call(newMap, key) ? `${key}=${newMap[key]}` : line);
293
+ continue;
294
+ }
295
+ out.push(line);
296
+ }
297
+ return { out, seen };
298
+ }
299
+
300
+ function appendMissingEnvKeysFromMap(out, seen, newMap) {
301
+ for (const key of Object.keys(newMap)) {
302
+ if (!seen.has(key)) {
303
+ out.push(`${key}=${newMap[key]}`);
304
+ }
305
+ }
306
+ }
307
+
308
+ function mergeEnvMapIntoContent(existingContent, newMap, options = {}) {
309
+ if (!newMap || Object.keys(newMap).length === 0) {
310
+ return typeof existingContent === 'string' ? existingContent : '';
311
+ }
312
+ const appendMissing = options.appendMissingFromNewMap !== false;
313
+ const lines = (existingContent || '').split(/\r?\n/);
314
+ const { out, seen } = collectSeenKeysAndMergeEnvLines(lines, newMap);
315
+ if (appendMissing) {
316
+ appendMissingEnvKeysFromMap(out, seen, newMap);
317
+ }
318
+ return out.join('\n');
319
+ }
320
+
321
+ /**
322
+ * Resolves content to write for .env: merges with existing file when present.
323
+ * @param {string} resolved - Newly generated content
324
+ * @param {string} pathToPreserve - Path to existing .env to merge from (or null)
325
+ * @returns {string} Content to write
326
+ */
327
+ function resolveEnvContentToWrite(resolved, pathToPreserve) {
328
+ if (!pathToPreserve || !fs.existsSync(pathToPreserve)) return resolved;
329
+ const existingContent = fs.readFileSync(pathToPreserve, 'utf8');
330
+ const existingMap = parseEnvContentToMap(existingContent);
331
+ return mergeEnvContentPreservingExisting(resolved, existingMap);
332
+ }
333
+
334
+ /**
335
+ * Generates and writes .env file. Newly resolved values win over existing .env; extra vars in existing .env are kept.
336
+ * When `options.envOnly` is true, only env.template is used; .env is written to `options.appPath`.
337
+ *
338
+ * When `options.noWrite` is true, the function resolves the .env content in memory and skips
339
+ * both writes — neither `<appPath>/.env` nor `build.envOutputPath` is materialized — and returns
340
+ * `null`. Use this from non-resolve flows (register/rotate-secret/build/up-*) so resolved secrets
341
+ * never land on disk except when the user runs `aifabrix resolve <app>` explicitly.
342
+ *
343
+ * @async
344
+ * @param {string} appName - Name of the application
345
+ * @param {string} [secretsPath] - Path to secrets file (optional)
346
+ * @param {string} [environment='local'] - Environment context ('local' or 'docker')
347
+ * @param {boolean} [force=false] - Generate missing secret keys in secrets file
348
+ * @param {Object} [options] - Optional: appPath, envOnly, skipOutputPath, preserveFromPath, noWrite,
349
+ * preferLocalEnvOutputPath (when **true**, `build.envOutputPath` is regenerated with **local** `url://` profile; **false**
350
+ * only when **both** `remote-server` is set and `applications.<app>.reload` is true — then docker flavor matches `builder/<app>/.env`)
351
+ * @param {boolean} [options.noWrite=false] - When true, resolve in-memory only; do not write
352
+ * `<appPath>/.env` and do not call `processEnvVariables`. Returns `null` in that case.
353
+ * @returns {Promise<string|null>} Path to generated .env file, or `null` when `noWrite` is true
354
+ *
355
+ * @example
356
+ * // up-platform / up-miso / up-dataplane / register / rotate-secret / build flows:
357
+ * await generateEnvFile('dataplane', null, 'local', false, { noWrite: true });
358
+ *
359
+ * @example
360
+ * // aifabrix resolve <app> — the only legitimate writer of a persistent .env:
361
+ * await generateEnvFile('dataplane', undefined, 'docker', force, {
362
+ * appPath, envOnly, skipOutputPath: false, preserveFromPath: null
363
+ * });
364
+ */
365
+ /**
366
+ * Materialize a resolved .env to `<appPath>/.env` and (optionally) copy through
367
+ * `build.envOutputPath`. Extracted so {@link generateEnvFile} can stay under the
368
+ * 20-statement limit while still expressing the in-memory vs on-disk branch clearly.
369
+ *
370
+ * @async
371
+ * @param {Object} params
372
+ * @param {string} params.appName
373
+ * @param {string} params.appPath
374
+ * @param {string} params.envPath - Resolved `<appPath>/.env` target
375
+ * @param {string} params.resolved - Fully resolved .env content
376
+ * @param {string|null} params.variablesPath - application.yaml path (or null when envOnly)
377
+ * @param {string} [params.secretsPath]
378
+ * @param {Object} params.opts - Caller options (preserveFromPath, skipOutputPath, preferLocalEnvOutputPath, appPath)
379
+ * @returns {Promise<string>} Path to the written .env file
380
+ */
381
+ async function writeResolvedEnv({ appName, envPath, resolved, variablesPath, secretsPath, opts }) {
382
+ const preservePath = opts.preserveFromPath !== undefined && opts.preserveFromPath !== null ? opts.preserveFromPath : null;
383
+ const pathToPreserve = preservePath !== null ? preservePath : envPath;
384
+ const toWrite = resolveEnvContentToWrite(resolved, pathToPreserve);
385
+ fs.writeFileSync(envPath, toWrite, { mode: 0o600 });
386
+
387
+ if (!opts.skipOutputPath) {
388
+ const { processEnvVariables } = require('../utils/env-copy');
389
+ await processEnvVariables(envPath, variablesPath, appName, secretsPath, {
390
+ preferLocalEnvOutputPath: opts.preferLocalEnvOutputPath === true,
391
+ appPath: opts.appPath || null
392
+ });
393
+ }
394
+ return envPath;
395
+ }
396
+
397
+ async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, options = {}) {
398
+ const opts = options && typeof options === 'object' ? options : {};
399
+ const appPath = opts.appPath || pathsUtil.getBuilderPath(appName);
400
+ const envOnly = !!opts.envOnly;
401
+ const noWrite = opts.noWrite === true;
402
+ const variablesPath = envOnly ? null : resolveApplicationConfigPath(appPath);
403
+ const envPath = path.join(appPath, '.env');
404
+
405
+ if (envOnly) {
406
+ const templatePath = path.join(appPath, 'env.template');
407
+ if (!fs.existsSync(templatePath)) {
408
+ throw new Error(`env.template not found at ${templatePath}. Resolve requires env.template in the app directory.`);
409
+ }
410
+ }
411
+
412
+ // Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
413
+ const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
414
+
415
+ if (noWrite) {
416
+ return null;
417
+ }
418
+
419
+ return writeResolvedEnv({ appName, appPath, envPath, resolved, variablesPath, secretsPath, opts });
420
+ }
421
+
422
+ module.exports = {
423
+ resolveKvReferences,
424
+ generateEnvContent,
425
+ generateEnvFile,
426
+ parseEnvContentToMap,
427
+ mergeEnvMapIntoContent
428
+ };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Declarative url:// expansion after kv:// (keeps secrets-env-content under max-lines).
3
+ *
4
+ * @fileoverview Declarative url:// expansion after kv://; shared ctx builder; show URL helper
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const config = require('./config');
12
+ const pathsUtil = require('../utils/paths');
13
+ const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
14
+ const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
15
+ const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
16
+ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
17
+ /**
18
+ * Config inputs for declarative url:// expansion.
19
+ * @param {string} appPath
20
+ * @returns {Promise<Object>}
21
+ */
22
+ async function loadDeclarativeUrlExpandInputs(appPath) {
23
+ const userCfg = await config.getConfig();
24
+ let remoteServer = null;
25
+ try {
26
+ const rs = await config.getRemoteServer();
27
+ if (rs && String(rs).trim()) {
28
+ remoteServer = String(rs).trim();
29
+ }
30
+ } catch {
31
+ remoteServer = null;
32
+ }
33
+ let developerIdRaw = null;
34
+ try {
35
+ developerIdRaw = await config.getDeveloperId();
36
+ } catch {
37
+ developerIdRaw = null;
38
+ }
39
+ let infraTlsEnabled = false;
40
+ try {
41
+ infraTlsEnabled = await config.getTlsEnabled();
42
+ } catch {
43
+ infraTlsEnabled = false;
44
+ }
45
+ return {
46
+ userCfg,
47
+ remoteServer,
48
+ developerIdRaw,
49
+ infraTlsEnabled,
50
+ appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Build ctx for {@link expandDeclarativeUrlsInEnvContent} from preloaded inputs.
56
+ * @param {string} appName
57
+ * @param {string|null|undefined} variablesPath
58
+ * @param {string} environment
59
+ * @param {Object} inputs - Result of {@link loadDeclarativeUrlExpandInputs}
60
+ * @returns {Object}
61
+ */
62
+ function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, inputs) {
63
+ const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
64
+ let projectRoot = null;
65
+ try {
66
+ const r = pathsUtil.getProjectRoot();
67
+ if (r && String(r).trim()) {
68
+ projectRoot = String(r).trim();
69
+ }
70
+ } catch {
71
+ projectRoot = null;
72
+ }
73
+ return {
74
+ profile: environment === 'docker' ? 'docker' : 'local',
75
+ currentAppKey: appName,
76
+ variablesPath,
77
+ useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
78
+ appEnvironmentScopedResources: appScopedFlag,
79
+ remoteServer,
80
+ developerIdRaw,
81
+ infraTlsEnabled,
82
+ userCfg,
83
+ projectRoot
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Resolve `url://public` and `url://internal` for the app (same rules as run `.env`, default `docker` profile).
89
+ * @param {string} appKey
90
+ * @param {string} appPath
91
+ * @param {string|null|undefined} variablesPath
92
+ * @param {string} [environment='docker'] - Matches `secrets-env-write` default so `url://internal` uses the same service host + vdir rules as the app `.env` copied for Docker run.
93
+ * @returns {Promise<{ publicUrl: string, internalUrl: string }|null>}
94
+ */
95
+ async function resolveDeclarativeShowUrlsForApp(appKey, appPath, variablesPath, environment = 'docker') {
96
+ if (!variablesPath || !appPath || !appKey) {
97
+ return null;
98
+ }
99
+ const inputs = await loadDeclarativeUrlExpandInputs(appPath);
100
+ const { parseSimpleEnvMap } = require('../utils/url-declarative-resolve');
101
+ let content = 'APP_SHOW_PUBLIC=url://public\nAPP_SHOW_INTERNAL=url://internal\n';
102
+ content = rewriteInactiveDeclarativeVdirPublicContent(content, variablesPath, inputs.userCfg);
103
+ if (!content.includes('url://')) {
104
+ return null;
105
+ }
106
+ const ctx = buildDeclarativeUrlExpandContextFromInputs(appKey, variablesPath, environment, inputs);
107
+ const out = await expandDeclarativeUrlsInEnvContent(content, ctx);
108
+ const m = parseSimpleEnvMap(out);
109
+ const publicUrl = m.APP_SHOW_PUBLIC;
110
+ const internalUrl = m.APP_SHOW_INTERNAL;
111
+ if (
112
+ !publicUrl ||
113
+ !internalUrl ||
114
+ String(publicUrl).includes('url://') ||
115
+ String(internalUrl).includes('url://')
116
+ ) {
117
+ return null;
118
+ }
119
+ return { publicUrl: String(publicUrl).trim(), internalUrl: String(internalUrl).trim() };
120
+ }
121
+
122
+ /**
123
+ * After kv:// resolution, expand url:// references when application config exists.
124
+ * Also refreshes {@link refreshUrlsLocalRegistryFromBuilder} whenever `variablesPath` is set so
125
+ * `urls.local.yaml` picks up `port` / `frontDoorRouting` changes even if `env.template` has no
126
+ * literal `url://` placeholders (they may already be expanded or absent).
127
+ * @param {string} resolved
128
+ * @param {string} appName
129
+ * @param {string} appPath
130
+ * @param {string|null} variablesPath
131
+ * @param {string} environment
132
+ * @param {boolean} envOnly
133
+ * @returns {Promise<string>}
134
+ */
135
+ async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variablesPath, environment, envOnly) {
136
+ if (envOnly || !variablesPath) {
137
+ return resolved;
138
+ }
139
+ const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
140
+ await loadDeclarativeUrlExpandInputs(appPath);
141
+ resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
142
+ const ctx = buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, {
143
+ userCfg,
144
+ remoteServer,
145
+ developerIdRaw,
146
+ infraTlsEnabled,
147
+ appScopedFlag
148
+ });
149
+ try {
150
+ const pr = ctx.projectRoot && String(ctx.projectRoot).trim() ? String(ctx.projectRoot).trim() : '';
151
+ const registryRoot = pr || pathsUtil.getProjectRoot();
152
+ if (ctx.excludeCwdBuilderScan === true) {
153
+ refreshUrlsLocalRegistryFromBuilder(registryRoot, { excludeCwdBuilderScan: true });
154
+ } else {
155
+ refreshUrlsLocalRegistryFromBuilder(registryRoot);
156
+ }
157
+ } catch {
158
+ // best-effort: registry refresh must not block .env generation
159
+ }
160
+ if (!resolved.includes('url://')) {
161
+ return resolved;
162
+ }
163
+ return expandDeclarativeUrlsInEnvContent(resolved, ctx);
164
+ }
165
+
166
+ module.exports = {
167
+ expandDeclarativeUrlsIfPresent,
168
+ loadDeclarativeUrlExpandInputs,
169
+ resolveDeclarativeShowUrlsForApp
170
+ };
@@ -100,6 +100,30 @@ async function injectRegistryTokens(content, secretsPath, appName) {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Append variables from `BASH_*` secret keys (merged secrets load) when not already set in content.
105
+ *
106
+ * @param {string} content - .env-style content
107
+ * @param {string|null} secretsPath - Optional secrets path
108
+ * @param {string|null} appName - Optional app name for loadSecrets
109
+ * @returns {Promise<string>}
110
+ */
111
+ async function injectBashPrefixedExportLines(content, secretsPath, appName) {
112
+ try {
113
+ const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
114
+ const bash = await getBashPrefixedProcessEnvOverlay(secretsPath, appName);
115
+ let out = content || '';
116
+ for (const [name, value] of Object.entries(bash)) {
117
+ if (!envContentHasKey(out, name)) {
118
+ out = appendEnvLine(out, name, value);
119
+ }
120
+ }
121
+ return out;
122
+ } catch {
123
+ return content || '';
124
+ }
125
+ }
126
+
103
127
  /**
104
128
  * Parse .env-style content into a key-value map (excludes comments and empty lines).
105
129
  * @param {string} content - .env-style content
@@ -123,7 +147,9 @@ function parseEnvContentToMap(content) {
123
147
 
124
148
  /**
125
149
  * Resolve .env in memory and write only to envOutputPath or temp (no builder/ or integration/).
126
- * Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing, then from process.env, so shell/install/test/build can use exported tokens.
150
+ * Injects NPM_TOKEN and PYPI_TOKEN from secrets when missing, then from process.env, then names derived from `BASH_*` keys in the merged secrets store, so shell/install/build match private registry tooling.
151
+ *
152
+ * **Ephemeral / tooling path:** Used by `app-install`, `app-shell`, and `app-test` to materialize a disposable `.env` (defaults to a temp file under `os.tmpdir()`). This is **not** the same as `aifabrix resolve <app>` (which uses `generateEnvFile` and preserves `build.envOutputPath` / comments). Prefer `aifabrix resolve` when you need a durable repo `.env` for local development.
127
153
  *
128
154
  * @async
129
155
  * @function resolveAndWriteEnvFile
@@ -145,6 +171,7 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
145
171
 
146
172
  let resolved = await secrets.generateEnvContent(appName, secretsPath, environment, force);
147
173
  resolved = await injectRegistryTokens(resolved, secretsPath, appName);
174
+ resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
148
175
 
149
176
  if (envOutputPath && typeof envOutputPath === 'string') {
150
177
  const dir = path.dirname(envOutputPath);
@@ -179,6 +206,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
179
206
  const force = options.force === true;
180
207
  let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
181
208
  content = await injectRegistryTokens(content, secretsPath, appName);
209
+ content = await injectBashPrefixedExportLines(content, secretsPath, appName);
182
210
  return parseEnvContentToMap(content);
183
211
  }
184
212