@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
@@ -8,6 +8,23 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ /**
12
+ * Treat undefined/null and whitespace-only strings as no value so prefixed slots
13
+ * cannot shadow unprefixed secrets with empty placeholders (plan 117 scoped KV).
14
+ *
15
+ * @param {*} v
16
+ * @returns {boolean}
17
+ */
18
+ function isKvSlotEmpty(v) {
19
+ if (v === undefined || v === null) {
20
+ return true;
21
+ }
22
+ if (typeof v === 'string' && v.trim() === '') {
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
11
28
  /**
12
29
  * @param {Function} getValueByPath - From secrets-helpers
13
30
  * @returns {{ getValueByPathWithEnvScope: Function, mergeSecretsWithPrefixedCopies: Function }}
@@ -27,7 +44,7 @@ function createScopedKvHelpers(getValueByPath) {
27
44
  const prefix = `${String(envKey).toLowerCase()}-`;
28
45
  const prefixedPath = prefix + pathStr;
29
46
  const fromPrefixed = getValueByPath(secrets, prefixedPath);
30
- if (fromPrefixed !== undefined && fromPrefixed !== null) {
47
+ if (!isKvSlotEmpty(fromPrefixed)) {
31
48
  return fromPrefixed;
32
49
  }
33
50
  return getValueByPath(secrets, pathStr);
@@ -47,7 +64,7 @@ function createScopedKvHelpers(getValueByPath) {
47
64
  for (const [k, v] of Object.entries(secrets)) {
48
65
  if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
49
66
  const pk = prefix + k;
50
- if (merged[pk] === undefined && v !== undefined && v !== null) {
67
+ if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
51
68
  merged[pk] = v;
52
69
  }
53
70
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Persist resolved kv:// values into the primary user secrets file so regenerate (.env) stays stable.
3
+ *
4
+ * @fileoverview After env.template resolves, copy merged secret values into ~/.aifabrix/secrets.local.yaml
5
+ * when keys are missing or empty locally **and** not already provided by the configured shared store
6
+ * (`aifabrix-secrets` remote API or shared YAML), so shared-only keys are not duplicated into user secrets.
7
+ * @author AI Fabrix Team
8
+ * @version 1.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const logger = require('./logger');
14
+ const { formatSuccessLine } = require('./cli-test-layout-chalk');
15
+ const localSecretsModule = require('./local-secrets');
16
+ const secretsUtils = require('./secrets-utils');
17
+ const { loadConfiguredSharedSecretsStore } = require('./remote-secrets-loader');
18
+ const { decryptSecretsObject } = require('../core/secrets-load');
19
+ const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
20
+ const {
21
+ resolveKvRefValue,
22
+ mergeSecretsWithPrefixedCopies,
23
+ isKvKeyAllowedEmptyWhenAbsent
24
+ } = require('./secrets-helpers');
25
+
26
+ /**
27
+ * After successful kv resolution, write resolved values to the user secrets file when local slot is empty.
28
+ * Does not overwrite non-empty local values (user wins).
29
+ *
30
+ * @async
31
+ * @param {string} templateContent - Original env.template text (still contains kv:// refs)
32
+ * @param {Object} mergedSecrets - Decrypted secrets map passed to resolveKvReferences (same as loadSecrets output)
33
+ * @param {{ effective?: boolean, envKey?: string }|null} scopedKv - Scoped kv context from generateEnvContent
34
+ * @param {Object} [options]
35
+ * @param {boolean} [options.skipMaterializeKvToLocal] - Skip persistence (tests / programmatic)
36
+ * @returns {Promise<string[]>} Keys materialized (written) to local file
37
+ */
38
+ function buildScopedMaps(mergedSecrets, scopedKv) {
39
+ const effective = Boolean(scopedKv && scopedKv.effective && scopedKv.envKey);
40
+ const secretsMap = effective ? mergeSecretsWithPrefixedCopies(mergedSecrets, scopedKv.envKey) : mergedSecrets;
41
+ const rawLocal = secretsUtils.loadPrimaryUserSecrets();
42
+ const localMap = effective ? mergeSecretsWithPrefixedCopies(rawLocal, scopedKv.envKey) : rawLocal;
43
+ return { effective, secretsMap, localMap };
44
+ }
45
+
46
+ function shouldSkipPersist(pathStr, resolvedVal, localVal) {
47
+ if (resolvedVal === undefined || resolvedVal === null) return true;
48
+ if (
49
+ typeof resolvedVal === 'string' &&
50
+ resolvedVal.trim() === '' &&
51
+ isKvKeyAllowedEmptyWhenAbsent(pathStr)
52
+ ) {
53
+ return true;
54
+ }
55
+ if (localVal !== undefined && localVal !== null && String(localVal).trim() !== '') {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function isNonEmptyResolvedKv(val) {
62
+ if (val === undefined || val === null) return false;
63
+ if (typeof val === 'string' && val.trim() === '') return false;
64
+ return true;
65
+ }
66
+
67
+ async function resolveSharedScopedMapForMaterialize(scopedKv, effective) {
68
+ try {
69
+ const rawShared = await loadConfiguredSharedSecretsStore();
70
+ const decryptedShared = rawShared ? await decryptSecretsObject(rawShared) : null;
71
+ if (!decryptedShared) return null;
72
+ return effective && scopedKv && scopedKv.envKey
73
+ ? mergeSecretsWithPrefixedCopies(decryptedShared, scopedKv.envKey)
74
+ : decryptedShared;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function plaintextKvForLocalPersist(pathStr, secretsMap, localMap, sharedScoped, scopedKv, effective) {
81
+ const resolvedVal = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
82
+ const localVal = resolveKvRefValue(localMap, pathStr, scopedKv?.envKey, effective);
83
+ if (shouldSkipPersist(pathStr, resolvedVal, localVal)) return null;
84
+ const sharedVal =
85
+ sharedScoped && typeof sharedScoped === 'object'
86
+ ? resolveKvRefValue(sharedScoped, pathStr, scopedKv?.envKey, effective)
87
+ : undefined;
88
+ if (isNonEmptyResolvedKv(sharedVal)) return null;
89
+ return typeof resolvedVal === 'string' ? resolvedVal : String(resolvedVal);
90
+ }
91
+
92
+ async function materializeResolvedKvSecretsToUserLocal(templateContent, mergedSecrets, scopedKv, options = {}) {
93
+ if (
94
+ !templateContent ||
95
+ typeof templateContent !== 'string' ||
96
+ options.skipMaterializeKvToLocal === true
97
+ ) {
98
+ return [];
99
+ }
100
+
101
+ try {
102
+ const { effective, secretsMap, localMap } = buildScopedMaps(mergedSecrets, scopedKv);
103
+ const sharedScoped = await resolveSharedScopedMapForMaterialize(scopedKv, effective);
104
+
105
+ const paths = collectUniqueKvPathStrings(templateContent);
106
+ const materialized = [];
107
+
108
+ for (const pathStr of paths) {
109
+ const plain = plaintextKvForLocalPersist(pathStr, secretsMap, localMap, sharedScoped, scopedKv, effective);
110
+ if (plain === null) continue;
111
+ await localSecretsModule.saveLocalSecret(pathStr, plain);
112
+ materialized.push(pathStr);
113
+ }
114
+
115
+ if (materialized.length > 0) {
116
+ logger.log(
117
+ formatSuccessLine(
118
+ `Saved ${materialized.length} resolved kv key(s) to local secrets for stable reinstalls: ${materialized.join(', ')}`
119
+ )
120
+ );
121
+ }
122
+
123
+ return materialized;
124
+ } catch (err) {
125
+ if (typeof logger.warn === 'function') {
126
+ logger.warn(`Could not materialize resolved kv secrets to local file: ${err.message}`);
127
+ }
128
+ return [];
129
+ }
130
+ }
131
+
132
+ module.exports = {
133
+ materializeResolvedKvSecretsToUserLocal
134
+ };
@@ -13,6 +13,15 @@ const path = require('path');
13
13
  const config = require('../core/config');
14
14
  const paths = require('./paths');
15
15
 
16
+ /**
17
+ * @param {string} s
18
+ * @returns {boolean}
19
+ */
20
+ function isHttpOrHttpsUrl(s) {
21
+ const t = String(s || '').trim();
22
+ return t.startsWith('http://') || t.startsWith('https://');
23
+ }
24
+
16
25
  /**
17
26
  * Resolves secrets file path when an explicit path is provided.
18
27
  * If not provided, returns default fallback under <home>/secrets.yaml.
@@ -32,9 +41,8 @@ function resolveSecretsPath(secretsPath) {
32
41
  }
33
42
 
34
43
  /**
35
- * Determines the actual secrets file paths that loadSecrets would use
36
- * Mirrors the cascading lookup logic from loadSecrets
37
- * Uses config.yaml for default secrets path as fallback
44
+ * Determines paths used for default `loadSecrets()` (no explicit path): primary user secrets file
45
+ * and configured `aifabrix-secrets` (shared YAML file path or remote API URL).
38
46
  *
39
47
  * @async
40
48
  * @function getActualSecretsPath
@@ -42,7 +50,8 @@ function resolveSecretsPath(secretsPath) {
42
50
  * @param {string} [_appName] - Application name (optional, for backward compatibility, unused)
43
51
  * @returns {Promise<Object>} Object with userPath and buildPath (if configured)
44
52
  * @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
45
- * @returns {string|null} returns.buildPath - App's secrets file path (if configured in config.yaml)
53
+ * @returns {string|null} returns.buildPath - On-disk shared secrets file (if configured; never an http(s) URL)
54
+ * @returns {string|null} [returns.sharedSecretsApiUrl] - When aifabrix-secrets is an API URL (for error messages; loadSecrets merges this API into the kv:// resolution map)
46
55
  */
47
56
  async function getActualSecretsPath(secretsPath, _appName) {
48
57
  // If explicit path provided, use it (backward compatibility)
@@ -50,30 +59,34 @@ async function getActualSecretsPath(secretsPath, _appName) {
50
59
  const resolvedPath = resolveSecretsPath(secretsPath);
51
60
  return {
52
61
  userPath: resolvedPath,
53
- buildPath: null
62
+ buildPath: null,
63
+ sharedSecretsApiUrl: null
54
64
  };
55
65
  }
56
66
 
57
- // Cascading lookup: user's file first (primary home: AIFABRIX_HOME or ~/.aifabrix)
58
- const userSecretsPath = path.join(paths.getConfigDirForPaths(), 'secrets.local.yaml');
67
+ // Default lookup: primary user file plus aifabrix-secrets (file or https URL)
68
+ const userSecretsPath = paths.getPrimaryUserSecretsLocalPath();
59
69
 
60
- // Check config.yaml for canonical secrets path
61
70
  let buildSecretsPath = null;
71
+ let sharedSecretsApiUrl = null;
62
72
  try {
63
73
  const canonicalSecretsPath = await config.getAifabrixSecretsPath();
64
74
  if (canonicalSecretsPath) {
65
- buildSecretsPath = path.isAbsolute(canonicalSecretsPath)
66
- ? canonicalSecretsPath
67
- : path.resolve(process.cwd(), canonicalSecretsPath);
75
+ const raw = String(canonicalSecretsPath).trim();
76
+ if (isHttpOrHttpsUrl(raw)) {
77
+ sharedSecretsApiUrl = raw.replace(/\/+$/, '');
78
+ } else {
79
+ buildSecretsPath = path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
80
+ }
68
81
  }
69
82
  } catch (error) {
70
83
  // Ignore errors, continue
71
84
  }
72
85
 
73
- // Return both paths (even if files don't exist) for error messages
74
86
  return {
75
87
  userPath: userSecretsPath,
76
- buildPath: buildSecretsPath
88
+ buildPath: buildSecretsPath,
89
+ sharedSecretsApiUrl
77
90
  };
78
91
  }
79
92
 
@@ -58,18 +58,14 @@ async function loadSecretsFromFile(filePath) {
58
58
  }
59
59
 
60
60
  /**
61
- * Loads user secrets from getPrimaryUserSecretsLocalPath() (same dir as config.yaml resolution).
62
- * Used as the master source when merging with project/public secrets: user values win,
63
- * missing keys are filled from the public (aifabrix-secrets) file.
61
+ * Loads user secrets from {@link pathsUtil.getPrimaryUserSecretsLocalPath} (beside `config.yaml`).
62
+ * If that file is missing, reads a legacy file at `getAifabrixHome()/secrets.local.yaml` when paths
63
+ * differ (older CLI when `aifabrix-home` was POSIX home).
64
64
  *
65
65
  * @function loadPrimaryUserSecrets
66
66
  * @returns {Object} Loaded secrets object or empty object
67
67
  */
68
- function loadPrimaryUserSecrets() {
69
- const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
70
- if (!fs.existsSync(userSecretsPath)) {
71
- return {};
72
- }
68
+ function readPrimaryUserSecretsAtPath(userSecretsPath) {
73
69
  ensureSecureFilePermissions(userSecretsPath);
74
70
 
75
71
  try {
@@ -84,8 +80,22 @@ function loadPrimaryUserSecrets() {
84
80
  throw error;
85
81
  }
86
82
  logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
87
- return {};
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function loadPrimaryUserSecrets() {
88
+ const primaryPath = pathsUtil.getPrimaryUserSecretsLocalPath();
89
+ if (fs.existsSync(primaryPath)) {
90
+ const fromPrimary = readPrimaryUserSecretsAtPath(primaryPath);
91
+ return fromPrimary !== null ? fromPrimary : {};
92
+ }
93
+ const legacyPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
94
+ if (path.resolve(legacyPath) !== path.resolve(primaryPath) && fs.existsSync(legacyPath)) {
95
+ const fromLegacy = readPrimaryUserSecretsAtPath(legacyPath);
96
+ return fromLegacy !== null ? fromLegacy : {};
88
97
  }
98
+ return {};
89
99
  }
90
100
 
91
101
  /**
@@ -128,7 +138,7 @@ function loadDefaultSecrets() {
128
138
 
129
139
  /**
130
140
  * Creates the primary user secrets file if missing (empty map) for first-run installs.
131
- * Uses the same directory as {@link loadPrimaryUserSecrets} (config dir / ~/.aifabrix).
141
+ * Uses the same directory as {@link loadPrimaryUserSecrets} (beside `config.yaml`).
132
142
  *
133
143
  * @function ensurePrimaryUserSecretsFileExists
134
144
  */
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Picks the parent directory for platform `builder/<app>` materialization when the project has no
3
+ * `builder/<app>`. Compares the effective config directory (`getAifabrixSystemDir`) to the resolved
4
+ * AI Fabrix home (`getAifabrixHome`, from `AIFABRIX_HOME` or `aifabrix-home` in config.yaml).
5
+ *
6
+ * @fileoverview System builder root parent (config dir vs home override)
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+
15
+ /**
16
+ * When the effective config directory lies under the resolved AI Fabrix home, keep state (including
17
+ * `builder/`) beside `config.yaml` (e.g. `$HOME/.aifabrix/builder` when `AIFABRIX_HOME=$HOME`).
18
+ * When `aifabrix-home` relocates home outside the config tree, materialize under that home instead.
19
+ *
20
+ * Uses a resolved-path prefix check (not `path.relative`) so "nested under home" is stable across
21
+ * platforms (e.g. sibling paths like `/var/aifabrix` vs `/var/aifabrix-config` must not be treated
22
+ * as nested).
23
+ *
24
+ * @param {string} systemDir - Absolute config/state directory (same as `getAifabrixSystemDir()`).
25
+ * @param {string} homeDir - Absolute AI Fabrix home (`getAifabrixHome()`).
26
+ * @returns {string} Absolute directory whose `builder/` subdir is used
27
+ */
28
+ function resolveSystemBuilderParentDir(systemDir, homeDir) {
29
+ const s = path.resolve(systemDir);
30
+ const h = path.resolve(homeDir);
31
+ if (s === h) {
32
+ return s;
33
+ }
34
+ const sep = path.sep;
35
+ const homePrefix = h.endsWith(sep) ? h : `${h}${sep}`;
36
+ const configUnderHome = s.startsWith(homePrefix);
37
+ return configUnderHome ? s : h;
38
+ }
39
+
40
+ module.exports = {
41
+ resolveSystemBuilderParentDir
42
+ };
@@ -11,6 +11,26 @@
11
11
  const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
12
12
  const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
13
13
 
14
+ /**
15
+ * Public URL authorities that must use `http://` (browsers hit loopback without TLS in practice).
16
+ *
17
+ * @param {string} hostname - Parsed URL hostname
18
+ * @returns {boolean}
19
+ */
20
+ function isLocalhostPublicSchemeHostname(hostname) {
21
+ const h = String(hostname || '').trim().toLowerCase();
22
+ if (!h) {
23
+ return false;
24
+ }
25
+ if (h === 'localhost') {
26
+ return true;
27
+ }
28
+ if (h === '127.0.0.1') {
29
+ return true;
30
+ }
31
+ return h === '::1';
32
+ }
33
+
14
34
  /**
15
35
  * Expand frontDoorRouting.host placeholders for url:// (same rules as Traefik labels in compose-generator).
16
36
  *
@@ -31,6 +51,17 @@ function hostPortForProfile(profile, listenPort, developerIdNum) {
31
51
  : localHostPort(listenPort, developerIdNum);
32
52
  }
33
53
 
54
+ /**
55
+ * Scheme for `declarativePublicUrlsUseLocalhost` bases: always **http** for loopback reachability
56
+ * (ignore `tlsEnabled` / `remote-server` https — localhost URLs are not served with TLS in dev).
57
+ * @param {Object} opts - Kept for call-site compatibility (same shape as {@link computePublicUrlBaseString})
58
+ * @returns {'http'}
59
+ */
60
+ function schemeForDeclarativeLocalhostPublicBase(opts) {
61
+ void opts;
62
+ return 'http';
63
+ }
64
+
34
65
  /**
35
66
  * Local profile: workstation `+10` applies only to the app being resolved (`currentAppKey`).
36
67
  * Cross-app tokens (e.g. `url://keycloak-public` from miso-controller) use `publishedHostPort`
@@ -62,10 +93,10 @@ function resolveHostPortForDeclarativePublic(opts) {
62
93
  }
63
94
 
64
95
  /**
65
- * Without Traefik, `remote-server` is the dev machine host. Apps bind published host ports, not 443 + path.
66
- * If the URL already has an explicit port, keep host:port but **scheme follows `infraTlsEnabled`**, not the
67
- * literal `https://` in `remote-server` when TLS is off (`up-infra` without `--tls`).
68
- * If `remote-server` omits a port, append the profile-specific published/listen-derived port.
96
+ * Public authority from `remote-server` when expansion opts in (Traefik on, or per-app `proxy: true`
97
+ * `declarativePublicUrlsUseLocalhost === false`). If the URL already has an explicit port, keep host:port but
98
+ * **scheme follows `infraTlsEnabled`**, not the literal `https://` in `remote-server` when TLS is off
99
+ * (`up-infra` without `--tls`). If `remote-server` omits a port, append the profile-specific published/listen-derived port.
69
100
  *
70
101
  * @param {string} rawRemote
71
102
  * @param {'docker'|'local'} profile
@@ -85,7 +116,10 @@ function remotePublicBaseWithoutTraefik(
85
116
  const raw = String(rawRemote || '').trim().replace(/\/+$/, '');
86
117
  const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : (infraTlsEnabled ? `https://${raw}` : `http://${raw}`);
87
118
  const u = new URL(withScheme);
88
- const scheme = infraTlsEnabled ? 'https' : 'http';
119
+ let scheme = infraTlsEnabled ? 'https' : 'http';
120
+ if (isLocalhostPublicSchemeHostname(u.hostname)) {
121
+ scheme = 'http';
122
+ }
89
123
 
90
124
  if (u.port !== '') {
91
125
  return `${scheme}://${u.host}`;
@@ -105,17 +139,31 @@ function remotePublicBaseWithoutTraefik(
105
139
  return `${scheme}://${u.hostname}:${hostPort}`;
106
140
  }
107
141
 
142
+ /**
143
+ * True when `frontDoorRouting.host` depends on `remote-server` for expansion (`${REMOTE_HOST}`).
144
+ * Without a remote, expansion yields a bare `devNN` label (no DNS) — public `url://` bases must fall back to localhost.
145
+ *
146
+ * @param {string|null|undefined} hostTemplate
147
+ * @returns {boolean}
148
+ */
149
+ function templateUsesRemoteHostPlaceholder(hostTemplate) {
150
+ return /\$\{REMOTE_HOST\}/.test(String(hostTemplate || ''));
151
+ }
152
+
108
153
  /**
109
154
  * @param {Object} opts - same shape as computePublicUrlBaseString
110
155
  * @returns {string|null}
111
156
  */
112
157
  function buildTraefikPublicBaseIfApplicable(opts) {
113
- const { traefik, pathActive, hostTemplate, tls, developerIdRaw, remoteServer, infraTlsEnabled } =
158
+ const { traefik, pathActive, hostTemplate, developerIdRaw, remoteServer, infraTlsEnabled } =
114
159
  opts;
115
160
  // Plan 124: Traefik host authority only when pathActive (traefik ∧ frontDoorRouting.enabled)
116
161
  if (!traefik || !pathActive || !hostTemplate || !String(hostTemplate).trim()) {
117
162
  return null;
118
163
  }
164
+ if (templateUsesRemoteHostPlaceholder(hostTemplate) && !String(remoteServer || '').trim()) {
165
+ return null;
166
+ }
119
167
  const expanded = expandFrontDoorHostTemplateForUrls(hostTemplate, {
120
168
  developerIdRaw,
121
169
  remoteServer
@@ -123,8 +171,12 @@ function buildTraefikPublicBaseIfApplicable(opts) {
123
171
  if (!expanded) {
124
172
  return null;
125
173
  }
126
- const useHttps = Boolean(infraTlsEnabled) || tls !== false;
127
- const scheme = useHttps ? 'https' : 'http';
174
+ // Scheme follows global `tlsEnabled` (`up-infra --tls`) only — not `frontDoorRouting.tls` when infra TLS is off.
175
+ const useHttps = Boolean(infraTlsEnabled);
176
+ let scheme = useHttps ? 'https' : 'http';
177
+ if (isLocalhostPublicSchemeHostname(expanded)) {
178
+ scheme = 'http';
179
+ }
128
180
  return `${scheme}://${expanded}`.replace(/\/+$/, '');
129
181
  }
130
182
 
@@ -140,8 +192,9 @@ function buildTraefikPublicBaseIfApplicable(opts) {
140
192
  * @param {'docker'|'local'} opts.profile
141
193
  * @param {number} opts.listenPort
142
194
  * @param {number} opts.developerIdNum
143
- * @param {boolean} [opts.infraTlsEnabled] - `tlsEnabled` from ~/.aifabrix/config.yaml (`up-infra --tls`); when true, Traefik front-door public URLs use https even if application.yaml has `frontDoorRouting.tls: false`
195
+ * @param {boolean} [opts.infraTlsEnabled] - `tlsEnabled` from ~/.aifabrix/config.yaml (`up-infra --tls`); Traefik front-door public bases use **`https`** only when this is **true** (even if `frontDoorRouting.tls` is **false**). When **false**, front-door bases use **`http`** regardless of `frontDoorRouting.tls`. Loopback hostnames stay **http** always.
144
196
  * @param {boolean} [opts.pathActive] - traefik ∧ frontDoorRouting.enabled; required for Traefik host branch (plan 124)
197
+ * @param {boolean|undefined} [opts.declarativePublicUrlsUseLocalhost] - **`true`**: force localhost authority (+ port rules below). **`false`**: allow `remote-server` as authority when Traefik is on, or when global `traefik` is not `false` and per-app `proxy` opts in without Traefik. **`undefined`**: default — omit `remote-server` when Traefik is off so published services use `http://localhost:<port>` (e.g. `af setup` / device login URLs). When user config has **`traefik: false`**, expansion forces localhost for public bases regardless of per-app `proxy`.
145
198
  * @returns {string}
146
199
  */
147
200
  function computePublicUrlBaseString(opts) {
@@ -159,7 +212,23 @@ function computePublicUrlBaseString(opts) {
159
212
  return traefikBase;
160
213
  }
161
214
 
162
- if (remoteServer && String(remoteServer).trim()) {
215
+ if (opts.declarativePublicUrlsUseLocalhost) {
216
+ const hostPort = resolveHostPortForDeclarativePublic({
217
+ profile,
218
+ listenPort,
219
+ developerIdNum,
220
+ declarativeTargetAppKey: declarativePortOpts.declarativeTargetAppKey,
221
+ declarativeCurrentAppKey: declarativePortOpts.declarativeCurrentAppKey
222
+ });
223
+ const scheme = schemeForDeclarativeLocalhostPublicBase(opts);
224
+ return `${scheme}://localhost:${hostPort}`;
225
+ }
226
+
227
+ const remoteTrimmed = remoteServer && String(remoteServer).trim();
228
+ const useRemotePublicAuthority =
229
+ Boolean(remoteTrimmed) &&
230
+ (Boolean(opts.traefik) || opts.declarativePublicUrlsUseLocalhost === false);
231
+ if (useRemotePublicAuthority) {
163
232
  return remotePublicBaseWithoutTraefik(
164
233
  remoteServer,
165
234
  profile,
@@ -177,8 +246,7 @@ function computePublicUrlBaseString(opts) {
177
246
  declarativeTargetAppKey: declarativePortOpts.declarativeTargetAppKey,
178
247
  declarativeCurrentAppKey: declarativePortOpts.declarativeCurrentAppKey
179
248
  });
180
- const scheme = infraTlsEnabled ? 'https' : 'http';
181
- return `${scheme}://localhost:${hostPort}`;
249
+ return `http://localhost:${hostPort}`;
182
250
  }
183
251
 
184
252
  module.exports = {