@aifabrix/builder 2.60.0 → 2.62.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 (177) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/api/applications.api.js +22 -2
  14. package/lib/api/bootstrap-snapshot.api.js +115 -0
  15. package/lib/api/configuration.api.js +131 -0
  16. package/lib/api/system-secrets.api.js +72 -0
  17. package/lib/app/deploy.js +14 -5
  18. package/lib/app/run-docker-fallback.js +6 -1
  19. package/lib/app/run-env-compose.js +5 -6
  20. package/lib/app/run-helpers.js +2 -1
  21. package/lib/app/run-parameter-sync.js +142 -0
  22. package/lib/app/run.js +6 -4
  23. package/lib/build/docker-build-args.js +5 -2
  24. package/lib/build/index.js +3 -2
  25. package/lib/build/standard-docker-build.js +6 -2
  26. package/lib/channels/channel-artifact-generator.js +13 -3
  27. package/lib/cli/infra-guided-footers.js +10 -8
  28. package/lib/cli/infra-guided.js +30 -10
  29. package/lib/cli/setup-app.js +5 -0
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  32. package/lib/cli/setup-platform.js +2 -0
  33. package/lib/commands/auth-config.js +2 -0
  34. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  35. package/lib/commands/datasource-capability.js +4 -2
  36. package/lib/commands/env-secret-context.js +113 -0
  37. package/lib/commands/env-secret-list.js +200 -0
  38. package/lib/commands/env-secret-push-confirm.js +54 -0
  39. package/lib/commands/env-secret-push-run.js +227 -0
  40. package/lib/commands/env-secret-push.js +168 -0
  41. package/lib/commands/login.js +4 -0
  42. package/lib/commands/repair-datasource-apply.js +5 -1
  43. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  44. package/lib/commands/repair-datasource-legacy.js +94 -0
  45. package/lib/commands/repair-datasource-run.js +1 -0
  46. package/lib/commands/setup-image-refresh.js +26 -0
  47. package/lib/commands/setup-modes.js +8 -9
  48. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  49. package/lib/commands/setup-prompts.js +2 -180
  50. package/lib/commands/setup.js +3 -0
  51. package/lib/commands/up-builder-api.js +24 -8
  52. package/lib/commands/up-common.js +16 -1
  53. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  54. package/lib/commands/up-dataplane-credentials.js +21 -0
  55. package/lib/commands/up-dataplane.js +37 -18
  56. package/lib/commands/up-integration-server.js +112 -0
  57. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  58. package/lib/commands/verify-operations-steps.js +13 -1
  59. package/lib/commands/wizard-config-normalizer.js +12 -5
  60. package/lib/commands/wizard-core.js +5 -157
  61. package/lib/commands/wizard-file-saving.js +163 -0
  62. package/lib/core/admin-secrets-env-overlay.js +8 -3
  63. package/lib/core/env-platform-expand.js +5 -1
  64. package/lib/core/environment-access-policy.js +121 -0
  65. package/lib/core/environment-mode-policy.js +177 -0
  66. package/lib/core/local-env-overrides.js +149 -0
  67. package/lib/core/secrets-ensure.js +4 -2
  68. package/lib/core/secrets-env-content.js +17 -10
  69. package/lib/core/secrets-env-write.js +11 -4
  70. package/lib/core/secrets-load.js +4 -2
  71. package/lib/datasource/binary-documents-validator.js +190 -0
  72. package/lib/datasource/capability/capability-manifest.js +1 -0
  73. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  74. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  75. package/lib/datasource/capability/upsert-operations.js +138 -0
  76. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  77. package/lib/datasource/validate.js +12 -5
  78. package/lib/deployment/installation/index.js +14 -1
  79. package/lib/deployment/installation/infra-catalog.js +3 -0
  80. package/lib/deployment/installation/local-environment-stage.js +285 -0
  81. package/lib/deployment/installation/local-installation.js +76 -0
  82. package/lib/deployment/installation/stage-input.js +5 -1
  83. package/lib/generator/builders.js +3 -1
  84. package/lib/generator/index.js +3 -0
  85. package/lib/generator/split-variables.js +2 -2
  86. package/lib/lifecycle/product-model.js +4 -3
  87. package/lib/lifecycle/report-display.js +3 -2
  88. package/lib/parameters/database-adoption.js +125 -0
  89. package/lib/parameters/infra-parameter-catalog.js +1 -1
  90. package/lib/parameters/physical-database-naming.js +136 -0
  91. package/lib/parameters/physical-secret-name.js +194 -0
  92. package/lib/parameters/rsa-secret-values.js +165 -0
  93. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  94. package/lib/programmatic/builder-help-governance.js +1 -1
  95. package/lib/programmatic/builder-help.js +1 -1
  96. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  97. package/lib/programmatic/run-channel-package.js +3 -1
  98. package/lib/programmatic/workspace-context.js +37 -2
  99. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  100. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  101. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  102. package/lib/role-assistant/test-runner-workhub.js +28 -19
  103. package/lib/schema/application-schema.json +14 -2
  104. package/lib/schema/external-datasource.schema.json +23 -3
  105. package/lib/schema/infra.parameter.yaml +440 -56
  106. package/lib/schema/infrastructure-schema.json +213 -120
  107. package/lib/utils/compose-generate-docker-compose.js +9 -14
  108. package/lib/utils/compose-generator.js +4 -7
  109. package/lib/utils/compose-miso-env.js +2 -2
  110. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  111. package/lib/utils/datasource-binary-evidence.js +92 -0
  112. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  113. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  114. package/lib/utils/datasource-test-run-display.js +8 -2
  115. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  116. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  117. package/lib/utils/declarative-url-ports.js +3 -0
  118. package/lib/utils/env-copy.js +11 -10
  119. package/lib/utils/env-environment-file-paths.js +105 -0
  120. package/lib/utils/environment-scoped-resources.js +48 -14
  121. package/lib/utils/external-system-system-test-tty.js +3 -2
  122. package/lib/utils/image-tags.js +2 -2
  123. package/lib/utils/paths-system-builder-keys.js +7 -1
  124. package/lib/utils/paths.js +7 -2
  125. package/lib/utils/platform-kv-ref.js +1 -1
  126. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  127. package/lib/utils/prepare-local-data-mount.js +58 -0
  128. package/lib/utils/redis-env-scope.js +21 -1
  129. package/lib/utils/registry-auth-sources.js +154 -0
  130. package/lib/utils/registry-credentials.js +131 -14
  131. package/lib/utils/secrets-generator.js +30 -21
  132. package/lib/utils/secrets-helpers.js +5 -8
  133. package/lib/utils/secrets-kv-scope.js +127 -28
  134. package/lib/utils/secrets-materialize-local.js +9 -0
  135. package/lib/utils/secrets-missing-error.js +35 -1
  136. package/lib/utils/system-secret-mapping.js +125 -0
  137. package/lib/utils/test-log-writer.js +2 -1
  138. package/lib/utils/token-manager.js +14 -8
  139. package/lib/utils/url-public-path-prefix.js +6 -10
  140. package/lib/validation/external-manifest-validator.js +5 -0
  141. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  142. package/lib/validation/validate-external-file.js +5 -1
  143. package/package.json +6 -4
  144. package/templates/agent-kit/agent-kit.yaml +1 -1
  145. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  146. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  147. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  148. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  149. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  150. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  151. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  152. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  153. package/templates/agent-kit/skills/shared/status.md +3 -12
  154. package/templates/applications/builder-api/application.yaml +1 -1
  155. package/templates/applications/builder-api/env.template +5 -1
  156. package/templates/applications/dataplane/application.yaml +2 -2
  157. package/templates/applications/dataplane/env.template +10 -12
  158. package/templates/applications/integration-server/application.yaml +91 -0
  159. package/templates/applications/integration-server/env.template +215 -0
  160. package/templates/applications/keycloak/application.yaml +6 -1
  161. package/templates/applications/mirrored-templates.json +52 -0
  162. package/templates/applications/miso-controller/application.yaml +116 -2
  163. package/templates/applications/miso-controller/env.template +62 -17
  164. package/templates/applications/miso-controller/rbac.yaml +17 -0
  165. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  166. package/templates/marketplace/createUiDefinition.json +52 -4
  167. package/templates/marketplace/main.json +88 -10
  168. package/templates/python/docker-compose.hbs +1 -1
  169. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  170. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  171. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  172. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  173. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  174. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  175. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  176. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  177. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Per-environment env file names (plan 210.0).
