@aifabrix/builder 2.61.2 → 2.63.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 (90) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/api/onboarding-status.api.js +117 -0
  4. package/lib/app/deploy.js +14 -5
  5. package/lib/app/register.js +24 -2
  6. package/lib/app/run-env-compose.js +44 -8
  7. package/lib/app/run-env-recovery.js +1 -0
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/app/run.js +6 -4
  10. package/lib/build/standard-docker-build.js +4 -0
  11. package/lib/channels/channel-artifact-generator.js +13 -3
  12. package/lib/cli/infra-guided-footers.js +10 -8
  13. package/lib/cli/infra-guided.js +60 -12
  14. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  15. package/lib/cli/setup-platform.js +20 -3
  16. package/lib/commands/auth-config.js +2 -0
  17. package/lib/commands/login.js +4 -0
  18. package/lib/commands/repair-datasource-apply.js +3 -1
  19. package/lib/commands/repair-datasource-legacy.js +94 -0
  20. package/lib/commands/setup-image-refresh.js +26 -0
  21. package/lib/commands/setup-modes.js +9 -2
  22. package/lib/commands/setup-onboarding-readiness.js +83 -0
  23. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  24. package/lib/commands/setup.js +35 -10
  25. package/lib/commands/up-builder-api.js +24 -8
  26. package/lib/commands/up-common.js +16 -1
  27. package/lib/commands/up-dataplane-bootstrap.js +149 -16
  28. package/lib/commands/up-dataplane-credentials.js +21 -0
  29. package/lib/commands/up-dataplane.js +26 -34
  30. package/lib/commands/up-integration-server.js +112 -0
  31. package/lib/commands/wizard-config-normalizer.js +5 -1
  32. package/lib/core/admin-secrets-env-overlay.js +8 -3
  33. package/lib/core/environment-access-policy.js +121 -0
  34. package/lib/core/environment-mode-policy.js +177 -0
  35. package/lib/core/local-env-overrides.js +149 -0
  36. package/lib/core/secrets-ensure.js +4 -2
  37. package/lib/core/secrets-env-content.js +12 -9
  38. package/lib/core/secrets-env-write.js +1 -1
  39. package/lib/datasource/capability/capability-manifest.js +1 -0
  40. package/lib/deployment/installation/index.js +14 -1
  41. package/lib/deployment/installation/infra-catalog.js +3 -0
  42. package/lib/deployment/installation/local-environment-stage.js +285 -0
  43. package/lib/deployment/installation/local-installation.js +76 -0
  44. package/lib/deployment/installation/stage-input.js +5 -1
  45. package/lib/generator/builders.js +3 -1
  46. package/lib/generator/index.js +32 -9
  47. package/lib/generator/split-variables.js +2 -2
  48. package/lib/parameters/database-adoption.js +125 -0
  49. package/lib/parameters/physical-database-naming.js +136 -0
  50. package/lib/parameters/physical-secret-name.js +194 -0
  51. package/lib/parameters/rsa-secret-values.js +165 -0
  52. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  53. package/lib/programmatic/run-channel-package.js +3 -1
  54. package/lib/programmatic/workspace-context.js +37 -2
  55. package/lib/schema/infra.parameter.yaml +61 -27
  56. package/lib/schema/infrastructure-schema.json +213 -120
  57. package/lib/utils/compose-generate-docker-compose.js +2 -2
  58. package/lib/utils/compose-generator.js +4 -7
  59. package/lib/utils/compose-miso-env.js +2 -2
  60. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  61. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  62. package/lib/utils/declarative-url-ports.js +3 -0
  63. package/lib/utils/env-environment-file-paths.js +105 -0
  64. package/lib/utils/environment-scoped-resources.js +48 -14
  65. package/lib/utils/log-redaction.js +4 -1
  66. package/lib/utils/paths-system-builder-keys.js +7 -1
  67. package/lib/utils/paths.js +7 -2
  68. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  69. package/lib/utils/redis-env-scope.js +21 -1
  70. package/lib/utils/registry-auth-sources.js +154 -0
  71. package/lib/utils/registry-credentials.js +131 -14
  72. package/lib/utils/secrets-generator.js +30 -21
  73. package/lib/utils/secrets-helpers.js +5 -7
  74. package/lib/utils/secrets-kv-scope.js +127 -28
  75. package/lib/utils/secrets-materialize-local.js +9 -0
  76. package/lib/utils/secrets-missing-error.js +35 -1
  77. package/lib/utils/token-manager.js +14 -8
  78. package/lib/utils/url-public-path-prefix.js +6 -10
  79. package/package.json +4 -2
  80. package/templates/README.md +6 -0
  81. package/templates/applications/builder-api/application.yaml +1 -1
  82. package/templates/applications/dataplane/application.yaml +2 -2
  83. package/templates/applications/dataplane/env.template +1 -1
  84. package/templates/applications/integration-server/application.yaml +91 -0
  85. package/templates/applications/integration-server/env.template +215 -0
  86. package/templates/applications/mirrored-templates.json +52 -0
  87. package/templates/applications/miso-controller/application.yaml +43 -1
  88. package/templates/applications/miso-controller/env.template +45 -39
  89. package/templates/marketplace/createUiDefinition.json +52 -4
  90. package/templates/marketplace/main.json +85 -7
