@aifabrix/builder 2.44.4 → 2.44.6

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 (214) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +68 -17
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  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 +44 -12
  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 +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -19,6 +19,7 @@ const { updateContainerPortInEnvFile } = require('./env-ports');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { getLocalPortFromPath } = require('./port-resolver');
21
21
  const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
22
+ const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
22
23
 
23
24
  /**
24
25
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -51,9 +52,13 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
51
52
  /**
52
53
  * Last-resort when infra.parameter.yaml cannot be read (e.g. Jest suites that mock `fs`;
53
54
  * catalog uses `node:fs`, which is often the same mocked instance). Must stay in sync with
54
- * `generator.type: emptyAllowed` keys in lib/schema/infra.parameter.yaml.
55
+ * `generator.type: emptyAllowed` keys in lib/schema/infra.parameter.yaml; plus optional Azure/OpenAI kv names.
55
56
  */
56
- const EMPTY_ALLOWED_KV_FALLBACK = new Set(['redis-passwordKeyVault']);
57
+ const EMPTY_ALLOWED_KV_FALLBACK = new Set([
58
+ 'redis-passwordKeyVault',
59
+ 'azure-openaiapi-urlKeyVault',
60
+ 'secrets-azureOpenaiApiKeyVault'
61
+ ]);
57
62
 
