@aifabrix/builder 2.48.0 → 2.49.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/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
  2. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
  3. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
  4. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
  5. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
  6. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
  7. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
  8. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
  9. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
  10. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
  11. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
  12. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
  13. package/.temp/validation/summary.txt +5 -0
  14. package/jest.isolated-projects.js +3 -0
  15. package/jest.projects.js +12 -0
  16. package/lib/api/applications.api.js +23 -0
  17. package/lib/api/dev.api.js +1 -1
  18. package/lib/api/index.js +5 -2
  19. package/lib/api/types/dev.types.js +3 -2
  20. package/lib/app/deploy-config.js +6 -4
  21. package/lib/app/register.js +1 -1
  22. package/lib/app/run-container-start.js +0 -9
  23. package/lib/app/run-helpers.js +8 -4
  24. package/lib/app/run-reload-sync.js +16 -18
  25. package/lib/app/run-stop-containers.js +48 -5
  26. package/lib/app/run.js +6 -3
  27. package/lib/app/show.js +2 -2
  28. package/lib/cli/infra-guided-auth.js +42 -6
  29. package/lib/cli/infra-guided-footers.js +164 -0
  30. package/lib/cli/infra-guided-spinners.js +57 -0
  31. package/lib/cli/infra-guided.js +41 -179
  32. package/lib/cli/setup-app.js +1 -1
  33. package/lib/cli/setup-dev.js +3 -2
  34. package/lib/cli/setup-infra.js +3 -2
  35. package/lib/cli/setup-platform.js +3 -0
  36. package/lib/commands/login-device.js +6 -2
  37. package/lib/commands/login.js +4 -1
  38. package/lib/commands/setup-modes.js +19 -3
  39. package/lib/commands/setup-platform-auth.js +25 -0
  40. package/lib/commands/setup-prompts-admin.js +9 -0
  41. package/lib/commands/setup.js +60 -8
  42. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  43. package/lib/commands/up-dataplane-credentials.js +19 -4
  44. package/lib/commands/up-dataplane.js +74 -11
  45. package/lib/core/secrets-env-declarative-expand.js +35 -23
  46. package/lib/core/setup-platform-mode.js +43 -0
  47. package/lib/deployment/deployer-status.js +26 -3
  48. package/lib/deployment/deployer.js +7 -14
  49. package/lib/external-system/download-helpers.js +10 -1
  50. package/lib/infrastructure/compose.js +40 -12
  51. package/lib/infrastructure/services.js +15 -3
  52. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  53. package/lib/utils/app-bootstrap-api.js +53 -0
  54. package/lib/utils/app-run-containers.js +14 -3
  55. package/lib/utils/applications-config-defaults.js +19 -3
  56. package/lib/utils/compose-generate-docker-compose.js +16 -5
  57. package/lib/utils/compose-generator.js +34 -15
  58. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  59. package/lib/utils/config-paths.js +6 -1
  60. package/lib/utils/dev-user-groups.js +1 -0
  61. package/lib/utils/docker-daemon-tls-ca.js +94 -57
  62. package/lib/utils/docker-reload-mount.js +12 -1
  63. package/lib/utils/external-env-template.js +53 -18
  64. package/lib/utils/health-check-poll-ui.js +84 -0
  65. package/lib/utils/health-check-url.js +54 -34
  66. package/lib/utils/health-check.js +43 -16
  67. package/lib/utils/infra-optional-service-flags.js +31 -2
  68. package/lib/utils/mutagen.js +7 -4
  69. package/lib/utils/platform-controller-url.js +23 -1
  70. package/lib/utils/refresh-remote-settings.js +67 -0
  71. package/lib/utils/remote-host-for-front-door.js +43 -0
  72. package/lib/utils/run-cli-flags.js +37 -1
  73. package/lib/utils/url-declarative-public-base.js +11 -4
  74. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  75. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  76. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  77. package/lib/utils/url-declarative-resolve.js +2 -0
  78. package/lib/utils/urls-local-registry-scan.js +95 -10
  79. package/lib/utils/urls-local-registry.js +24 -1
  80. package/lib/utils/worker-sidecars.js +2 -2
  81. package/lib/validation/external-system-auth-rules.js +45 -7
  82. package/package.json +2 -1
  83. package/scripts/test-dataplane-bootstrap.js +331 -0
  84. package/templates/applications/dataplane/env.template +22 -5
  85. package/templates/applications/dataplane/rbac.yaml +11 -0
  86. package/templates/applications/miso-controller/application.yaml +1 -1
  87. package/templates/applications/miso-controller/env.template +3 -0
  88. package/templates/infra/compose.yaml.hbs +13 -9
  89. package/templates/python/docker-compose.hbs +3 -0
  90. package/templates/typescript/docker-compose.hbs +3 -0