@@ -202,6 +202,26 @@ async function getApplicationStatus(controllerUrl, envKey, appKey, authConfig) {
202
202
  );
203
203
  }
204
204
 
205
+ /**
206
+ * Update the authenticated application's own runtime status and URLs.
207
+ * POST /api/v1/environments/{envKey}/applications/self/status
208
+ * @requiresPermission {Controller} Application client token
209
+ * @async
210
+ * @function updateApplicationSelfStatus
211
+ * @param {string} controllerUrl - Controller base URL
212
+ * @param {string} envKey - Environment key
213
+ * @param {Object} authConfig - Client-token authentication configuration
214
+ * @param {Object} statusData - Runtime status payload
215
+ * @returns {Promise<Object>} Updated application response
216
+ * @throws {Error} If request fails
217
+ */
218
+ async function updateApplicationSelfStatus(controllerUrl, envKey, authConfig, statusData) {
219
+ const client = new ApiClient(controllerUrl, authConfig);
220
+ return await client.post(`/api/v1/environments/${envKey}/applications/self/status`, {
221
+ body: statusData
222
+ });
223
+ }
224
+
205
225
  module.exports = {
206
226
  listApplications,
207
227
  createApplication,
@@ -211,6 +231,6 @@ module.exports = {
211
231
  registerApplication,
212
232
  bootstrapApplication,
213
233
  rotateApplicationSecret,
214
- getApplicationStatus
234
+ getApplicationStatus,
235
+ updateApplicationSelfStatus
215
236
  };
216
-
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `POST /api/v1/auth/bootstrap` — an application's resolved configuration (Miso 257.0).
3
+ *
4
+ * This is what removes the drift. Miso generates an environment's variables through the same
5
+ * catalog and reconciliation it uses for Azure, holds them in whichever store the deployment
6
+ * calls for — Key Vault, or encrypted in `Environment.configuration` for a local install —
7
+ * and hands back the resolved set. Builder stops generating values and stops deriving secret
8
+ * names, so there is one generator, one naming rule and one record.
9
+ *
10
+ * **The caller is the application, not the operator.** A client id and secret are provisioned
11
+ * with the application itself, so there is no chicken-and-egg: they exchange for a client
12
+ * token at `/api/v1/auth/token`, which needs no prior authentication. The token carries the
13
+ * application and environment, which means the snapshot is server-assigned — an application
14
+ * cannot ask for another application's configuration, because it cannot mint a token naming
15
+ * one. Builder passes credentials it already holds and is told what that application gets.
16
+ *
17
+ * **`success === true` is load-bearing.** Miso writes this envelope out explicitly rather than
18
+ * through the helper every neighbouring route uses, because that helper omits `success` and a
19
+ * consumer treats its absence as a protocol error. Checking it here keeps Builder honest with
20
+ * the same contract the SDK enforces.
21
+ *
22
+ * @fileoverview Fetch an application's bootstrap configuration snapshot from Miso
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ /** The only protocol version the endpoint accepts. */
28
+ const BOOTSTRAP_PROTOCOL_VERSION = 1;
29
+
30
+ /**
31
+ * @param {string} controllerUrl - Controller base URL
32
+ * @returns {string} Without a trailing slash
33
+ */
34
+ function normalizeControllerUrl(controllerUrl) {
35
+ return String(controllerUrl || '').trim().replace(/\/+$/, '');
36
+ }
37
+
38
+ /**
39
+ * Exchange an application's client credentials for a client token.
40
+ *
41
+ * @async
42
+ * @param {string} controllerUrl - Controller base URL
43
+ * @param {{ clientId: string, clientSecret: string }} credentials - Application credentials
44
+ * @param {Function} [makeApiCall] - Injected transport
45
+ * @returns {Promise<string>} The client token
46
+ * @throws {Error} When the exchange fails or returns no token
47
+ */
48
+ async function exchangeClientToken(controllerUrl, credentials, makeApiCall) {
49
+ const call = makeApiCall || require('../utils/api').makeApiCall;
50
+ const response = await call(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/token`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'x-client-id': credentials.clientId,
55
+ 'x-client-secret': credentials.clientSecret
56
+ }
57
+ });
58
+ if (!response || response.success !== true) {
59
+ throw new Error(
60
+ `Could not authenticate as ${credentials.clientId}: ${(response && response.error) || 'request failed'}`
61
+ );
62
+ }
63
+ const data = response.data || {};
64
+ const token = data.token || data.data?.token || data.accessToken;
65
+ if (!token) {
66
+ // Names the client, never the secret, because this message reaches the terminal.
67
+ throw new Error(`Authentication for ${credentials.clientId} returned no token`);
68
+ }
69
+ return token;
70
+ }
71
+
72
+ /**
73
+ * Fetch an application's resolved configuration for its environment.
74
+ *
75
+ * @async
76
+ * @param {string} controllerUrl - Controller base URL
77
+ * @param {{ clientId: string, clientSecret: string }} credentials - Application credentials
78
+ * @param {Object} [deps] - Injected collaborators
79
+ * @returns {Promise<{ configuration: Object, context: Object, issuedAt: string }>} The snapshot
80
+ * @throws {Error} When authentication fails, or the snapshot is refused or malformed
81
+ */
82
+ async function fetchBootstrapSnapshot(controllerUrl, credentials, deps = {}) {
83
+ if (!credentials || !credentials.clientId || !credentials.clientSecret) {
84
+ throw new Error('Client id and secret are required to fetch a bootstrap snapshot');
85
+ }
86
+ const call = deps.makeApiCall || require('../utils/api').makeApiCall;
87
+ const token = await exchangeClientToken(controllerUrl, credentials, call);
88
+ const response = await call(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/bootstrap`, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ Authorization: `Bearer ${token}`
93
+ },
94
+ body: JSON.stringify({ protocolVersion: BOOTSTRAP_PROTOCOL_VERSION })
95
+ });
96
+ if (!response || response.success !== true) {
97
+ throw new Error(
98
+ `Miso refused a configuration snapshot for ${credentials.clientId}: ` +
99
+ `${(response && response.error) || 'request failed'}`
100
+ );
101
+ }
102
+ const snapshot = response.data && response.data.data ? response.data.data : response.data;
103
+ if (!snapshot || typeof snapshot.configuration !== 'object' || snapshot.configuration === null) {
104
+ throw new Error(
105
+ `Miso returned a snapshot with no configuration for ${credentials.clientId}`
106
+ );
107
+ }
108
+ return snapshot;
109
+ }
110
+
111
+ module.exports = {
112
+ BOOTSTRAP_PROTOCOL_VERSION,
113
+ exchangeClientToken,
114
+ fetchBootstrapSnapshot
115
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Read the public Miso required-baseline onboarding projection.
3
+ *
4
+ * @fileoverview Public Miso onboarding readiness client
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { ApiClient } = require('./index');
12
+
13
+ const ENDPOINT = '/api/v1/health/onboarding-status';
14
+ const PROJECTION_KEYS = ['code', 'retryable', 'state'];
15
+ const VALID_PROJECTIONS = new Set([
16
+ 'pending|null|true',
17
+ 'complete|null|false',
18
+ 'incomplete|baseline-dimensions-incomplete|true',
19
+ 'incomplete|baseline-roles-incomplete|true'
20
+ ]);
21
+ const VALID_PAIRS = new Set([
22
+ 'pending|null|true::pending|null|true',
23
+ 'incomplete|baseline-dimensions-incomplete|true::pending|null|true',
24
+ 'complete|null|false::pending|null|true',
25
+ 'complete|null|false::incomplete|baseline-roles-incomplete|true',
26
+ 'complete|null|false::complete|null|false'
27
+ ]);
28
+
29
+ function onboardingStatusError(message, code = 'MISO_ONBOARDING_STATUS_INVALID') {
30
+ const error = new Error(message);
31
+ error.code = code;
32
+ return error;
33
+ }
34
+
35
+ function projectionSignature(projection) {
36
+ return `${projection.state}|${projection.code === null ? 'null' : projection.code}|${projection.retryable}`;
37
+ }
38
+
39
+ function hasValidProjectionTypes(projection) {
40
+ return typeof projection.state === 'string' &&
41
+ (projection.code === null || typeof projection.code === 'string') &&
42
+ typeof projection.retryable === 'boolean';
43
+ }
44
+
45
+ function parseProjection(value, name) {
46
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
47
+ throw onboardingStatusError(`Miso ${name} readiness projection is missing or malformed.`);
48
+ }
49
+ const keys = Object.keys(value).sort();
50
+ if (keys.length !== PROJECTION_KEYS.length || keys.some((key, index) => key !== PROJECTION_KEYS[index])) {
51
+ throw onboardingStatusError(`Miso ${name} readiness projection has unsupported fields.`);
52
+ }
53
+ const projection = { state: value.state, code: value.code, retryable: value.retryable };
54
+ if (!hasValidProjectionTypes(projection)) {
55
+ throw onboardingStatusError(`Miso ${name} readiness projection has invalid field types.`);
56
+ }
57
+ if (!VALID_PROJECTIONS.has(projectionSignature(projection))) {
58
+ throw onboardingStatusError(`Miso ${name} readiness projection has an unsupported state.`);
59
+ }
60
+ return Object.freeze(projection);
61
+ }
62
+
63
+ function parseOnboardingStatusResponse(response) {
64
+ if (!response || response.success !== true || response.status !== 200) {
65
+ throw onboardingStatusError(
66
+ 'Miso onboarding readiness is unavailable.',
67
+ 'MISO_ONBOARDING_STATUS_UNAVAILABLE'
68
+ );
69
+ }
70
+ const wireBody = response.data;
71
+ if (!wireBody || typeof wireBody !== 'object' || Array.isArray(wireBody) ||
72
+ Object.prototype.hasOwnProperty.call(wireBody, 'success')) {
73
+ throw onboardingStatusError('Miso onboarding readiness response envelope is malformed.');
74
+ }
75
+ const data = wireBody.data;
76
+ if (!data || typeof data !== 'object' || Array.isArray(data) ||
77
+ Object.prototype.hasOwnProperty.call(data, 'success')) {
78
+ throw onboardingStatusError('Miso onboarding readiness data envelope is malformed.');
79
+ }
80
+ const baselineDimensions = parseProjection(data.baselineDimensions, 'Dimension');
81
+ const baselineRoles = parseProjection(data.baselineRoles, 'Business Role');
82
+ const pair = `${projectionSignature(baselineDimensions)}::${projectionSignature(baselineRoles)}`;
83
+ if (!VALID_PAIRS.has(pair)) {
84
+ throw onboardingStatusError('Miso onboarding readiness contains an unsupported step ordering.');
85
+ }
86
+ return Object.freeze({ baselineDimensions, baselineRoles });
87
+ }
88
+
89
+ /**
90
+ * Fetch required-baseline readiness from Miso Controller.
91
+ * GET /api/v1/health/onboarding-status
92
+ * @requiresPermission {Controller} Public
93
+ * @param {string} controllerUrl
94
+ * @param {{timeoutMs?: number, client?: ApiClient}} [options]
95
+ * @returns {Promise<{baselineDimensions: Object, baselineRoles: Object}>}
96
+ */
97
+ async function fetchOnboardingStatus(controllerUrl, options = {}) {
98
+ const client = options.client || new ApiClient(controllerUrl);
99
+ const requestOptions = {};
100
+ if (options.timeoutMs !== undefined) requestOptions.timeoutMs = options.timeoutMs;
101
+ let response;
102
+ try {
103
+ response = await client.get(ENDPOINT, requestOptions);
104
+ } catch {
105
+ throw onboardingStatusError(
106
+ 'Miso onboarding readiness request failed.',
107
+ 'MISO_ONBOARDING_STATUS_UNAVAILABLE'
108
+ );
109
+ }
110
+ return parseOnboardingStatusResponse(response);
111
+ }
112
+
113
+ module.exports = {
114
+ ENDPOINT,
115
+ fetchOnboardingStatus,
116
+ parseOnboardingStatusResponse
117
+ };
package/lib/app/deploy.js CHANGED
@@ -191,12 +191,21 @@ async function handleDeploymentError(error, appName, controllerUrl, usedExternal
191
191
  * @throws {Error} If image is missing or has no registry
192
192
  */
193
193
  function validateImageIsPullable(imageRef, appName) {
194
- if (!imageRef || !imageRef.includes('/')) {
195
- const hint = `Set image.registry and image.tag in builder/${appName}/application.yaml, or pass a full image ref (e.g. --image <registry>/${appName}:<tag>) when deploying`;
196
- throw new Error(
197
- `Deployed image must be pullable (include a registry). Current image: "${imageRef || 'none'}". ${hint}`
198
- );
194
+ if (imageRef && imageRef.includes('/')) {
195
+ return;
196
+ }
197
+ // No registry configured means the installation deploys the application.yaml image as it
198
+ // stands, from the local Docker daemon. Demanding a registry component here would make a
199
+ // local install impossible for the one case it is meant to serve. A configured registry
200
+ // still has to appear in the reference, because then the image is expected to be pullable.
201
+ const { readInstallationRegistrySync } = require('../utils/config-registry-preference');
202
+ if (!readInstallationRegistrySync()) {
203
+ return;
199
204
  }
205
+ const hint = `Set image.registry and image.tag in builder/${appName}/application.yaml, or pass a full image ref (e.g. --image <registry>/${appName}:<tag>) when deploying`;
206
+ throw new Error(
207
+ `Deployed image must be pullable (include a registry). Current image: "${imageRef || 'none'}". ${hint}`
208
+ );
200
209
  }
201
210
 
202
211
  /**
@@ -28,6 +28,24 @@ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-reg
28
28
  const { resolveControllerUrl } = require('../utils/controller-url');
29
29
  const config = require('../core/config');
30
30
 
31
+ /**
32
+ * Handle local credential persistence failure without exposing details in strict mode.
33
+ * @param {Error} error
34
+ * @param {boolean} required
35
+ * @returns {false}
36
+ */
37
+ function handleCredentialPersistenceFailure(error, required) {
38
+ if (required) {
39
+ const persistenceError = new Error(
40
+ 'Application credentials could not be saved to the supported local secret store.'
41
+ );
42
+ persistenceError.code = 'LOCAL_CREDENTIAL_PERSISTENCE_FAILED';
43
+ throw persistenceError;
44
+ }
45
+ logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
46
+ return false;
47
+ }
48
+
31
49
  /**
32
50
  * Build registration data payload from app configuration
33
51
  * @param {Object} appConfig - Application configuration
@@ -83,8 +101,11 @@ function buildRegistrationData(appConfig, options) {
83
101
  * @async
84
102
  * @param {Object} responseData - Registration response data
85
103
  * @param {string} apiUrl - API URL
104
+ * @param {Object} [options] - Persistence behavior
105
+ * @param {boolean} [options.required=false] - Throw a safe typed error when persistence fails
106
+ * @returns {Promise<boolean>} True when credentials were persisted
86
107
  */
87
- async function saveLocalCredentials(responseData, apiUrl) {
108
+ async function saveLocalCredentials(responseData, apiUrl, options = {}) {
88
109
  const registeredAppKey = responseData.application.key;
89
110
  const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
90
111
  const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
@@ -113,8 +134,9 @@ async function saveLocalCredentials(responseData, apiUrl) {
113
134
  logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
114
135
  }
115
136
  } catch (error) {
116
- logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
137
+ return handleCredentialPersistenceFailure(error, options.required === true);
117
138
  }
139
+ return true;
118
140
  }
119
141
 
120
142
  /**
@@ -13,20 +13,26 @@ const path = require('path');
13
13
  const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const pathsUtil = require('../utils/paths');
16
+ const { isScopeActiveForOptions } = require('../utils/environment-scoped-resources');
16
17
  const adminSecrets = require('../core/admin-secrets');
17
18
  const config = require('../core/config');
18
19
  const secretsEnsure = require('../core/secrets-ensure');
19
20
  const secretsEnvWrite = require('../core/secrets-env-write');
20
21
  const { getContainerPort } = require('../utils/port-resolver');
21
22
  const { applyAdminSecretsPlatformOverlay } = require('../core/admin-secrets-env-overlay');
23
+ const {
24
+ resolveDimensionAbacSetting,
25
+ applyDimensionAbacToEnvMap
26
+ } = require('../utils/dataplane-dimension-abac-setting');
27
+ const { resolveEffectiveRegistry } = require('../utils/resolve-docker-image-ref');
22
28
 
23
29
  /**
24
30
  * Merge app env with admin-secrets; overlay ensures platform keys come from admin file.
25
31
  * @param {Object.<string, string>} merged
26
32
  * @param {Object.<string, string>} adminObj
27
33
  */
28
- function mergeRunEnvWithAdminOverlay(merged, adminObj) {
29
- applyAdminSecretsPlatformOverlay(merged, adminObj);
34
+ function mergeRunEnvWithAdminOverlay(merged, adminObj, appName) {
35
+ applyAdminSecretsPlatformOverlay(merged, adminObj, appName);
30
36
  }
31
37
 
32
38
  async function applyOnboardingPlatformModeIfNeeded(appName, merged) {
@@ -37,6 +43,13 @@ async function applyOnboardingPlatformModeIfNeeded(appName, merged) {
37
43
  merged.ONBOARDING_FULL_PLATFORM = mode === 'full' ? 'true' : 'false';
38
44
  }
39
45
 
46
+ async function applyDimensionAbacSettingIfNeeded(appName, merged) {
47
+ if (appName !== 'dataplane') return;
48
+ const userConfig = await config.getConfig();
49
+ const resolved = resolveDimensionAbacSetting(userConfig);
50
+ applyDimensionAbacToEnvMap(appName, merged, resolved.value);
51
+ }
52
+
40
53
  /**
41
54
  * Backfill only secrets needed for `aifabrix run <app>`: kv:// keys from that app's env.template
42
55
  * (catalog defaults via placeholderContext). Does not run full `ensureInfraSecrets`.
@@ -108,9 +121,7 @@ function injectDatabaseNamesAndUsers(env, appConfig, scopeOpts = null) {
108
121
  if (!Array.isArray(databases) || databases.length === 0) return;
109
122
  const prefix =
110
123
  scopeOpts &&
111
- scopeOpts.effectiveEnvironmentScopedResources &&
112
- scopeOpts.runEnvKey &&
113
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
124
+ isScopeActiveForOptions(scopeOpts)
114
125
  ? `${String(scopeOpts.runEnvKey).toLowerCase()}-`
115
126
  : '';
116
127
  for (let i = 0; i < databases.length; i++) {
@@ -261,6 +272,29 @@ async function applyPostgresPasswordFromPgpass(merged, developerId) {
261
272
  if (pgpassPwd !== undefined) merged.POSTGRES_PASSWORD = pgpassPwd;
262
273
  }
263
274
 
275
+ /**
276
+ * Deliver existing local installation inputs to privileged Miso bootstrap only.
277
+ * @param {string} appName
278
+ * @param {Object} merged
279
+ * @param {Object} appConfig
280
+ * @param {Object|null} scopeOpts
281
+ */
282
+ function applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts) {
283
+ if (appName !== 'miso-controller') return;
284
+ const postgresPassword = merged.POSTGRES_PASSWORD;
285
+ if (typeof postgresPassword !== 'string' || postgresPassword.trim().length === 0) {
286
+ throw new Error(
287
+ 'Local PostgreSQL administrator password is unavailable. Re-run "aifabrix up-infra" before starting Miso Controller.'
288
+ );
289
+ }
290
+ merged.POSTGRES_ADMIN_PASSWORD = postgresPassword;
291
+ const { registry } = resolveEffectiveRegistry(appConfig, {
292
+ registry: scopeOpts?.registry
293
+ });
294
+ if (registry) merged.ACR_LOGIN_SERVER = registry;
295
+ else delete merged.ACR_LOGIN_SERVER;
296
+ }
297
+
264
298
  /**
265
299
  * @param {string} runEnvPath
266
300
  * @param {string} runEnvAdminPath
@@ -298,11 +332,13 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId
298
332
  runEnvKey
299
333
  });
300
334
  const merged = { ...adminObj, ...appObj };
301
- mergeRunEnvWithAdminOverlay(merged, adminObj);
335
+ mergeRunEnvWithAdminOverlay(merged, adminObj, appName);
302
336
  await applyOnboardingPlatformModeIfNeeded(appName, merged);
303
337
  await applyPostgresPasswordFromPgpass(merged, developerId);
338
+ applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts);
304
339
  injectDatabaseNamesAndUsers(merged, appConfig, scopeOpts);
305
340
  injectContainerPortForRun(merged, appConfig, appName);
341
+ await applyDimensionAbacSettingIfNeeded(appName, merged);
306
342
 
307
343
  const runEnvPath = path.join(devDir, '.env.run');
308
344
  const runEnvAdminPath = path.join(devDir, '.env.run.admin');
@@ -352,9 +388,9 @@ async function ensureRunEnvFilesPresent(
352
388
  * @throws {Error} If password keys appear in environment-like assignment
353
389
  */
354
390
  function assertNoPasswordLiteralsInCompose(composeContent) {
355
- const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
391
+ const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|POSTGRES_ADMIN_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
356
392
  if (badPattern.test(composeContent)) {
357
- throw new Error('Generated compose must not contain password literals (POSTGRES_PASSWORD, DB_*_PASSWORD). Use env_file only.');
393
+ throw new Error('Generated compose must not contain password literals (PostgreSQL or application database passwords). Use env_file only.');
358
394
  }
359
395
  }
360
396
 
@@ -26,6 +26,7 @@ function buildRunEnvRecovery(appName, prepared) {
26
26
  prepared.appConfig.developerId,
27
27
  {
28
28
  runEnvKey: prepared.mergedRunOptions.env,
29
+ registry: prepared.mergedRunOptions.registry,
29
30
  effectiveEnvironmentScopedResources:
30
31
  prepared.mergedRunOptions.effectiveEnvironmentScopedResources
31
32
  }
@@ -366,7 +366,7 @@ async function prepareEnvironment(appName, appConfig, options) {
366
366
  appConfig,
367
367
  devDir,
368
368
  developerId,
369
- { runEnvKey, effectiveEnvironmentScopedResources }
369
+ { runEnvKey, effectiveEnvironmentScopedResources, registry: options.registry }
370
370
  );
371
371
  const { hydrateMissingLocalParameters } = require('./run-parameter-sync');
372
372
  await hydrateMissingLocalParameters(appName, runEnvPath);
package/lib/app/run.js CHANGED
@@ -258,10 +258,12 @@ function applyTraefikFlagToRunOptions(runOptions, userCfg) {
258
258
 
259
259
  /**
260
260
  * @param {string} envKey
261
- * @throws {Error} If env is not dev, tst, or pro
261
+ * @param {string} appName
262
+ * @throws {Error} If env is not a supported workload environment or a miso-only platform app
262
263
  */
263
- function assertValidRunEnvKey(envKey) {
264
- if (envKey !== 'dev' && envKey !== 'tst' && envKey !== 'pro') {
264
+ function assertValidRunEnvKey(envKey, appName) {
265
+ const misoOnlyPlatformApp = appName === 'builder-api' || appName === 'integration-server';
266
+ if (envKey !== 'dev' && envKey !== 'tst' && envKey !== 'pro' && !(envKey === 'miso' && misoOnlyPlatformApp)) {
265
267
  throw new Error('--env must be dev, tst, or pro');
266
268
  }
267
269
  }
@@ -345,7 +347,7 @@ function emitRunManifestMetadataIfRequested(appName, options) {
345
347
  */
346
348
  async function prepareAppRun(appName, options, debug) {
347
349
  const envKey = (options.env || 'dev').toLowerCase();
348
- assertValidRunEnvKey(envKey);
350
+ assertValidRunEnvKey(envKey, appName);
349
351
  const shouldContinue = await validateAppForRun(appName, debug);
350
352
  if (!shouldContinue) {
351
353
  return null;
@@ -104,6 +104,10 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
104
104
  * @returns {Promise<string>} Built image ref e.g. name:tag
105
105
  */
106
106
  async function runStandardDockerBuild(appName, options) {
107
+ // Before the tag plan, the promote shortcut and any Docker call: every build path reaches
108
+ // here, and a build into tst or pro must not get as far as producing an artifact.
109
+ const { assertBuildEnvironmentForOptions } = require('../core/environment-mode-policy');
110
+ await assertBuildEnvironmentForOptions(options);
107
111
  const { resolveBuildTagPlan, readAppVersion } = require('../utils/image-tags');
108
112
  const { buildConfig, config: appConfig } = await buildHelpers.loadAndValidateConfig(appName);
109
113
  // Resolve and validate every tag before any Docker build, tag, login or promote.
@@ -92,7 +92,7 @@ function renderTemplateFile(templatePath, context) {
92
92
  * @param {boolean} dryRun
93
93
  * @returns {{ installPath: string, content: string }|null}
94
94
  */
95
- function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dryRun) {
95
+ function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dryRun, durableDir) {
96
96
  const installTemplate = path.join(templatesDir, 'install.template.md.hbs');
97
97
  if (!fs.existsSync(installTemplate)) {
98
98
  return null;
@@ -100,11 +100,19 @@ function renderInstallGuideIfPresent(templatesDir, runtimeValues, outputDir, dry
100
100
  const content = renderTemplateFile(installTemplate, runtimeValues);
101
101
  assertNoSecretsInArtifact(content);
102
102
  const installPath = path.join(outputDir, 'INSTALL.md');
103
+ // `outputDir` is under gitignored dist/, so a copy there alone is lost on any clean and the
104
+ // API that serves this document 404s afterwards. Write the durable copy too when the caller
105
+ // knows where the channel's committed files live.
106
+ const durablePath = durableDir ? path.join(durableDir, 'INSTALL.md') : '';
103
107
  if (!dryRun) {
104
108
  fs.mkdirSync(outputDir, { recursive: true });
105
109
  fs.writeFileSync(installPath, content, 'utf8');
110
+ if (durablePath) {
111
+ fs.mkdirSync(durableDir, { recursive: true });
112
+ fs.writeFileSync(durablePath, content, 'utf8');
113
+ }
106
114
  }
107
- return { installPath, content };
115
+ return { installPath, content, durablePath: durablePath || undefined };
108
116
  }
109
117
 
110
118
  /**
@@ -130,6 +138,7 @@ function generateCopilotChannelArtifact(params, templatesDir, outputPath) {
130
138
  * @param {string} params.channelTarget
131
139
  * @param {Object} params.runtimeValues
132
140
  * @param {boolean} [params.dryRun]
141
+ * @param {string} [params.durableDir] - Committed channel directory for the persisted INSTALL.md
133
142
  * @returns {{ outputPath: string, artifactHash: string, content: string, installPath?: string }}
134
143
  */
135
144
  function generateChannelArtifact(params) {
@@ -158,7 +167,8 @@ function generateChannelArtifact(params) {
158
167
  templatesDir,
159
168
  runtimeValues,
160
169
  outputDir,
161
- params.dryRun === true
170
+ params.dryRun === true,
171
+ params.durableDir
162
172
  );
163
173
 
164
174
  const installReady = runtimeValues?.installReady === true;
@@ -46,11 +46,12 @@ async function resolvePlatformControllerUrlForDisplay() {
46
46
  return resolvePlatformControllerUrl();
47
47
  }
48
48
 
49
- async function logPlatformReadyFooter() {
50
- const [controllerUrl, dataplaneUrl, keycloakUrl] = await Promise.all([
49
+ async function logPlatformReadyFooter(options = {}) {
50
+ const [controllerUrl, dataplaneUrl, keycloakUrl, integrationServerUrl] = await Promise.all([
51
51
  resolvePlatformControllerUrlForDisplay(),
52
52
  computeAppBaseUrl('dataplane'),
53
- computeAppBaseUrl('keycloak')
53
+ computeAppBaseUrl('keycloak'),
54
+ options.integrationServer === true ? computeAppBaseUrl('integration-server') : null
54
55
  ]);
55
56
  const cfg = await config.getConfig();
56
57
  const env = cfg && cfg.environment ? cfg.environment : 'dev';
@@ -60,7 +61,7 @@ async function logPlatformReadyFooter() {
60
61
  logSection('Miso Controller', controllerUrl);
61
62
  logSection('Dataplane (DEV)', dataplaneUrl);
62
63
  logSection('Keycloak', keycloakUrl);
63
- logSection('API Documentation', `${dataplaneUrl}/api/docs`);
64
+ if (integrationServerUrl) logSection('Integration Server', `${integrationServerUrl}/download`);
64
65
  logSection('Knowledge Base', 'https://docs.aifabrix.ai');
65
66
  logSection('Getting Started', 'https://docs.aifabrix.ai/get-started');
66
67
  logFooterEnd();
@@ -91,20 +92,21 @@ async function logDataplaneReadyFooter() {
91
92
  /**
92
93
  * @param {'platform'|'dataplane'} [scope]
93
94
  */
94
- function logPlatformSetupDurationHint(scope = 'platform') {
95
+ function logPlatformSetupDurationHint(scope = 'platform', options = {}) {
96
+ const optionalApp = options.integrationServer === true ? ', Builder API, and Integration Server' : ', and Builder API';
95
97
  const line =
96
98
  scope === 'dataplane'
97
99
  ? 'Installation may take a few minutes. Authenticating and starting Dataplane — please wait.'
98
- : 'Installation may take a few minutes. Keycloak, Miso Controller, Dataplane, and Builder API are starting — please wait.';
100
+ : `Installation may take a few minutes. Keycloak, Miso Controller, Dataplane${optionalApp} are starting — please wait.`;
99
101
  logger.log(chalk.yellow(`⚠ ${line}`));
100
102
  }
101
103
 
102
- function logGuidedPlatformSetupHeader() {
104
+ function logGuidedPlatformSetupHeader(options = {}) {
103
105
  logger.log('');
104
106
  logger.log(title('AI Fabrix Platform Setup'));
105
107
  logger.log(SEPARATOR);
106
108
  logger.log('');
107
- logPlatformSetupDurationHint('platform');
109
+ logPlatformSetupDurationHint('platform', options);
108
110
  logger.log('');
109
111
  }
110
112