@aifabrix/builder 2.48.0 → 2.49.1

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 (83) hide show
  1. package/.cursor/skills/README.md +14 -0
  2. package/jest.isolated-projects.js +3 -0
  3. package/jest.projects.js +12 -0
  4. package/lib/api/applications.api.js +23 -0
  5. package/lib/api/dev.api.js +1 -1
  6. package/lib/api/index.js +5 -2
  7. package/lib/api/types/dev.types.js +3 -2
  8. package/lib/app/deploy-config.js +6 -4
  9. package/lib/app/register.js +1 -1
  10. package/lib/app/run-container-start.js +0 -9
  11. package/lib/app/run-helpers.js +8 -4
  12. package/lib/app/run-reload-sync.js +16 -18
  13. package/lib/app/run-stop-containers.js +48 -5
  14. package/lib/app/run.js +6 -3
  15. package/lib/app/show.js +2 -2
  16. package/lib/cli/infra-guided-auth.js +50 -12
  17. package/lib/cli/infra-guided-footers.js +164 -0
  18. package/lib/cli/infra-guided-spinners.js +57 -0
  19. package/lib/cli/infra-guided.js +52 -181
  20. package/lib/cli/setup-app.js +1 -1
  21. package/lib/cli/setup-dev.js +3 -2
  22. package/lib/cli/setup-infra.js +3 -2
  23. package/lib/cli/setup-platform.js +3 -0
  24. package/lib/commands/login-device.js +6 -2
  25. package/lib/commands/login.js +4 -1
  26. package/lib/commands/setup-modes.js +19 -3
  27. package/lib/commands/setup-platform-auth.js +25 -0
  28. package/lib/commands/setup-prompts-admin.js +9 -0
  29. package/lib/commands/setup.js +60 -8
  30. package/lib/commands/teardown.js +3 -5
  31. package/lib/commands/up-common.js +8 -11
  32. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  33. package/lib/commands/up-dataplane-credentials.js +19 -4
  34. package/lib/commands/up-dataplane.js +155 -24
  35. package/lib/commands/up-miso.js +6 -2
  36. package/lib/core/secrets-env-declarative-expand.js +35 -23
  37. package/lib/core/setup-platform-mode.js +43 -0
  38. package/lib/deployment/deployer-status.js +26 -3
  39. package/lib/deployment/deployer.js +7 -14
  40. package/lib/external-system/download-helpers.js +10 -1
  41. package/lib/infrastructure/compose.js +40 -12
  42. package/lib/infrastructure/services.js +15 -3
  43. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  44. package/lib/utils/app-bootstrap-api.js +53 -0
  45. package/lib/utils/app-run-containers.js +14 -3
  46. package/lib/utils/applications-config-defaults.js +19 -3
  47. package/lib/utils/compose-generate-docker-compose.js +16 -5
  48. package/lib/utils/compose-generator.js +34 -15
  49. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  50. package/lib/utils/config-paths.js +6 -1
  51. package/lib/utils/dev-user-groups.js +1 -0
  52. package/lib/utils/docker-daemon-tls-ca.js +94 -57
  53. package/lib/utils/docker-reload-mount.js +12 -1
  54. package/lib/utils/external-env-template.js +53 -18
  55. package/lib/utils/health-check-poll-ui.js +119 -0
  56. package/lib/utils/health-check-url.js +54 -34
  57. package/lib/utils/health-check.js +91 -35
  58. package/lib/utils/infra-optional-service-flags.js +31 -2
  59. package/lib/utils/mutagen.js +7 -4
  60. package/lib/utils/paths.js +26 -36
  61. package/lib/utils/platform-controller-url.js +23 -1
  62. package/lib/utils/refresh-remote-settings.js +67 -0
  63. package/lib/utils/remote-host-for-front-door.js +43 -0
  64. package/lib/utils/run-cli-flags.js +37 -1
  65. package/lib/utils/url-declarative-public-base.js +11 -4
  66. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  67. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  68. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  69. package/lib/utils/url-declarative-resolve.js +2 -0
  70. package/lib/utils/urls-local-registry-scan.js +95 -10
  71. package/lib/utils/urls-local-registry.js +24 -1
  72. package/lib/utils/worker-sidecars.js +2 -2
  73. package/lib/validation/external-system-auth-rules.js +45 -7
  74. package/package.json +2 -1
  75. package/scripts/test-dataplane-bootstrap.js +331 -0
  76. package/templates/applications/dataplane/application.yaml +1 -1
  77. package/templates/applications/dataplane/env.template +24 -6
  78. package/templates/applications/dataplane/rbac.yaml +11 -0
  79. package/templates/applications/miso-controller/application.yaml +1 -1
  80. package/templates/applications/miso-controller/env.template +5 -0
  81. package/templates/infra/compose.yaml.hbs +13 -9
  82. package/templates/python/docker-compose.hbs +3 -0
  83. package/templates/typescript/docker-compose.hbs +3 -0