58
63
  /**
59
64
  * Infra catalog keys with generator `emptyAllowed` may be absent from the secrets file;
@@ -191,7 +196,12 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
191
196
  if (secretsFilePaths.buildPath) {
192
197
  paths.push(secretsFilePaths.buildPath);
193
198
  }
194
- return `\n\nSecrets file location: ${paths.join(' and ')}`;
199
+ let msg = `\n\nSecrets file location: ${paths.join(' and ')}`;
200
+ if (secretsFilePaths.sharedSecretsApiUrl && typeof secretsFilePaths.sharedSecretsApiUrl === 'string') {
201
+ msg +=
202
+ `\n(Shared secrets API: ${secretsFilePaths.sharedSecretsApiUrl.trim()}. Keys from that API are merged when resolving secrets; if a key is still missing, add it via "aifabrix secret set" / shared store or check "aifabrix secret list --shared".)`;
203
+ }
204
+ return msg;
195
205
  }
196
206
  return '';
197
207
  }
@@ -459,13 +469,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
459
469
  return updated;
460
470
  }
461
471
 
462
- /**
463
- * Validate secrets against the env template (skips commented and empty lines)
464
- * @function validateSecrets
465
- * @param {string} envTemplate - Environment template content
466
- * @param {Object} secrets - Available secrets
467
- * @returns {Object} Validation result
468
- */
472
+ /** Validate secrets vs env template (commented/empty lines skipped). */
469
473
  function validateSecrets(envTemplate, secrets) {
470
474
  const missing = collectMissingSecrets(envTemplate, secrets);
471
475
  return { valid: missing.length === 0, missing };
@@ -475,6 +479,9 @@ module.exports = {
475
479
  loadEnvConfig,
476
480
  interpolateEnvVars,
477
481
  collectMissingSecrets,
482
+ collectUniqueKvPathStrings,
483
+ resolveKvRefValue,
484
+ isKvKeyAllowedEmptyWhenAbsent,
478
485
  resolveBashKvFromProcessEnv,
479
486
  mergeSecretsWithPrefixedCopies,
480
487
  formatMissingSecretsFileInfo,
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Scan env-style content for unique kv:// path segments (comments skipped).
3
+ * @fileoverview Keeps secrets-helpers under max-lines
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
9
+
10
+ function isCommentOrEmptyLine(line) {
11
+ const t = line.trim();
12
+ return t === '' || t.startsWith('#');
13
+ }
14
+
15
+ /**
16
+ * @param {string} content
17
+ * @returns {string[]}
18
+ */
19
+ function collectUniqueKvPathStrings(content) {
20
+ const seen = new Set();
21
+ const out = [];
22
+ if (!content || typeof content !== 'string') {
23
+ return out;
24
+ }
25
+ for (const line of content.split('\n')) {
26
+ if (isCommentOrEmptyLine(line)) continue;
27
+ let match;
28
+ KV_REF_PATTERN.lastIndex = 0;
29
+ while ((match = KV_REF_PATTERN.exec(line)) !== null) {
30
+ const pathStr = match[1];
31
+ if (!seen.has(pathStr)) {
32
+ seen.add(pathStr);
33
+ out.push(pathStr);
34
+ }
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ module.exports = {
41
+ collectUniqueKvPathStrings
42
+ };
@@ -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.
@@ -42,7 +51,8 @@ function resolveSecretsPath(secretsPath) {
42
51
  * @param {string} [_appName] - Application name (optional, for backward compatibility, unused)
43
52
  * @returns {Promise<Object>} Object with userPath and buildPath (if configured)
44
53
  * @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)
54
+ * @returns {string|null} returns.buildPath - On-disk shared secrets file (if configured; never an http(s) URL)
55
+ * @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
56
  */
47
57
  async function getActualSecretsPath(secretsPath, _appName) {
48
58
  // If explicit path provided, use it (backward compatibility)
@@ -50,30 +60,34 @@ async function getActualSecretsPath(secretsPath, _appName) {
50
60
  const resolvedPath = resolveSecretsPath(secretsPath);
51
61
  return {
52
62
  userPath: resolvedPath,
53
- buildPath: null
63
+ buildPath: null,
64
+ sharedSecretsApiUrl: null
54
65
  };
55
66
  }
56
67
 
57
- // Cascading lookup: user's file first (primary home: AIFABRIX_HOME or ~/.aifabrix)
58
- const userSecretsPath = path.join(paths.getConfigDirForPaths(), 'secrets.local.yaml');
68
+ // Cascading lookup: user's file first (same path as `secret set`: getPrimaryUserSecretsLocalPath)
69
+ const userSecretsPath = paths.getPrimaryUserSecretsLocalPath();
59
70
 
60
- // Check config.yaml for canonical secrets path
61
71
  let buildSecretsPath = null;
72
+ let sharedSecretsApiUrl = null;
62
73
  try {
63
74
  const canonicalSecretsPath = await config.getAifabrixSecretsPath();
64
75
  if (canonicalSecretsPath) {
65
- buildSecretsPath = path.isAbsolute(canonicalSecretsPath)
66
- ? canonicalSecretsPath
67
- : path.resolve(process.cwd(), canonicalSecretsPath);
76
+ const raw = String(canonicalSecretsPath).trim();
77
+ if (isHttpOrHttpsUrl(raw)) {
78
+ sharedSecretsApiUrl = raw.replace(/\/+$/, '');
79
+ } else {
80
+ buildSecretsPath = path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
81
+ }
68
82
  }
69
83
  } catch (error) {
70
84
  // Ignore errors, continue
71
85
  }
72
86
 
73
- // Return both paths (even if files don't exist) for error messages
74
87
  return {
75
88
  userPath: userSecretsPath,
76
- buildPath: buildSecretsPath
89
+ buildPath: buildSecretsPath,
90
+ sharedSecretsApiUrl
77
91
  };
78
92
  }
79
93
 
@@ -58,7 +58,7 @@ async function loadSecretsFromFile(filePath) {
58
58
  }
59
59
 
60
60
  /**
61
- * Loads user secrets from getPrimaryUserSecretsLocalPath() (same dir as config.yaml resolution).
61
+ * Loads user secrets from getPrimaryUserSecretsLocalPath() (under {@link module:lib/utils/paths.getAifabrixHome}).
62
62
  * Used as the master source when merging with project/public secrets: user values win,
63
63
  * missing keys are filled from the public (aifabrix-secrets) file.
64
64
  *
@@ -128,7 +128,7 @@ function loadDefaultSecrets() {
128
128
 
129
129
  /**
130
130
  * 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).
131
+ * Uses the same directory as {@link loadPrimaryUserSecrets} (resolved AI Fabrix home).
132
132
  *
133
133
  * @function ensurePrimaryUserSecretsFileExists
134
134
  */
@@ -0,0 +1,34 @@
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
+ * @param {string} systemDir - Absolute config/state directory (same as `getAifabrixSystemDir()`).
21
+ * @param {string} homeDir - Absolute AI Fabrix home (`getAifabrixHome()`).
22
+ * @returns {string} Absolute directory whose `builder/` subdir is used
23
+ */
24
+ function resolveSystemBuilderParentDir(systemDir, homeDir) {
25
+ const s = path.resolve(systemDir);
26
+ const h = path.resolve(homeDir);
27
+ const rel = path.relative(h, s);
28
+ const configUnderHome = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
29
+ return configUnderHome ? s : h;
30
+ }
31
+
32
+ module.exports = {
33
+ resolveSystemBuilderParentDir
34
+ };
@@ -19,6 +19,7 @@ const {
19
19
  const { computePathActive } = require('./url-declarative-url-flags');
20
20
  const { loadApplicationYamlDocForUrlResolve } = require('./url-declarative-resolve-load-doc');
21
21
  const { parseUrlToken } = require('./url-declarative-token-parse');
22
+ const runtimeBasePath = require('./url-declarative-runtime-base-path');
22
23
 
23
24
  /**
24
25
  * Plan 122: **Developer subdomain `devNN` + remote hostname is not derived from envKey `tst`.** It comes only from
@@ -209,6 +210,7 @@ function buildInternalUrlString(opts) {
209
210
  profile,
210
211
  listenPort,
211
212
  targetAppKey,
213
+ runtimeBasePath,
212
214
  remoteServer,
213
215
  pathPrefix,
214
216
  patternPath,
@@ -223,7 +225,8 @@ function buildInternalUrlString(opts) {
223
225
  declarativeCurrentAppKey
224
226
  } = opts;
225
227
  if (profile === 'docker') {
226
- return `http://${targetAppKey}:${listenPort}`;
228
+ const origin = `http://${targetAppKey}:${listenPort}`;
229
+ return runtimeBasePath ? joinUrlPath(origin, runtimeBasePath) : origin;
227
230
  }
228
231
  if (remoteServer && String(remoteServer).trim()) {
229
232
  return buildPublicUrlString({
@@ -432,10 +435,12 @@ function expandFullSurfaceInternal(r) {
432
435
  if (isLocalProfileWithoutRemoteServer(r)) {
433
436
  return expandFullSurfacePublic(r);
434
437
  }
438
+ const runtimePath = runtimeBasePath.normalizeRuntimeBasePath(r.patternPath, r);
435
439
  return buildInternalUrlString({
436
440
  profile: r.profile,
437
441
  listenPort: r.listenPort,
438
442
  targetAppKey: r.appKey,
443
+ runtimeBasePath: runtimePath,
439
444
  remoteServer: r.remoteServer,
440
445
  pathPrefix: r.pathPrefix,
441
446
  patternPath: r.patternPath,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Runtime base path helpers for declarative url:// resolution.
3
+ *
4
+ * @fileoverview Keeps path-aware internal URLs out of the main resolver file.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Runtime base path is derived from the existing frontDoorRouting path only
13
+ * when that path is active for the target app.
14
+ * @param {string|null|undefined} patternPath
15
+ * @param {Object} r
16
+ * @returns {string}
17
+ */
18
+ function normalizeRuntimeBasePath(patternPath, r) {
19
+ if (!r.frontDoorIngressActive) {
20
+ return '';
21
+ }
22
+ const value = String(patternPath || '').trim();
23
+ if (!value || value === '/') {
24
+ return '';
25
+ }
26
+ const pathValue = value.startsWith('/') ? value : `/${value}`;
27
+ return pathValue.replace(/\/{2,}/g, '/').replace(/\/+$/, '');
28
+ }
29
+
30
+ module.exports = {
31
+ normalizeRuntimeBasePath
32
+ };
@@ -95,5 +95,6 @@ module.exports = {
95
95
  URL_DECLARATIVE_VDIR_PUBLIC_TOKEN,
96
96
  INACTIVE_VDIR_PUBLIC_ENV_FALLBACK,
97
97
  applyInactiveVdirPublicTokenRewrite,
98
- rewriteInactiveDeclarativeVdirPublicContent
98
+ rewriteInactiveDeclarativeVdirPublicContent,
99
+ isFrontDoorRoutingEnabledInDoc
99
100
  };
@@ -1,7 +1,6 @@
1
1
  /**
2
- * urls.local.yaml beside effective config.yaml (same directory as secrets.local.yaml).
3
- * When AIFABRIX_HOME is POSIX $HOME but config lives in $HOME/.aifabrix/, the registry
4
- * is $HOME/.aifabrix/urls.local.yaml (not $HOME/urls.local.yaml).
2
+ * Primary `urls.local.yaml` under {@link module:lib/utils/paths.getAifabrixHome} (same as
3
+ * `secrets.local.yaml`). When that file is missing, falls back to the config directory for migration.
5
4
  *
6
5
  * @fileoverview Read/write registry; scan each builder app folder for application.yaml
7
6
  * @author AI Fabrix Team
@@ -17,15 +16,15 @@ const { DECLARATIVE_URL_INFRA_DEFAULTS } = require('./infra-env-defaults');
17
16
  const pathsUtil = require('./paths');
18
17
 
19
18
  /**
20
- * @returns {string} Absolute path to urls.local.yaml (primary; beside config.yaml)
19
+ * @returns {string} Absolute path to urls.local.yaml (primary; under resolved AI Fabrix home)
21
20
  */
22
21
  function getUrlsLocalYamlPath() {
23
- return path.join(pathsUtil.getConfigDirForPaths(), 'urls.local.yaml');
22
+ return path.join(pathsUtil.getAifabrixHome(), 'urls.local.yaml');
24
23
  }
25
24
 
26
- /** @returns {string} Legacy path when registry was stored under getAifabrixHome() only */
27
- function getLegacyUrlsLocalYamlPath() {
28
- return path.join(pathsUtil.getAifabrixHome(), 'urls.local.yaml');
25
+ /** @returns {string} Path beside config.yaml (migration / older layouts) */
26
+ function getConfigDirUrlsLocalYamlPath() {
27
+ return path.join(pathsUtil.getConfigDirForPaths(), 'urls.local.yaml');
29
28
  }
30
29
 
31
30
  function loadRegistryYamlFile(filePath) {
@@ -45,9 +44,9 @@ function readUrlsLocalRegistrySync() {
45
44
  if (fsRealSync.existsSync(primary)) {
46
45
  return loadRegistryYamlFile(primary);
47
46
  }
48
- const legacy = getLegacyUrlsLocalYamlPath();
49
- if (legacy !== primary && fsRealSync.existsSync(legacy)) {
50
- return loadRegistryYamlFile(legacy);
47
+ const atConfig = getConfigDirUrlsLocalYamlPath();
48
+ if (path.resolve(atConfig) !== path.resolve(primary) && fsRealSync.existsSync(atConfig)) {
49
+ return loadRegistryYamlFile(atConfig);
51
50
  }
52
51
  return {};
53
52
  }
@@ -175,6 +174,54 @@ function mergeBuilderDirIntoRegistry(merged, builderDir) {
175
174
  }
176
175
  }
177
176
 
177
+ /**
178
+ * True when getBuilderRoot() resolves to the same path as AIFABRIX_BUILDER_DIR (authoritative override).
179
+ * @param {string|null} resolvedEffective
180
+ * @param {string|null} envResolved
181
+ * @returns {boolean}
182
+ */
183
+ function effectiveBuilderMatchesEnvVar(resolvedEffective, envResolved) {
184
+ return Boolean(envResolved && resolvedEffective && resolvedEffective === envResolved);
185
+ }
186
+
187
+ /**
188
+ * Builder dirs to scan in order; later merges overwrite registry keys from earlier dirs.
189
+ * When `AIFABRIX_BUILDER_DIR` is set and differs from projectRoot/builder, env builder root is merged last.
190
+ * Otherwise projectRoot/builder is merged last so explicit refresh roots override getBuilderRoot().
191
+ *
192
+ * @param {string} root - Resolved project root passed to refresh
193
+ * @param {string|null} effectiveBuilderDir - pathsUtil.getBuilderRoot()
194
+ * @returns {string[]}
195
+ */
196
+ function getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir) {
197
+ const legacyBuilderDir = path.join(root, 'builder');
198
+ let resolvedLegacy;
199
+ let resolvedEffective;
200
+ try {
201
+ resolvedLegacy = path.resolve(legacyBuilderDir);
202
+ resolvedEffective = effectiveBuilderDir ? path.resolve(effectiveBuilderDir) : null;
203
+ } catch {
204
+ return [legacyBuilderDir];
205
+ }
206
+ const envRaw = process.env.AIFABRIX_BUILDER_DIR && String(process.env.AIFABRIX_BUILDER_DIR).trim();
207
+ const envResolved = envRaw ? path.resolve(envRaw) : null;
208
+ // Only treat env as authoritative when getBuilderRoot() is actually that path. Otherwise a stray
209
+ // AIFABRIX_BUILDER_DIR on CI (or Jest mocking getBuilderRoot to a temp dir) must not force
210
+ // [legacy, effective] — that order lets the real checkout builder overwrite the mocked root last.
211
+ const effectiveMatchesEnvVar = effectiveBuilderMatchesEnvVar(resolvedEffective, envResolved);
212
+
213
+ if (effectiveBuilderDir && resolvedEffective && resolvedEffective === resolvedLegacy) {
214
+ return [legacyBuilderDir];
215
+ }
216
+ if (effectiveMatchesEnvVar && effectiveBuilderDir && resolvedEffective && resolvedEffective !== resolvedLegacy) {
217
+ return [legacyBuilderDir, effectiveBuilderDir];
218
+ }
219
+ if (effectiveBuilderDir && resolvedEffective && resolvedEffective !== resolvedLegacy) {
220
+ return [effectiveBuilderDir, legacyBuilderDir];
221
+ }
222
+ return [legacyBuilderDir];
223
+ }
224
+
178
225
  /**
179
226
  * Merge scan results into registry (does not remove stale keys).
180
227
  * @param {string|null} projectRoot - getProjectRoot() or null (same semantics as projectRoot || getProjectRoot())
@@ -188,18 +235,24 @@ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
188
235
  }
189
236
  // Published npm tarball omits builder/ under the package root (.npmignore). Global installs must
190
237
  // still refresh from the real builder tree (AIFABRIX_BUILDER_DIR or integration base + builder).
191
- const legacyBuilderDir = path.join(root, 'builder');
192
- const effectiveBuilderDir = pathsUtil.getBuilderRoot();
193
- const builderDirs = [legacyBuilderDir];
238
+ let effectiveBuilderDir = null;
239
+ try {
240
+ effectiveBuilderDir = pathsUtil.getBuilderRoot();
241
+ } catch {
242
+ effectiveBuilderDir = null;
243
+ }
244
+ let builderDirs = getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir);
194
245
  try {
195
- if (
196
- effectiveBuilderDir &&
197
- path.resolve(effectiveBuilderDir) !== path.resolve(legacyBuilderDir)
198
- ) {
199
- builderDirs.push(effectiveBuilderDir);
246
+ const sysRoot = pathsUtil.getSystemBuilderRoot();
247
+ const resolvedSys = path.resolve(sysRoot);
248
+ if (fsRealSync.existsSync(sysRoot)) {
249
+ const resolvedList = builderDirs.map((d) => path.resolve(d));
250
+ if (!resolvedList.includes(resolvedSys)) {
251
+ builderDirs = [sysRoot, ...builderDirs];
252
+ }
200
253
  }
201
254
  } catch {
202
- /* ignore path resolution errors */
255
+ // ignore
203
256
  }
204
257
  for (const builderDir of builderDirs) {
205
258
  mergeBuilderDirIntoRegistry(merged, builderDir);