@@ -29,6 +29,8 @@ What this command does:
29
29
  2) Wipe data — drop every database and DB user, then up-infra + up-platform --force.
30
30
  3) Clean installation files — remove user-local secrets, then up-infra + up-platform --force.
31
31
  4) Update images — pull the latest infra and platform images, then up-infra + up-platform.
32
+ - With --mode fresh: first-time install when no infra is running (non-interactive; use with --admin-email and --admin-password).
33
+ - With --mode update-images: refresh images when infra is already running (non-interactive).
32
34
 
33
35
  AI tool keys are read from the user-local file (~/.aifabrix/secrets.local.yaml) merged with the shared aifabrix-secrets file. If either source already provides keys, the AI tool prompt is skipped.
34
36
 
@@ -59,6 +61,7 @@ function registerSetup(program) {
59
61
  .option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
60
62
  .option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
61
63
  .option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
64
+ .option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
62
65
  .option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
63
66
  .addHelpText('after', SETUP_HELP_AFTER)
64
67
  .action(async(options) => {
@@ -96,7 +96,7 @@ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, ex
96
96
  logger.log(chalk.gray(`Environment: ${envKey}`));
97
97
  }
98
98
  logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
99
- } else {
99
+ } else if (!opts.suppressSuccessLine && !opts.guidedInstall) {
100
100
  logger.log(formatSuccessLine('Authentication successful'));
101
101
  }
102
102
  }
@@ -116,8 +116,10 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
116
116
  const spinner = useSpinner
117
117
  ? ora({ text: 'Waiting for approval', spinner: 'dots' }).start()
118
118
  : null;
119
- if (!useSpinner) {
119
+ if (!useSpinner && !opts.onProgressText) {
120
120
  logger.log('Waiting for approval...');
121
+ } else if (opts.onProgressText) {
122
+ opts.onProgressText('Waiting for approval...');
121
123
  }
122
124
 
123
125
  let pollCount = 0;
@@ -125,6 +127,8 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
125
127
  pollCount++;
126
128
  if (spinner) {
127
129
  spinner.text = `Waiting for approval (attempt ${pollCount})...`;
130
+ } else if (opts.onProgressText) {
131
+ opts.onProgressText(`Waiting for approval (attempt ${pollCount})...`);
128
132
  }
129
133
  };
130
134
 
@@ -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
  };
@@ -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);
@@ -35,6 +35,10 @@ const {
35
35
  refreshUrlsLocalRegistryForCurrentProject
36
36
  } = require('./up-common');
37
37
  const { alignDataplaneCredentialsAfterDeploy } = require('./up-dataplane-credentials');
38
+ const {
39
+ canUseBootstrapProvision,
40
+ bootstrapDataplaneApplication
41
+ } = require('./up-dataplane-bootstrap');
38
42
 
39
43
  const CONTROLLER_HEALTH_PATH = '/health';
40
44
 
@@ -120,7 +124,9 @@ async function resolveUpDataplaneAuthContext(options, environmentKey) {
120
124
  if (options.allowNonDev !== true) {
121
125
  assertDevEnvironmentForDataplane(cfg);
122
126
  }
123
- logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
127
+ if (options.platformInstall !== true) {
128
+ logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
129
+ }
124
130
  return { controllerUrl, authConfig };
125
131
  }
126
132
 
@@ -223,6 +229,57 @@ async function computeDataplaneRunOverrides(options) {
223
229
  return { runEnvKey, portOverride };
224
230
  }
