@aifabrix/builder 2.44.5 → 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 (207) 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 +48 -2
  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/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. 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
  }
@@ -242,7 +241,19 @@ function refreshUrlsLocalRegistryFromBuilder(projectRoot) {
242
241
  } catch {
243
242
  effectiveBuilderDir = null;
244
243
  }
245
- const builderDirs = getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir);
244
+ let builderDirs = getOrderedBuilderDirsForRegistryScan(root, effectiveBuilderDir);
245
+ try {
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
+ }
253
+ }
254
+ } catch {
255
+ // ignore
256
+ }
246
257
  for (const builderDir of builderDirs) {
247
258
  mergeBuilderDirIntoRegistry(merged, builderDir);
248
259
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * TTY ora spinner + throttled non-TTY lines for unified validation run polling (aligned with deploy-poll-ui).
3
+ *
4
+ * @fileoverview Validation run poll presentation helpers
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ function shouldUseValidationPollSpinner() {
15
+ return Boolean(process && process.stdout && process.stdout.isTTY);
16
+ }
17
+
18
+ /**
19
+ * Single-line ora text / log line for validation polling.
20
+ *
21
+ * @param {Object|null|undefined} envelope - Latest GET envelope (optional before first poll tick)
22
+ * @param {number} attemptIndex - Zero-based poll index after initial POST
23
+ * @param {number} deadlineMs - Wall-clock deadline (Date.now() + remaining budget)
24
+ * @returns {string}
25
+ */
26
+ function buildValidationPollSpinnerText(envelope, attemptIndex, deadlineMs) {
27
+ const remainingSec = Math.max(0, Math.ceil((deadlineMs - Date.now()) / 1000));
28
+ if (!envelope || typeof envelope !== 'object') {
29
+ return `Waiting for validation run… starting (budget ~${remainingSec}s)`;
30
+ }
31
+ const c =
32
+ envelope.reportCompleteness !== undefined && envelope.reportCompleteness !== null
33
+ ? String(envelope.reportCompleteness)
34
+ : '?';
35
+ const st =
36
+ envelope.status !== undefined && envelope.status !== null ? String(envelope.status) : '?';
37
+ return `Waiting for validation run… completeness=${c} status=${st} (poll ${attemptIndex + 1} · ~${remainingSec}s left)`;
38
+ }
39
+
40
+ const NON_TTY_THROTTLE_MS = 5000;
41
+
42
+ /**
43
+ * @param {number} deadlineMs - Absolute deadline for poll budget display
44
+ * @returns {{ usesSpinner: boolean, onPollProgress: Function, finish: () => void }}
45
+ */
46
+ function createValidationPollHandlers(deadlineMs) {
47
+ if (shouldUseValidationPollSpinner()) {
48
+ const ora = require('ora');
49
+ const spinner = ora({
50
+ text: buildValidationPollSpinnerText(null, 0, deadlineMs),
51
+ spinner: 'dots'
52
+ }).start();
53
+ return {
54
+ usesSpinner: true,
55
+ onPollProgress: (envelope, attemptIndex, _meta) => {
56
+ spinner.text = buildValidationPollSpinnerText(envelope, attemptIndex, deadlineMs);
57
+ },
58
+ finish: () => {
59
+ spinner.stop();
60
+ }
61
+ };
62
+ }
63
+
64
+ let lastNonTtyLogAt = 0;
65
+ return {
66
+ usesSpinner: false,
67
+ onPollProgress: (envelope, attemptIndex, _meta) => {
68
+ const now = Date.now();
69
+ if (now - lastNonTtyLogAt < NON_TTY_THROTTLE_MS) return;
70
+ lastNonTtyLogAt = now;
71
+ logger.log(chalk.gray(buildValidationPollSpinnerText(envelope, attemptIndex, deadlineMs)));
72
+ },
73
+ finish: () => {}
74
+ };
75
+ }
76
+
77
+ module.exports = {
78
+ createValidationPollHandlers,
79
+ shouldUseValidationPollSpinner,
80
+ buildValidationPollSpinnerText
81
+ };