3
+ *
4
+ * One host runs several environments, so the env file an environment is given has to be its
5
+ * own. The suffix order is **environment before profile**:
6
+ *
7
+ * ```text
8
+ * .env .env.local dev
9
+ * .env.tst .env.tst.local tst
10
+ * .env.pro .env.pro.local pro
11
+ * ```
12
+ *
13
+ * Two properties make that ordering the right one. `dev` keeps the names it has, so nothing
14
+ * existing is renamed and no tooling that looks for `.env` stops finding it. And `.local`
15
+ * stays the last segment, so the rule "a `.local` file is host-only and is never mounted
16
+ * into a container" is still readable from the name alone — `.env.local.tst` would have
17
+ * quietly broken that, since the profile would no longer be where every existing check looks
18
+ * for it.
19
+ *
20
+ * Pure: no filesystem access. The environment comes from the caller.
21
+ *
22
+ * @fileoverview Environment-suffixed env and env.local file paths
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const path = require('path');
28
+ const { isPromotionOnlyEnvironment } = require('../core/environment-mode-policy');
29
+
30
+ /** The host-only profile suffix, which must stay last. */
31
+ const LOCAL_PROFILE_SUFFIX = '.local';
32
+
33
+ /**
34
+ * The environment segment for an env file name.
35
+ *
36
+ * Empty for `dev` and for an absent environment, which is what keeps `.env` meaning what it
37
+ * has always meant.
38
+ *
39
+ * @param {string} [envKey] - Environment key
40
+ * @returns {string} `''`, `.tst` or `.pro`
41
+ */
42
+ function environmentEnvFileSegment(envKey) {
43
+ return isPromotionOnlyEnvironment(envKey) ? `.${String(envKey).trim().toLowerCase()}` : '';
44
+ }
45
+
46
+ /**
47
+ * The env file an environment is given.
48
+ *
49
+ * @param {string} envOutputPath - Resolved path ending in `.env`
50
+ * @param {string} [envKey] - Environment key
51
+ * @returns {string} Path ending in `.env`, `.env.tst` or `.env.pro`
52
+ * @throws {Error} When no base path is given
53
+ */
54
+ function resolveEnvOutputPathForEnvironment(envOutputPath, envKey) {
55
+ const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
56
+ if (!base) {
57
+ throw new Error('envOutputPath is required to resolve an environment env file');
58
+ }
59
+ const segment = environmentEnvFileSegment(envKey);
60
+ if (!segment || base.endsWith(segment)) {
61
+ return base;
62
+ }
63
+ return `${base}${segment}`;
64
+ }
65
+
66
+ /**
67
+ * The host-only sibling of an environment's env file.
68
+ *
69
+ * Built from the environment path so the profile suffix is always last, whichever
70
+ * environment it belongs to.
71
+ *
72
+ * @param {string} envOutputPath - Resolved path ending in `.env`
73
+ * @param {string} [envKey] - Environment key
74
+ * @returns {string} Path ending in `.local`
75
+ * @throws {Error} When no base path is given
76
+ */
77
+ function resolveEnvLocalOutputPathForEnvironment(envOutputPath, envKey) {
78
+ const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
79
+ if (!base) {
80
+ throw new Error('envOutputPath is required to resolve an environment .local env file');
81
+ }
82
+ const withoutProfile = base.endsWith(LOCAL_PROFILE_SUFFIX)
83
+ ? base.slice(0, -LOCAL_PROFILE_SUFFIX.length)
84
+ : base;
85
+ const forEnvironment = resolveEnvOutputPathForEnvironment(withoutProfile, envKey);
86
+ return `${forEnvironment}${LOCAL_PROFILE_SUFFIX}`;
87
+ }
88
+
89
+ /**
90
+ * Whether a file name is the host-only profile, for any environment.
91
+ *
92
+ * @param {string} filePath - Path or file name
93
+ * @returns {boolean}
94
+ */
95
+ function isHostOnlyEnvFile(filePath) {
96
+ return path.basename(String(filePath ?? '')).endsWith(LOCAL_PROFILE_SUFFIX);
97
+ }
98
+
99
+ module.exports = {
100
+ LOCAL_PROFILE_SUFFIX,
101
+ environmentEnvFileSegment,
102
+ resolveEnvOutputPathForEnvironment,
103
+ resolveEnvLocalOutputPathForEnvironment,
104
+ isHostOnlyEnvFile
105
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Environment-scoped resource naming (shared infra: dev/tst on same Postgres/Redis/Docker).
3
- * Effective only when user gate AND app flag AND run env is dev|tst.
3
+ * Effective only when user gate AND app flag AND run env is dev|tst|pro.
4
4
  *
5
5
  * @fileoverview environmentScopedResources + useEnvironmentScopedResources helpers
6
6
  * @author AI Fabrix Team
@@ -10,14 +10,44 @@
10
10
  'use strict';
11
11
 
12
12
  /**
13
- * Run/env keys that participate in resource prefixing (not pro).
13
+ * Run/env keys that participate in resource prefixing.
14
+ *
15
+ * `pro` is included (plan 210.0). It was excluded while `tst` was the only promotion target,
16
+ * which left `/pro/data` ungenerated and every pro resource sharing dev's name, Redis index
17
+ * and port. On a shared developer host that is a collision, not an absence.
18
+ *
19
+ * Callers must use this predicate rather than comparing keys inline: the same `dev || tst`
20
+ * test was written out in five other modules, and every one of them had to be found to add
21
+ * a single environment.
22
+ */
23
+ const SCOPED_RUN_ENVIRONMENTS = Object.freeze(['dev', 'tst', 'pro']);
24
+
25
+ /** Redis logical DB per environment on one shared instance. Distinct by construction. */
26
+ const REDIS_DB_INDEX_BY_ENV = Object.freeze({ dev: 0, tst: 1, pro: 2 });
27
+
28
+ /**
14
29
  * @param {string} envKey - Normalized lowercase env key
15
30
  * @returns {boolean}
16
31
  */
17
32
  function isScopedRunEnvironmentKey(envKey) {
18
33
  if (!envKey || typeof envKey !== 'string') return false;
19
- const k = envKey.toLowerCase();
20
- return k === 'dev' || k === 'tst';
34
+ return SCOPED_RUN_ENVIRONMENTS.includes(envKey.toLowerCase());
35
+ }
36
+
37
+ /**
38
+ * Whether a scope context carries both the gate and a scopable environment.
39
+ *
40
+ * The five call sites that need this each combined `effectiveEnvironmentScopedResources`
41
+ * with their own copy of the environment test; this is that combination, once.
42
+ *
43
+ * @param {{ effectiveEnvironmentScopedResources?: boolean, runEnvKey?: string }|null} scopeOpts
44
+ * @returns {boolean}
45
+ */
46
+ function isScopeActiveForOptions(scopeOpts) {
47
+ if (!scopeOpts || !scopeOpts.effectiveEnvironmentScopedResources) {
48
+ return false;
49
+ }
50
+ return isScopedRunEnvironmentKey(scopeOpts.runEnvKey);
21
51
  }
22
52
 
23
53
  /**
@@ -42,17 +72,18 @@ function computeEffectiveEnvironmentScopedResources(
42
72
 
43
73
  /**
44
74
  * Redis logical DB index when env-scoped resources are effective.
45
- * dev → 0, tst → 1 (single shared Redis instance).
75
+ * dev → 0, tst → 1, pro → 2 (single shared Redis instance).
76
+ *
77
+ * Returning null for a scoped environment would put it on DB 0 alongside dev, so this must
78
+ * cover every key in {@link SCOPED_RUN_ENVIRONMENTS}.
46
79
  *
47
- * @param {string} runEnvKey - dev | tst
80
+ * @param {string} runEnvKey - dev | tst | pro
48
81
  * @returns {number|null} Index or null if not applicable
49
82
  */
50
83
  function redisDbIndexForScopedRunEnv(runEnvKey) {
51
84
  if (!runEnvKey || typeof runEnvKey !== 'string') return null;
52
- const k = runEnvKey.toLowerCase();
53
- if (k === 'dev') return 0;
54
- if (k === 'tst') return 1;
55
- return null;
85
+ const index = REDIS_DB_INDEX_BY_ENV[runEnvKey.toLowerCase()];
86
+ return index === undefined ? null : index;
56
87
  }
57
88
 
58
89
  /**
@@ -61,7 +92,7 @@ function redisDbIndexForScopedRunEnv(runEnvKey) {
61
92
  * @param {string} appName - Application key
62
93
  * @param {string|number} devId - Developer id
63
94
  * @param {number} idNum - Parsed numeric developer id
64
- * @param {string} envKey - dev | tst
95
+ * @param {string} envKey - dev | tst | pro
65
96
  * @returns {string}
66
97
  */
67
98
  function buildScopedLocalContainerName(appName, devId, idNum, envKey) {
@@ -93,7 +124,7 @@ function buildDefaultLocalContainerName(appName, devId, idNum) {
93
124
  * @param {string} appName - Application key
94
125
  * @param {string|number} developerId - Developer id
95
126
  * @param {boolean} effectiveScoped - From {@link computeEffectiveEnvironmentScopedResources}
96
- * @param {string} [runEnvKey] - dev | tst (required when effectiveScoped)
127
+ * @param {string} [runEnvKey] - dev | tst | pro (required when effectiveScoped)
97
128
  * @returns {string}
98
129
  */
99
130
  function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKey) {
@@ -108,7 +139,7 @@ function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKe
108
139
  * Traefik PathPrefix: prefix /{envKey} before pattern base (e.g. /api → /dev/api).
109
140
  *
110
141
  * @param {string} basePath - From derivePathFromPattern
111
- * @param {string} envKey - dev | tst
142
+ * @param {string} envKey - dev | tst | pro
112
143
  * @returns {string}
113
144
  */
114
145
  function buildEnvScopedTraefikPath(basePath, envKey) {
@@ -125,7 +156,7 @@ function buildEnvScopedTraefikPath(basePath, envKey) {
125
156
  * Compose service + Traefik router key when env-scoped (unique per env on shared host).
126
157
  *
127
158
  * @param {string} appName - Application key
128
- * @param {string} envKey - dev | tst
159
+ * @param {string} envKey - dev | tst | pro
129
160
  * @returns {string}
130
161
  */
131
162
  function composeTraefikServiceKey(appName, envKey) {
@@ -133,7 +164,10 @@ function composeTraefikServiceKey(appName, envKey) {
133
164
  }
134
165
 
135
166
  module.exports = {
167
+ SCOPED_RUN_ENVIRONMENTS,
168
+ REDIS_DB_INDEX_BY_ENV,
136
169
  isScopedRunEnvironmentKey,
170
+ isScopeActiveForOptions,
137
171
  computeEffectiveEnvironmentScopedResources,
138
172
  redisDbIndexForScopedRunEnv,
139
173
  buildScopedLocalContainerName,
@@ -182,7 +182,9 @@ function pickBlockingDatasourceKey(rows) {
182
182
  }
183
183
 
184
184
  function issueKey(issue) {
185
- const code = issue && issue.code ? String(issue.code) : '';
185
+ const code = issue && (issue.stableCode || issue.code)
186
+ ? String(issue.stableCode || issue.code)
187
+ : '';
186
188
  const msg = issue && issue.message ? String(issue.message) : '';
187
189
  return `${code}::${msg}`.toLowerCase();
188
190
  }
@@ -402,4 +404,3 @@ module.exports = {
402
404
  systemCertStatus,
403
405
  drillDownCommand
404
406
  };
405
-
@@ -48,7 +48,7 @@ function expandTag(entry, version) {
48
48
  * @throws {Error} On blank entries, unsupported placeholders, missing version or invalid tags
49
49
  */
50
50
  function resolveImageTags(tagInput, appVersion) {
51
- if (tagInput === undefined || tagInput === null || String(tagInput).trim() === '') {
51
+ if (tagInput === undefined || tagInput === null) {
52
52
  return [DEFAULT_TAG];
53
53
  }
54
54
  const entries = String(tagInput).split(',').map((entry) => entry.trim());
@@ -72,7 +72,7 @@ function resolveImageTags(tagInput, appVersion) {
72
72
  */
73
73
  function resolveBuildTagPlan(tagInput, appVersion) {
74
74
  const tags = resolveImageTags(tagInput, appVersion);
75
- const explicitSingle = tags.length === 1 && tagInput !== undefined && tagInput !== null && String(tagInput).trim() !== '';
75
+ const explicitSingle = tagInput !== undefined && tagInput !== null && !String(tagInput).includes(',');
76
76
  if (explicitSingle && !FLOATING_TAGS.includes(tags[0])) {
77
77
  tags.push(DEFAULT_TAG);
78
78
  }
@@ -8,7 +8,13 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane', 'builder-api']);
11
+ const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
12
+ 'keycloak',
13
+ 'miso-controller',
14
+ 'dataplane',
15
+ 'builder-api',
16
+ 'integration-server'
17
+ ]);
12
18
 
13
19
  /**
14
20
  * @param {string} appName
@@ -473,7 +473,13 @@ function getBuilderRoot() {
473
473
  * {@link getSystemBuilderRoot} (= {@link getAppsMaterializationParent}`/builder`), never the global CLI package tree.
474
474
  * @readonly
475
475
  */
476
- const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane', 'builder-api']);
476
+ const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
477
+ 'keycloak',
478
+ 'miso-controller',
479
+ 'dataplane',
480
+ 'builder-api',
481
+ 'integration-server'
482
+ ]);
477
483
 
478
484
  /**
479
485
  * @param {string} appName
@@ -1156,4 +1162,3 @@ module.exports = {
1156
1162
  resolveAppWorkspace: require('./app-path-resolve').resolveAppWorkspace,
1157
1163
  resolveExternalSystemPath: require('./app-path-resolve').resolveExternalSystemPath
1158
1164
  };
1159
-
@@ -42,7 +42,7 @@ function isPlatformCatalogKvRef(val) {
42
42
 
43
43
  /**
44
44
  * True when repair should normalize a kv value to path-style kv://systemKey/variable.
45
- * Platform catalog refs (e.g. kv://secrets-openaiApiKeyVault) are never legacy.
45
+ * Platform catalog refs (e.g. kv://redis-passwordKeyVault) are never legacy.
46
46
  *
47
47
  * @param {string} val - Value from authentication.security or configuration
48
48
  * @returns {boolean}
@@ -164,6 +164,35 @@ async function ensurePlatformDatabaseSlot(container, slot, secrets, adminPasswor
164
164
  return slot.dbName;
165
165
  }
166
166
 
167
+ /**
168
+ * Databases this server already has.
169
+ *
170
+ * Read before anything is created, so an environment can adopt a database an older
171
+ * installation left behind instead of renaming it — a rename here means dropping and
172
+ * recreating, which means losing the data.
173
+ *
174
+ * @async
175
+ * @param {string} container - Postgres container name
176
+ * @param {string} adminPassword - Superuser password
177
+ * @returns {Promise<string[]>} Database names, empty when the server cannot be listed
178
+ */
179
+ async function listExistingDatabaseNames(container, adminPassword) {
180
+ try {
181
+ const result = await dockerExec.execWithDockerEnv(
182
+ `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "SELECT datname FROM pg_database WHERE datistemplate = false"`,
183
+ { env: { PGPASSWORD: adminPassword } }
184
+ );
185
+ return String((result && result.stdout) || '')
186
+ .split('\n')
187
+ .map((line) => line.trim())
188
+ .filter(Boolean);
189
+ } catch {
190
+ // An unreadable server adopts nothing and creates under the contract name. Failing here
191
+ // would block a fresh install, where there is nothing to list in the first place.
192
+ return [];
193
+ }
194
+ }
195
+
167
196
  /**
168
197
  * @async
169
198
  * @returns {Promise<{ container: string, adminPassword: string, secrets: Object }>}
@@ -202,5 +231,10 @@ module.exports = {
202
231
  PLATFORM_DATABASE_SLOTS,
203
232
  bootstrapPlatformPostgresDatabases,
204
233
  pgRoleName,
205
- extractPasswordFromUrlOrValue
234
+ extractPasswordFromUrlOrValue,
235
+ // Reused by the local environment stage, which needs the same primitives under
236
+ // environment-scoped names rather than the unprefixed defaults this module creates.
237
+ resolveBootstrapContext,
238
+ listExistingDatabaseNames,
239
+ ensureRoleAndDatabase
206
240
  };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Prepare persistent storage before starting a local application container.
3
+ * @fileoverview Local storage mount and configured DATA_DIR initialization
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const dotenv = require('dotenv');
10
+
11
+ /**
12
+ * Read only a DATA_DIR beneath the container's storage mount.
13
+ * @param {string|undefined} envFile - Resolved application env file
14
+ * @returns {string[]} Relative directory components
15
+ */
16
+ function dataDirectoryParts(envFile) {
17
+ if (!envFile || !fs.existsSync(envFile)) return [];
18
+ const { DATA_DIR: dataDir } = dotenv.parse(fs.readFileSync(envFile, 'utf8'));
19
+ if (!dataDir || !dataDir.startsWith('/mnt/data/')) return [];
20
+ const parts = dataDir.slice('/mnt/data/'.length).split('/').filter(part => part && part !== '.');
21
+ if (parts.some(part => part === '..' || part.includes('\\') || part.includes('\0'))) {
22
+ throw new Error('DATA_DIR must remain inside the /mnt/data storage mount');
23
+ }
24
+ return parts;
25
+ }
26
+
27
+ /**
28
+ * Create the mount root and any configured DATA_DIR inside it, preserving state.
29
+ * Refuse child symlinks so initialization cannot write outside the storage root.
30
+ * @param {string} localPath - Host directory mounted at /mnt/data
31
+ * @param {string|undefined} envFile - Resolved application env file
32
+ * @returns {void}
33
+ */
34
+ function prepareLocalDataMount(localPath, envFile) {
35
+ try {
36
+ const parts = dataDirectoryParts(envFile);
37
+ fs.mkdirSync(localPath, { recursive: true });
38
+ let directory = localPath;
39
+ for (const part of parts) {
40
+ directory = path.join(directory, part);
41
+ try {
42
+ const stat = fs.lstatSync(directory);
43
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
44
+ throw new Error('DATA_DIR components must be directories, not symlinks or files');
45
+ }
46
+ } catch (error) {
47
+ if (error.code !== 'ENOENT') throw error;
48
+ fs.mkdirSync(directory);
49
+ }
50
+ }
51
+ } catch (error) {
52
+ // Filesystem errors can include env content or paths; report only their code.
53
+ const reason = error.code || error.message;
54
+ throw new Error(`Unable to prepare local storage before container startup: ${reason}`);
55
+ }
56
+ }
57
+
58
+ module.exports = { prepareLocalDataMount };
@@ -59,4 +59,24 @@ function applyRedisDbIndexToEnvContent(content, dbIndex) {
59
59
  return out.join('\n');
60
60
  }
61
61
 
62
- module.exports = { applyRedisDbIndexToEnvContent, setRedisUrlDbIndex };
62
+ /**
63
+ * Point a scoped environment at its own Redis logical database.
64
+ *
65
+ * Here rather than at the call site because this module already owns the rewrite, and the
66
+ * caller is a 500-line file whose budget this would spend. Separating it is honest anyway:
67
+ * it is the only step that rewrites a resolved value instead of resolving one.
68
+ *
69
+ * @param {string} content - Resolved env content
70
+ * @param {string} runEnvKey - Run environment key
71
+ * @param {boolean} effective - Whether env-scoped resources apply
72
+ * @returns {string} Content, unchanged when scoping does not apply
73
+ */
74
+ function applyScopedRedisDbIndex(content, runEnvKey, effective) {
75
+ if (!effective) {
76
+ return content;
77
+ }
78
+ const { redisDbIndexForScopedRunEnv } = require('./environment-scoped-resources');
79
+ return applyRedisDbIndexToEnvContent(content, redisDbIndexForScopedRunEnv(runEnvKey));
80
+ }
81
+
82
+ module.exports = { applyRedisDbIndexToEnvContent, setRedisUrlDbIndex, applyScopedRedisDbIndex };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Where a registry credential comes from when none is stored.
3
+ *
4
+ * Builder is registry-agnostic: Docker Hub, GHCR and ACR are all a host, a username and a
5
+ * password, and an individual identity, a registry token and an admin key all fit that
6
+ * shape. The only special case is that an `*.azurecr.io` host can also be authenticated by
7
+ * the Azure CLI, which is worth trying before asking a person to paste a secret.
8
+ *
9
+ * Order: the stored pair (handled by the caller), then `az acr login` for ACR, then a
10
+ * prompt. A non-interactive run never prompts — it fails naming the host and how to
11
+ * configure it, because a hung prompt in CI is worse than a clear failure.
12
+ *
13
+ * @fileoverview Credential source strategy for registries without a stored pair
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ /** Azure Container Registry login server suffix. */
19
+ const ACR_HOST_SUFFIX = '.azurecr.io';
20
+
21
+ /**
22
+ * @param {string} host - Registry host
23
+ * @returns {boolean} True for an Azure Container Registry login server
24
+ */
25
+ function isAzureContainerRegistryHost(host) {
26
+ return String(host || '').trim().toLowerCase().endsWith(ACR_HOST_SUFFIX);
27
+ }
28
+
29
+ /**
30
+ * Message for a run that cannot ask for credentials.
31
+ *
32
+ * @param {string} host - Registry host
33
+ * @returns {string}
34
+ */
35
+ function nonInteractiveCredentialMessage(host) {
36
+ const acrHint = isAzureContainerRegistryHost(host)
37
+ ? ` For Azure Container Registry you can instead run "az login" so "az acr login --name ${String(host).split('.')[0]}" succeeds.`
38
+ : '';
39
+ return (
40
+ `No credentials for ${host} and nothing to ask: this run is not interactive. ` +
41
+ `Store a pair with "aifabrix dev set-registry ${host} --username <username> --password-stdin".${acrHint}`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Build the fallback `ensureRegistryLogin` uses when no pair is stored.
47
+ *
48
+ * Dependencies are injected so the chain can be tested without a terminal, an Azure CLI or
49
+ * a Docker daemon.
50
+ *
51
+ * @param {Object} [deps] - Injectable dependencies
52
+ * @param {boolean} [deps.interactive] - Whether a person can be asked; defaults to TTY
53
+ * @param {Function} [deps.acrLogin] - async (host) => void, defaults to `az acr login`
54
+ * @param {Function} [deps.promptCredentials] - async (host) => { username, password }
55
+ * @param {Function} [deps.saveCredentials] - async (host, username, password) => void
56
+ * @param {Function} [deps.dockerLogin] - async (host, username, password) => void
57
+ * @returns {(host: string) => Promise<'acr'|'prompt'>} Fallback for ensureRegistryLogin
58
+ */
59
+ /**
60
+ * An error that names the host it is about.
61
+ *
62
+ * Callers read `registryHost` off it to report which registry refused, so the property is
63
+ * attached in one place rather than at each throw.
64
+ *
65
+ * @param {string} message - Message
66
+ * @param {string} host - Registry host
67
+ * @returns {Error}
68
+ */
69
+ function hostError(message, host) {
70
+ const err = new Error(message);
71
+ err.registryHost = host;
72
+ return err;
73
+ }
74
+
75
+ function buildRegistryAuthFallback(deps = {}) {
76
+ const interactive = deps.interactive === undefined
77
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY)
78
+ : Boolean(deps.interactive);
79
+
80
+ /**
81
+ * Try `az acr login` for an ACR host.
82
+ *
83
+ * Extracted so the fallback itself stays readable. A failure here is deliberately not
84
+ * fatal when a prompt is still possible: an expired or absent `az login` is an ordinary
85
+ * state, and a stored pair or an answered prompt can still authenticate the host.
86
+ *
87
+ * @param {string} host - Registry host
88
+ * @returns {Promise<boolean>} True when ACR login succeeded
89
+ */
90
+ async function tryAcrLogin(host) {
91
+ if (!isAzureContainerRegistryHost(host)) {
92
+ return false;
93
+ }
94
+ const acrLogin = deps.acrLogin
95
+ || ((target) => require('../deployment/push').authenticateACR(target));
96
+ try {
97
+ await acrLogin(host);
98
+ return true;
99
+ } catch (error) {
100
+ if (!interactive) {
101
+ throw hostError(`${nonInteractiveCredentialMessage(host)} (az acr login failed: ${error.message})`, host);
102
+ }
103
+ return false;
104
+ }
105
+ }
106
+
107
+ return async function registryAuthFallback(host) {
108
+ if (await tryAcrLogin(host)) {
109
+ return 'acr';
110
+ }
111
+ if (!interactive) {
112
+ throw hostError(nonInteractiveCredentialMessage(host), host);
113
+ }
114
+
115
+ const promptCredentials = deps.promptCredentials || promptForRegistryCredentials;
116
+ const { username, password } = await promptCredentials(host);
117
+ if (!username || !password) {
118
+ throw new Error(`Registry credentials for ${host} are required: both a username and a password.`);
119
+ }
120
+
121
+ const saveCredentials = deps.saveCredentials
122
+ || ((target, user, pass) => require('./registry-credentials').saveRegistryCredentials(target, user, pass));
123
+ // Store before logging in, so the answer is not asked for again on the next command
124
+ // even if the login itself fails for an unrelated reason.
125
+ await saveCredentials(host, username, password);
126
+
127
+ const dockerLogin = deps.dockerLogin
128
+ || ((target, user, pass) => require('../deployment/push').authenticateExternalRegistry(target, user, pass));
129
+ await dockerLogin(host, username, password);
130
+ return 'prompt';
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Ask for a registry username and password. The password is never echoed.
136
+ *
137
+ * @param {string} host - Registry host
138
+ * @returns {Promise<{ username: string, password: string }>}
139
+ */
140
+ async function promptForRegistryCredentials(host) {
141
+ const inquirer = require('inquirer');
142
+ return inquirer.prompt([
143
+ { type: 'input', name: 'username', message: `Username for ${host}:` },
144
+ { type: 'password', name: 'password', mask: '*', message: `Password or token for ${host}:` }
145
+ ]);
146
+ }
147
+
148
+ module.exports = {
149
+ ACR_HOST_SUFFIX,
150
+ isAzureContainerRegistryHost,
151
+ nonInteractiveCredentialMessage,
152
+ buildRegistryAuthFallback,
153
+ promptForRegistryCredentials
154
+ };