225
231
 
232
+ /**
233
+ * Resolve controller URL and auth for bootstrap vs device-login paths.
234
+ * @async
235
+ * @param {Object} options
236
+ * @param {string} environmentKey
237
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object|null, useBootstrap: boolean }>}
238
+ */
239
+ async function resolveUpDataplaneControllerContext(options, environmentKey) {
240
+ const useBootstrap = await canUseBootstrapProvision(options);
241
+ if (useBootstrap) {
242
+ if (options.skipInfraCheck !== true) {
243
+ await assertDevInfraUp();
244
+ }
245
+ const controllerUrl = await resolveControllerUrlWithHealthCheck();
246
+ const cfg = await config.getConfig();
247
+ if (options.allowNonDev !== true) {
248
+ assertDevEnvironmentForDataplane(cfg);
249
+ }
250
+ if (options.platformInstall !== true) {
251
+ logger.log(formatSuccessLine('Using bootstrap provision (no device login)'));
252
+ }
253
+ return { controllerUrl, authConfig: null, useBootstrap: true };
254
+ }
255
+
256
+ const authContext = await resolveUpDataplaneAuthContext(options, environmentKey);
257
+ return {
258
+ controllerUrl: authContext.controllerUrl,
259
+ authConfig: authContext.authConfig,
260
+ useBootstrap: false
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Register/bootstrap, deploy manifest, align credentials.
266
+ * @async
267
+ */
268
+ async function provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig) {
269
+ if (useBootstrap) {
270
+ await bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRef);
271
+ } else {
272
+ await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
273
+ }
274
+
275
+ await deployDataplaneToController({
276
+ ...options,
277
+ envKey: environmentKey,
278
+ ...(useBootstrap ? { deploymentAuth: 'client-credentials' } : {})
279
+ });
280
+ logger.log('');
281
+ }
282
+
226
283
  /**
227
284
  * Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
228
285
  * register or rotate (if already registered), deploy (send manifest to controller),
@@ -239,30 +296,34 @@ async function computeDataplaneRunOverrides(options) {
239
296
  * @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
240
297
  */
241
298
  async function handleUpDataplane(options = {}) {
242
- logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
299
+ if (options.platformInstall !== true) {
300
+ logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
301
+ }
243
302
 
244
303
  const environmentKey =
245
304
  options && options.envKey
246
305
  ? String(options.envKey).trim().toLowerCase()
247
306
  : await resolveEnvironment();
248
- const { controllerUrl, authConfig } = await resolveUpDataplaneAuthContext(options, environmentKey);
307
+
308
+ const { controllerUrl, authConfig, useBootstrap } = await resolveUpDataplaneControllerContext(
309
+ options,
310
+ environmentKey
311
+ );
249
312
 
250
313
  await ensureDataplaneAppsFromTemplate();
251
- // If envOutputPath target folder does not exist, set envOutputPath to null
252
314
  validateEnvOutputPathFolderOrNull('dataplane');
253
-
254
315
  refreshUrlsLocalRegistryForCurrentProject();
255
316
 
256
317
  if (options.platformInstall !== true) {
257
318
  emitDataplaneManifestLineFromBuilder();
258
319
  }
259
320
 
260
- await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
321
+ await provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig);
261
322
 
262
- await deployDataplaneToController({ ...options, envKey: environmentKey });
263
- logger.log('');
264
-
265
- await alignDataplaneCredentialsAfterDeploy(options, controllerUrl);
323
+ await alignDataplaneCredentialsAfterDeploy(
324
+ { ...options, useBootstrap },
325
+ controllerUrl
326
+ );
266
327
 
267
328
  const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
268
329
 
@@ -275,7 +336,9 @@ async function handleUpDataplane(options = {}) {
275
336
  skipManifestMetadataLine: true
276
337
  });
277
338
 
278
- logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
339
+ if (options.platformInstall !== true) {
340
+ logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
341
+ }
279
342
  }
280
343
 
281
344
  module.exports = { handleUpDataplane, buildDataplaneImageRef };