@@ -163,7 +163,10 @@ async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
163
163
  return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope, {
164
164
  compact: Boolean(options.compact),
165
165
  exitOnFailure: options.exitOnFailure !== false,
166
- tokenControllerUrl: options.tokenControllerUrl
166
+ tokenControllerUrl: options.tokenControllerUrl,
167
+ guidedInstall: options.guidedInstall === true,
168
+ onProgressText: options.onProgressText,
169
+ suppressSuccessLine: options.suppressSuccessLine === true
167
170
  });
168
171
  }
169
172
 
@@ -42,11 +42,19 @@ const postgresBootstrap = require('../utils/postgres-platform-bootstrap');
42
42
  const appLib = require('../app');
43
43
  const setupPrompts = require('./setup-prompts');
44
44
  const { ensureSetupPlatformAuth, syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
45
+ const {
46
+ refreshRemoteSettingsFromBuilderIfPossible
47
+ } = require('../utils/refresh-remote-settings');
45
48
  const { handleLogin } = require('./login');
46
49
  const infraGuided = require('../cli/infra-guided');
50
+ const {
51
+ applyPlatformTopologyFlagsToConfig,
52
+ platformTopologyNeedsReconcile
53
+ } = require('../core/setup-platform-mode');
47
54
  const {
48
55
  computeEffectiveInfraOptionalFlags,
49
- persistMissingInfraOptionalServiceFlags
56
+ persistMissingInfraOptionalServiceFlags,
57
+ topologyFlagsForConfig
50
58
  } = require('../utils/infra-optional-service-flags');
51
59
 
52
60
  /** Builder app keys touched by `up-platform --force`. */
@@ -197,11 +205,19 @@ async function maybePromptForExistingBuilderDir({ force, skipBuilderConflictProm
197
205
  */
198
206
  async function startInfraFromConfig(overrides = {}) {
199
207
  await config.ensureSecretsEncryptionKey();
200
- const cfg = await config.getConfig();
208
+ await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
209
+ let cfg = await config.getConfig();
210
+ if (platformTopologyNeedsReconcile(cfg)) {
211
+ applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
212
+ await config.saveConfig(cfg);
213
+ cfg = await config.getConfig();
214
+ }
201
215
  const emailOverride = String(overrides.adminEmail || '').trim();
202
216
  const emailFromConfig = typeof cfg.adminEmail === 'string' ? cfg.adminEmail.trim() : '';
203
217
  const adminEmailMerged = emailOverride || emailFromConfig || undefined;
204
218
  const effective = computeEffectiveInfraOptionalFlags(cfg, {});
219
+ const topology = topologyFlagsForConfig(cfg);
220
+ const tlsEnabled = topology ? topology.tlsEnabled : cfg.tlsEnabled === true;
205
221
  const legacyPwd =
206
222
  overrides.passwordBundle && overrides.passwordBundle.mode === 'single'
207
223
  ? overrides.passwordBundle.password
@@ -215,7 +231,7 @@ async function startInfraFromConfig(overrides = {}) {
215
231
  adminPwd: legacyPwd,
216
232
  adminEmail: adminEmailMerged,
217
233
  passwordBundle: overrides.passwordBundle,
218
- tlsEnabled: cfg.tlsEnabled === true
234
+ tlsEnabled
219
235
  });
220
236
  });
221
237
  await persistMissingInfraOptionalServiceFlags(cfg, effective);
@@ -14,6 +14,14 @@ const { resolvePlatformControllerUrl } = require('../utils/platform-controller-u
14
14
  const { isPlatformAuthValidForController } = require('../utils/controller-url');
15
15
  const { infoLine } = require('../utils/cli-test-layout-chalk');
16
16
  const logger = require('../utils/logger');
17
+ const chalk = require('chalk');
18
+ const {
19
+ refreshRemoteSettingsFromBuilderIfPossible
20
+ } = require('../utils/refresh-remote-settings');
21
+ const {
22
+ applyPlatformTopologyFlagsToConfig,
23
+ platformTopologyNeedsReconcile
24
+ } = require('../core/setup-platform-mode');
17
25
 
18
26
  /**
19
27
  * Recompute `controller` and `platform-controller` from traefik / setupPlatformMode / front-door config.
@@ -24,6 +32,23 @@ const logger = require('../utils/logger');
24
32
  * @returns {Promise<string>} Normalized platform controller URL
25
33
  */
26
34
  async function syncPlatformControllerUrlsInConfig() {
35
+ const refresh = await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
36
+ const cfg = await config.getConfig();
37
+ if (platformTopologyNeedsReconcile(cfg)) {
38
+ applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
39
+ await config.saveConfig(cfg);
40
+ }
41
+ const remoteServer = await config.getRemoteServer();
42
+ const rootDomain = await config.getDeveloperRootDomain();
43
+ if (remoteServer && !rootDomain && !refresh.refreshed) {
44
+ logger.warn(
45
+ chalk.yellow(
46
+ 'Platform URLs may be wrong: developer-root-domain is missing. ' +
47
+ 'Run "aifabrix dev refresh" (or "aifabrix dev init") to sync from the Builder Server.'
48
+ )
49
+ );
50
+ }
51
+
27
52
  const platformControllerUrl = await resolvePlatformControllerUrl();
28
53
  await config.setPlatformControllerUrl(platformControllerUrl);
29
54
  await config.setControllerUrl(platformControllerUrl);
@@ -225,6 +225,15 @@ async function promptAdminCredentialsWithProfile(options = {}) {
225
225
  await config.setSetupInstallationProfile(profile);
226
226
 
227
227
  const adminEmail = await promptAdminEmail(options);
228
+ const cliPassword = String(options.adminPassword || '').trim();
229
+ if (cliPassword) {
230
+ validatePassword(cliPassword);
231
+ const passwordBundle = isProInstallationProfile(profile)
232
+ ? singlePasswordBundle(cliPassword)
233
+ : singlePasswordBundle(cliPassword);
234
+ return { adminEmail, passwordBundle, profile };
235
+ }
236
+
228
237
  const passwordBundle = isProInstallationProfile(profile)
229
238
  ? await promptProPasswordBundle(options)
230
239
  : await promptDevPasswordBundle();
@@ -29,7 +29,9 @@ const {
29
29
 
30
30
  const setupPrompts = require('./setup-prompts');
31
31
  const setupPlatformModePrompts = require('./setup-prompts-platform-mode');
32
- const { flagsForSetupPlatformMode } = require('../core/setup-platform-mode');
32
+ const {
33
+ applyPlatformTopologyFlagsToConfig
34
+ } = require('../core/setup-platform-mode');
33
35
  const setupModes = require('./setup-modes');
34
36
  const { syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
35
37
  const installationLog = require('../utils/installation-log');
@@ -37,6 +39,11 @@ const { beginSetupCommandFlow, endSetupCommandFlow } = require('./setup-run-cont
37
39
 
38
40
  const MODE = setupPrompts.MODE;
39
41
 
42
+ const CLI_SETUP_MODES = Object.freeze({
43
+ FRESH: 'fresh',
44
+ UPDATE_IMAGES: 'update-images'
45
+ });
46
+
40
47
  const SEPARATOR = '────────────────────────────────────────';
41
48
 
42
49
  function title(text) {
@@ -58,15 +65,53 @@ async function maybePinDeveloperIdForFreshInstall(rawDeveloper) {
58
65
  logger.log(infoLine(`Developer ID pinned to ${dev}.`));
59
66
  }
60
67
 
68
+ /**
69
+ * @param {Object} options
70
+ * @returns {'fresh'|'update-images'|null}
71
+ */
72
+ function normalizeCliMode(options) {
73
+ const raw = options.mode ?? options.setupMode;
74
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
75
+ return null;
76
+ }
77
+ const mode = String(raw).trim().toLowerCase();
78
+ if (mode === CLI_SETUP_MODES.FRESH || mode === CLI_SETUP_MODES.UPDATE_IMAGES) {
79
+ return mode;
80
+ }
81
+ throw new Error(`Invalid --mode "${raw}". Use fresh or update-images.`);
82
+ }
83
+
84
+ /**
85
+ * Non-interactive path when --mode is set (af-server automation).
86
+ * @param {Object} options
87
+ * @param {'fresh'|'update-images'} cliMode
88
+ * @returns {Promise<{ outcome: string, setupMode: string }>}
89
+ */
90
+ async function runSetupWithCliMode(options, cliMode) {
91
+ logSetupHeader();
92
+ logger.log(formatProgress('Detecting current installation state...'));
93
+ const running = await isInfraRunning();
94
+ if (cliMode === CLI_SETUP_MODES.FRESH) {
95
+ if (running) {
96
+ throw new Error(
97
+ 'Infrastructure is already running; use --mode update-images or run teardown first.'
98
+ );
99
+ }
100
+ await runFreshInstallFlow(options);
101
+ return { outcome: 'success', setupMode: CLI_SETUP_MODES.FRESH };
102
+ }
103
+ if (!running) {
104
+ throw new Error('No infrastructure running; use --mode fresh for first-time install.');
105
+ }
106
+ await dispatchMode(MODE.UPDATE_IMAGES);
107
+ logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${cliMode}).`));
108
+ return { outcome: 'success', setupMode: cliMode };
109
+ }
110
+
61
111
  async function applySetupPlatformMode(options) {
62
112
  const mode = await setupPlatformModePrompts.promptSetupPlatformMode(options);
63
- await config.setSetupPlatformMode(mode);
64
- const derived = flagsForSetupPlatformMode(mode);
65
- // These are the user gates that control full-mode behavior.
66
113
  const cfg = await config.getConfig();
67
- cfg.traefik = derived.traefik;
68
- cfg.tlsEnabled = derived.tlsEnabled;
69
- cfg.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
114
+ applyPlatformTopologyFlagsToConfig(cfg, mode);
70
115
  await config.saveConfig(cfg);
71
116
  await syncPlatformControllerUrlsInConfig();
72
117
  return mode;
@@ -150,6 +195,11 @@ async function dispatchMode(mode) {
150
195
  * @returns {Promise<{ outcome: string, setupMode: string|null }>}
151
196
  */
152
197
  async function runSetupMainPath(options, assumeYes) {
198
+ const cliMode = normalizeCliMode(options);
199
+ if (cliMode) {
200
+ return runSetupWithCliMode(options, cliMode);
201
+ }
202
+
153
203
  logSetupHeader();
154
204
  logger.log(formatProgress('Detecting current installation state...'));
155
205
  const running = await isInfraRunning();
@@ -273,5 +323,7 @@ async function handleSetup(options = {}) {
273
323
  module.exports = {
274
324
  handleSetup,
275
325
  isInfraRunning,
276
- dispatchMode
326
+ dispatchMode,
327
+ normalizeCliMode,
328
+ CLI_SETUP_MODES
277
329
  };
@@ -120,17 +120,15 @@ function cleanAifabrixSystemDir() {
120
120
  }
121
121
 
122
122
  /**
123
- * Remove materialized `builder/` trees (aifabrix-work, aifabrix-home / config dir, cwd).
124
- * Complements {@link cleanAifabrixSystemDir} when `builder/` lives under `aifabrix-work` outside
125
- * the config directory.
123
+ * Remove materialized `builder/` trees under home/work and config dir only.
124
+ * Does not remove `cwd/builder` in product repos (e.g. aifabrix-dataplane/builder/dataplane).
126
125
  *
127
126
  * @returns {{ removed: string[], failed: string[] }}
128
127
  */
129
128
  function cleanMaterializedBuilderTrees() {
130
129
  const candidates = [
131
130
  pathsUtil.getBuilderRoot(),
132
- path.join(pathsUtil.getAifabrixSystemDir(), 'builder'),
133
- path.join(process.cwd(), 'builder')
131
+ path.join(pathsUtil.getAifabrixSystemDir(), 'builder')
134
132
  ];
135
133
  const seen = new Set();
136
134
  const removed = [];
@@ -242,26 +242,21 @@ function isUnderAllowedBuilderRoot(resolvedAppPath, allowedRoots) {
242
242
  }
243
243
 
244
244
  /**
245
- * Removes builder app directories for the given app names. Only removes paths under an allowed
246
- * builder root (project `builder/` or `~/.aifabrix/builder/`) to prevent path traversal.
247
- * Uses {@link pathsUtil.getBuilderPath} for each app (system apps may resolve under the config dir).
245
+ * Removes platform app directories under the home/work materialized `builder/` root only.
246
+ * Never deletes `cwd/builder` trees in product repos (e.g. aifabrix-miso/builder/miso-controller).
248
247
  *
249
248
  * @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
250
249
  * @returns {Promise<void>}
251
- * @throws {Error} If any path is outside allowed builder roots (path traversal attempt)
250
+ * @throws {Error} If any path is outside the home builder root (path traversal attempt)
252
251
  */
253
252
  async function cleanBuilderAppDirs(appNames, opts = {}) {
254
253
  const silent = Boolean(opts.silent);
255
254
  if (!Array.isArray(appNames) || appNames.length === 0) return;
256
- const allowedRoots = uniqueResolvedRoots([
257
- path.join(process.cwd(), 'builder'),
258
- pathsUtil.getBuilderRoot(),
259
- pathsUtil.getSystemBuilderRoot()
260
- ]);
255
+ const allowedRoots = uniqueResolvedRoots([pathsUtil.getSystemBuilderRoot()]);
261
256
  const cleaned = [];
262
257
  for (const appName of appNames) {
263
258
  if (!appName || typeof appName !== 'string') continue;
264
- const appPath = path.resolve(pathsUtil.getBuilderPath(appName));
259
+ const appPath = path.resolve(pathsUtil.getHomeBuilderAppPath(appName));
265
260
  if (!isUnderAllowedBuilderRoot(appPath, allowedRoots)) {
266
261
  throw new Error(
267
262
  `Path ${appPath} is outside allowed builder roots (${allowedRoots.join(', ')}); refusing to clean`
@@ -303,7 +298,9 @@ async function ensureAppFromTemplate(appName, opts = {}) {
303
298
  }
304
299
  const silent = Boolean(opts.silent);
305
300
 
306
- const appPath = pathsUtil.getBuilderPath(appName);
301
+ const appPath = pathsUtil.isSystemBuilderAppName(appName)
302
+ ? pathsUtil.getHomeBuilderAppPath(appName)
303
+ : pathsUtil.getBuilderPath(appName);
307
304
  const primaryCopied = await ensureTemplateAtPath(appName, appPath);
308
305
  if (primaryCopied && !silent) {
309
306
  logger.log(chalk.blue(`Creating builder/${appName} from template...`));
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Headless dataplane bootstrap provisioning (plan 197.0).
3
+ *
4
+ * @fileoverview up-dataplane bootstrap path when Docker + ENCRYPTION_KEY available
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('../utils/logger');
13
+ const config = require('../core/config');
14
+ const dockerUtils = require('../utils/docker');
15
+ const { loadSecrets } = require('../core/secrets-load');
16
+ const { callBootstrapApi } = require('../utils/app-bootstrap-api');
17
+ const {
18
+ loadVariablesYaml,
19
+ extractAppConfiguration
20
+ } = require('../utils/app-register-config');
21
+ const { buildRegistrationData, saveLocalCredentials, applyLocalhostPortAdjustment } = require('../app/register');
22
+ const { loadDataplanePipelineCredentialsFromSecretsFile } = require('./up-dataplane-credentials');
23
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
24
+
25
+ const ENCRYPTION_VAULT_KEYS = [
26
+ 'miso-controller-secrets-encryptionKeyVault',
27
+ 'secrets-encryptionKeyVault'
28
+ ];
29
+
30
+ /**
31
+ * @param {Record<string, unknown>} secrets
32
+ * @returns {string|null}
33
+ */
34
+ function pickBootstrapEncryptionKeyFromSecrets(secrets) {
35
+ if (!secrets || typeof secrets !== 'object') {
36
+ return null;
37
+ }
38
+ for (const vaultKey of ENCRYPTION_VAULT_KEYS) {
39
+ const raw = secrets[vaultKey];
40
+ if (raw === undefined || raw === null) {
41
+ continue;
42
+ }
43
+ const trimmed = String(raw).trim();
44
+ if (trimmed) {
45
+ return trimmed;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Resolve shared ENCRYPTION_KEY for bootstrap header (never log).
53
+ * Uses the same merged secrets store as kv:// resolution (local + aifabrix-secrets / remote API).
54
+ * Must match miso-controller `ENCRYPTION_KEY` (kv://miso-controller-secrets-encryptionKeyVault),
55
+ * not `secrets-encryption` in config.yaml (CLI file-encryption key only).
56
+ *
57
+ * @async
58
+ * @returns {Promise<string|null>}
59
+ */
60
+ async function resolveBootstrapEncryptionKey() {
61
+ await config.ensureSecretsEncryptionKey();
62
+ const secrets = await loadSecrets(undefined);
63
+ return pickBootstrapEncryptionKeyFromSecrets(secrets);
64
+ }
65
+
66
+ /**
67
+ * True when bootstrap path is eligible (dev + Docker + encryption key).
68
+ * @async
69
+ * @param {Object} [options]
70
+ * @returns {Promise<boolean>}
71
+ */
72
+ async function canUseBootstrapProvision(options = {}) {
73
+ if (options.allowNonDev === true) {
74
+ return false;
75
+ }
76
+ const cfg = await config.getConfig();
77
+ const environment = cfg && cfg.environment ? cfg.environment : 'dev';
78
+ if (environment !== 'dev') {
79
+ return false;
80
+ }
81
+ try {
82
+ await dockerUtils.ensureDockerAndCompose();
83
+ } catch {
84
+ return false;
85
+ }
86
+ const encryptionKey = await resolveBootstrapEncryptionKey();
87
+ return Boolean(encryptionKey);
88
+ }
89
+
90
+ /**
91
+ * Register dataplane via bootstrap endpoint (idempotent).
92
+ * @async
93
+ * @param {Object} options - up-dataplane options
94
+ * @param {string} controllerUrl
95
+ * @param {string} environmentKey
96
+ * @param {Function} buildDataplaneImageRefFn
97
+ * @returns {Promise<void>}
98
+ */
99
+ async function bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRefFn) {
100
+ const encryptionKey = await resolveBootstrapEncryptionKey();
101
+ if (!encryptionKey) {
102
+ throw new Error(
103
+ 'Bootstrap encryption key is not available. Ensure miso-controller-secrets-encryptionKeyVault ' +
104
+ 'exists in merged secrets (local secrets.local.yaml or aifabrix-secrets remote store) after up-miso resolve.'
105
+ );
106
+ }
107
+
108
+ const { variables } = await loadVariablesYaml('dataplane');
109
+ const appConfig = await extractAppConfiguration(variables, 'dataplane', options);
110
+ await applyLocalhostPortAdjustment(appConfig, controllerUrl, options);
111
+
112
+ const imageOverride = options.image || buildDataplaneImageRefFn(options);
113
+ const registerOpts = {
114
+ imageOverride,
115
+ image: imageOverride,
116
+ registryMode: options.registryMode,
117
+ envKey: environmentKey
118
+ };
119
+ const registrationData = buildRegistrationData(appConfig, registerOpts);
120
+
121
+ logger.log(chalk.blue('Provisioning dataplane via bootstrap (no device login)...'));
122
+ const responseData = await callBootstrapApi(
123
+ controllerUrl,
124
+ encryptionKey,
125
+ environmentKey,
126
+ registrationData
127
+ );
128
+
129
+ if (responseData.credentials && responseData.credentials.clientSecret) {
130
+ await saveLocalCredentials(responseData, controllerUrl);
131
+ logger.log(formatSuccessLine('Dataplane bootstrap credentials saved locally'));
132
+ } else if (responseData.status === 'already_provisioned') {
133
+ logger.log(formatSuccessLine('Dataplane already provisioned (bootstrap idempotent)'));
134
+ const localCredentials = await loadDataplanePipelineCredentialsFromSecretsFile();
135
+ if (!localCredentials) {
136
+ throw new Error(
137
+ 'Dataplane is registered on the controller but pipeline credentials are missing locally. ' +
138
+ 'Run fresh setup with volume wipe, or: aifabrix login && aifabrix app rotate-secret dataplane'
139
+ );
140
+ }
141
+ }
142
+ }
143
+
144
+ module.exports = {
145
+ canUseBootstrapProvision,
146
+ resolveBootstrapEncryptionKey,
147
+ bootstrapDataplaneApplication,
148
+ pickBootstrapEncryptionKeyFromSecrets
149
+ };
@@ -26,6 +26,12 @@ const MISALIGN_ERROR =
26
26
  'Dataplane pipeline credentials are not aligned with the controller. ' +
27
27
  'Run: aifabrix app rotate-secret dataplane && aifabrix run dataplane';
28
28
 
29
+ const BOOTSTRAP_MISALIGN_ERROR =
30
+ 'Dataplane bootstrap credentials are not accepted by the controller after deploy. ' +
31
+ 'Ensure miso-controller has BOOTSTRAP_APPKEYS=dataplane and ENCRYPTION_KEY set, then re-run: aifabrix up-dataplane';
32
+
33
+ const BOOTSTRAP_PROBE_RETRY = { maxAttempts: 15, delayMs: 3000 };
34
+
29
35
  /**
30
36
  * @param {string} controllerUrl
31
37
  * @returns {string}
@@ -120,9 +126,10 @@ async function credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts = 5, d
120
126
 
121
127
  function resolveCredentialProbeRetry(options = {}) {
122
128
  const retry = options.credentialProbeRetry || {};
129
+ const bootstrapDefaults = options.useBootstrap === true ? BOOTSTRAP_PROBE_RETRY : { maxAttempts: 5, delayMs: 2000 };
123
130
  return {
124
- maxAttempts: Number.isFinite(retry.maxAttempts) ? retry.maxAttempts : 5,
125
- delayMs: Number.isFinite(retry.delayMs) ? retry.delayMs : 2000
131
+ maxAttempts: Number.isFinite(retry.maxAttempts) ? retry.maxAttempts : bootstrapDefaults.maxAttempts,
132
+ delayMs: Number.isFinite(retry.delayMs) ? retry.delayMs : bootstrapDefaults.delayMs
126
133
  };
127
134
  }
128
135
 
@@ -155,7 +162,12 @@ async function credentialsAcceptToken(controllerUrl) {
155
162
  */
156
163
  async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
157
164
  const quiet = options.platformInstall === true || options.quiet === true;
158
- const aligned = await credentialsAcceptToken(controllerUrl);
165
+ const bootstrapPath = options.useBootstrap === true;
166
+ const { maxAttempts, delayMs } = resolveCredentialProbeRetry(options);
167
+
168
+ const aligned = bootstrapPath
169
+ ? await credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts, delayMs)
170
+ : await credentialsAcceptToken(controllerUrl);
159
171
  if (aligned) {
160
172
  if (!quiet) {
161
173
  logger.log(formatSuccessLine('Dataplane pipeline credentials verified.'));
@@ -163,12 +175,15 @@ async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
163
175
  return;
164
176
  }
165
177
 
178
+ if (bootstrapPath) {
179
+ throw new Error(BOOTSTRAP_MISALIGN_ERROR);
180
+ }
181
+
166
182
  if (!quiet) {
167
183
  logger.log(chalk.blue('Aligning dataplane pipeline credentials with controller...'));
168
184
  }
169
185
  await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet, exitOnFailure: false });
170
186
 
171
- const { maxAttempts, delayMs } = resolveCredentialProbeRetry(options);
172
187
  const stillMisaligned = !(await credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts, delayMs));
173
188
  if (stillMisaligned) {
174
189
  throw new Error(MISALIGN_ERROR);