@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
@@ -35,8 +35,23 @@ 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';
44
+ /** Automation / af-server: wait for edge routing before dataplane (nginx + Traefik). */
45
+ const CONTROLLER_HEALTH_RETRY_ATTEMPTS = 30;
46
+ const CONTROLLER_HEALTH_RETRY_DELAY_MS = 2000;
47
+
48
+ /**
49
+ * @param {number} ms
50
+ * @returns {Promise<void>}
51
+ */
52
+ function sleep(ms) {
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
+ }
40
55
 
41
56
  /**
42
57
  * Check if controller is reachable (health endpoint).
@@ -72,19 +87,46 @@ function promptForControllerUrl(currentUrl) {
72
87
  }
73
88
 
74
89
  /**
75
- * Resolve controller URL and ensure it is healthy; if not, prompt once for new URL.
76
- * @returns {Promise<string>} Controller URL to use
77
- * @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
90
+ * Poll controller /health until success or attempts exhausted.
91
+ * @param {string} baseUrl - Controller base URL (no trailing slash)
92
+ * @param {{ attempts?: number, delayMs?: number }} [opts]
93
+ * @returns {Promise<boolean>}
78
94
  */
79
- async function resolveControllerUrlWithHealthCheck() {
80
- let controllerUrl = await resolveControllerUrl();
81
- controllerUrl = controllerUrl.replace(/\/+$/, '');
82
-
83
- let healthy = await isControllerHealthy(controllerUrl);
84
- if (healthy) {
85
- return controllerUrl;
95
+ async function waitForControllerHealthy(baseUrl, opts = {}) {
96
+ const attempts = opts.attempts ?? CONTROLLER_HEALTH_RETRY_ATTEMPTS;
97
+ const delayMs = opts.delayMs ?? CONTROLLER_HEALTH_RETRY_DELAY_MS;
98
+ const normalized = baseUrl.replace(/\/+$/, '');
99
+ for (let i = 0; i < attempts; i += 1) {
100
+ if (await isControllerHealthy(normalized)) {
101
+ return true;
102
+ }
103
+ if (i < attempts - 1) {
104
+ await sleep(delayMs);
105
+ }
86
106
  }
107
+ return false;
108
+ }
109
+
110
+ /**
111
+ * @param {string} controllerUrl
112
+ * @returns {string}
113
+ */
114
+ function buildControllerUnavailableMessage(controllerUrl) {
115
+ return (
116
+ `Controller at ${controllerUrl} is not responding after ${CONTROLLER_HEALTH_RETRY_ATTEMPTS} health checks ` +
117
+ `(~${Math.round((CONTROLLER_HEALTH_RETRY_ATTEMPTS * CONTROLLER_HEALTH_RETRY_DELAY_MS) / 1000)}s). ` +
118
+ 'Miso may be up on localhost before nginx/Traefik routes are ready. ' +
119
+ 'On the builder host run: sudo systemctl start aifabrix-sync-dev-traefik-nginx.service ' +
120
+ 'then retry: aifabrix up-dataplane --force'
121
+ );
122
+ }
87
123
 
124
+ /**
125
+ * Prompt for a new controller URL and verify it responds to health checks.
126
+ * @param {string} controllerUrl - URL that failed the initial health check
127
+ * @returns {Promise<string>} Verified controller URL
128
+ */
129
+ async function promptForHealthyControllerUrl(controllerUrl) {
88
130
  logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
89
131
  const newUrl = await promptForControllerUrl(controllerUrl);
90
132
  if (!newUrl) {
@@ -99,7 +141,7 @@ async function resolveControllerUrlWithHealthCheck() {
99
141
 
100
142
  await setControllerUrl(newUrl);
101
143
  const normalizedNew = newUrl.trim().replace(/\/+$/, '');
102
- healthy = await isControllerHealthy(normalizedNew);
144
+ const healthy = await isControllerHealthy(normalizedNew);
103
145
  if (!healthy) {
104
146
  throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
105
147
  }
@@ -108,6 +150,28 @@ async function resolveControllerUrlWithHealthCheck() {
108
150
  return normalizedNew;
109
151
  }
110
152
 
153
+ /**
154
+ * Resolve controller URL and ensure it is healthy; if not, prompt once for new URL (TTY only).
155
+ * @returns {Promise<string>} Controller URL to use
156
+ * @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
157
+ */
158
+ async function resolveControllerUrlWithHealthCheck() {
159
+ let controllerUrl = await resolveControllerUrl();
160
+ controllerUrl = controllerUrl.replace(/\/+$/, '');
161
+
162
+ const healthy = await waitForControllerHealthy(controllerUrl);
163
+ if (healthy) {
164
+ return controllerUrl;
165
+ }
166
+
167
+ const isInteractive = Boolean(process.stdin && process.stdin.isTTY);
168
+ if (!isInteractive) {
169
+ throw new Error(buildControllerUnavailableMessage(controllerUrl));
170
+ }
171
+
172
+ return promptForHealthyControllerUrl(controllerUrl);
173
+ }
174
+
111
175
  async function resolveUpDataplaneAuthContext(options, environmentKey) {
112
176
  if (options.skipInfraCheck !== true) {
113
177
  await assertDevInfraUp();
@@ -120,7 +184,9 @@ async function resolveUpDataplaneAuthContext(options, environmentKey) {
120
184
  if (options.allowNonDev !== true) {
121
185
  assertDevEnvironmentForDataplane(cfg);
122
186
  }
123
- logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
187
+ if (options.platformInstall !== true) {
188
+ logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
189
+ }
124
190
  return { controllerUrl, authConfig };
125
191
  }
126
192
 
@@ -223,6 +289,57 @@ async function computeDataplaneRunOverrides(options) {
223
289
  return { runEnvKey, portOverride };
224
290
  }
225
291
 
292
+ /**
293
+ * Resolve controller URL and auth for bootstrap vs device-login paths.
294
+ * @async
295
+ * @param {Object} options
296
+ * @param {string} environmentKey
297
+ * @returns {Promise<{ controllerUrl: string, authConfig: Object|null, useBootstrap: boolean }>}
298
+ */
299
+ async function resolveUpDataplaneControllerContext(options, environmentKey) {
300
+ const useBootstrap = await canUseBootstrapProvision(options);
301
+ if (useBootstrap) {
302
+ if (options.skipInfraCheck !== true) {
303
+ await assertDevInfraUp();
304
+ }
305
+ const controllerUrl = await resolveControllerUrlWithHealthCheck();
306
+ const cfg = await config.getConfig();
307
+ if (options.allowNonDev !== true) {
308
+ assertDevEnvironmentForDataplane(cfg);
309
+ }
310
+ if (options.platformInstall !== true) {
311
+ logger.log(formatSuccessLine('Using bootstrap provision (no device login)'));
312
+ }
313
+ return { controllerUrl, authConfig: null, useBootstrap: true };
314
+ }
315
+
316
+ const authContext = await resolveUpDataplaneAuthContext(options, environmentKey);
317
+ return {
318
+ controllerUrl: authContext.controllerUrl,
319
+ authConfig: authContext.authConfig,
320
+ useBootstrap: false
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Register/bootstrap, deploy manifest, align credentials.
326
+ * @async
327
+ */
328
+ async function provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig) {
329
+ if (useBootstrap) {
330
+ await bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRef);
331
+ } else {
332
+ await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
333
+ }
334
+
335
+ await deployDataplaneToController({
336
+ ...options,
337
+ envKey: environmentKey,
338
+ ...(useBootstrap ? { deploymentAuth: 'client-credentials' } : {})
339
+ });
340
+ logger.log('');
341
+ }
342
+
226
343
  /**
227
344
  * Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
228
345
  * register or rotate (if already registered), deploy (send manifest to controller),
@@ -239,43 +356,57 @@ async function computeDataplaneRunOverrides(options) {
239
356
  * @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
240
357
  */
241
358
  async function handleUpDataplane(options = {}) {
242
- logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
359
+ if (options.platformInstall !== true) {
360
+ logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
361
+ }
243
362
 
244
363
  const environmentKey =
245
364
  options && options.envKey
246
365
  ? String(options.envKey).trim().toLowerCase()
247
366
  : await resolveEnvironment();
248
- const { controllerUrl, authConfig } = await resolveUpDataplaneAuthContext(options, environmentKey);
367
+
368
+ const { controllerUrl, authConfig, useBootstrap } = await resolveUpDataplaneControllerContext(
369
+ options,
370
+ environmentKey
371
+ );
249
372
 
250
373
  await ensureDataplaneAppsFromTemplate();
251
- // If envOutputPath target folder does not exist, set envOutputPath to null
252
374
  validateEnvOutputPathFolderOrNull('dataplane');
253
-
254
375
  refreshUrlsLocalRegistryForCurrentProject();
255
376
 
256
377
  if (options.platformInstall !== true) {
257
378
  emitDataplaneManifestLineFromBuilder();
258
379
  }
259
380
 
260
- await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
261
-
262
- await deployDataplaneToController({ ...options, envKey: environmentKey });
263
- logger.log('');
381
+ await provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig);
264
382
 
265
- await alignDataplaneCredentialsAfterDeploy(options, controllerUrl);
383
+ await alignDataplaneCredentialsAfterDeploy(
384
+ { ...options, useBootstrap },
385
+ controllerUrl
386
+ );
266
387
 
267
388
  const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
268
389
 
269
- await app.runApp('dataplane', {
390
+ const runAppOpts = {
270
391
  skipEnvOutputPath: true,
271
392
  env: runEnvKey,
272
393
  port: portOverride,
273
394
  registry: options.registry || undefined,
274
395
  base: options.base !== false,
275
396
  skipManifestMetadataLine: true
276
- });
397
+ };
398
+ if (options.platformInstall === true) {
399
+ if (typeof options.onPollText === 'function') {
400
+ runAppOpts.onPollText = options.onPollText;
401
+ } else {
402
+ runAppOpts.silentPoll = true;
403
+ }
404
+ }
405
+ await app.runApp('dataplane', runAppOpts);
277
406
 
278
- logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
407
+ if (options.platformInstall !== true) {
408
+ logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
409
+ }
279
410
  }
280
411
 
281
412
  module.exports = { handleUpDataplane, buildDataplaneImageRef };
@@ -57,11 +57,15 @@ async function runMisoApps(options) {
57
57
  base: useBaseImage,
58
58
  skipManifestMetadataLine: true
59
59
  };
60
- const keycloakRunOpts = { ...common };
60
+ const platformPollOpts =
61
+ options.platformInstall === true
62
+ ? { silentPoll: true }
63
+ : {};
64
+ const keycloakRunOpts = { ...common, ...platformPollOpts };
61
65
  if (imageMap.keycloak) {
62
66
  keycloakRunOpts.image = imageMap.keycloak;
63
67
  }
64
- const misoRunOpts = { ...common };
68
+ const misoRunOpts = { ...common, ...platformPollOpts };
65
69
  if (imageMap['miso-controller']) {
66
70
  misoRunOpts.image = imageMap['miso-controller'];
67
71
  }
@@ -14,6 +14,30 @@ const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-
14
14
  const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
15
15
  const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
16
16
  const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
17
+
18
+ /**
19
+ * @param {() => Promise<*>} getter
20
+ * @param {*} [fallback]
21
+ * @returns {Promise<*>}
22
+ */
23
+ async function tryConfigValue(getter, fallback = null) {
24
+ try {
25
+ return await getter();
26
+ } catch {
27
+ return fallback;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {() => Promise<*>} getter
33
+ * @returns {Promise<string|null>}
34
+ */
35
+ async function tryConfigTrimmedString(getter) {
36
+ const value = await tryConfigValue(getter, null);
37
+ const trimmed = value !== null && value !== undefined ? String(value).trim() : '';
38
+ return trimmed || null;
39
+ }
40
+
17
41
  /**
18
42
  * Config inputs for declarative url:// expansion.
19
43
  * @param {string} appPath
@@ -21,30 +45,16 @@ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-reg
21
45
  */
22
46
  async function loadDeclarativeUrlExpandInputs(appPath) {
23
47
  const userCfg = await config.getConfig();
24
- let remoteServer = null;
25
- try {
26
- const rs = await config.getRemoteServer();
27
- if (rs && String(rs).trim()) {
28
- remoteServer = String(rs).trim();
29
- }
30
- } catch {
31
- remoteServer = null;
32
- }
33
- let developerIdRaw = null;
34
- try {
35
- developerIdRaw = await config.getDeveloperId();
36
- } catch {
37
- developerIdRaw = null;
38
- }
39
- let infraTlsEnabled = false;
40
- try {
41
- infraTlsEnabled = await config.getTlsEnabled();
42
- } catch {
43
- infraTlsEnabled = false;
44
- }
48
+ const [remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled] = await Promise.all([
49
+ tryConfigTrimmedString(() => config.getRemoteServer()),
50
+ tryConfigTrimmedString(() => config.getDeveloperRootDomain()),
51
+ tryConfigValue(() => config.getDeveloperId(), null),
52
+ tryConfigValue(() => config.getTlsEnabled(), false)
53
+ ]);
45
54
  return {
46
55
  userCfg,
47
56
  remoteServer,
57
+ developerRootDomain,
48
58
  developerIdRaw,
49
59
  infraTlsEnabled,
50
60
  appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
@@ -60,7 +70,7 @@ async function loadDeclarativeUrlExpandInputs(appPath) {
60
70
  * @returns {Object}
61
71
  */
62
72
  function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, inputs) {
63
- const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
73
+ const { userCfg, remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
64
74
  let projectRoot = null;
65
75
  try {
66
76
  const r = pathsUtil.getProjectRoot();
@@ -77,6 +87,7 @@ function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, envi
77
87
  useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
78
88
  appEnvironmentScopedResources: appScopedFlag,
79
89
  remoteServer,
90
+ developerRootDomain,
80
91
  developerIdRaw,
81
92
  infraTlsEnabled,
82
93
  userCfg,
@@ -136,12 +147,13 @@ async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variab
136
147
  if (envOnly || !variablesPath) {
137
148
  return resolved;
138
149
  }
139
- const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
150
+ const { userCfg, remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled, appScopedFlag } =
140
151
  await loadDeclarativeUrlExpandInputs(appPath);
141
152
  resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
142
153
  const ctx = buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, {
143
154
  userCfg,
144
155
  remoteServer,
156
+ developerRootDomain,
145
157
  developerIdRaw,
146
158
  infraTlsEnabled,
147
159
  appScopedFlag
@@ -61,6 +61,46 @@ function flagsForSetupPlatformMode(mode) {
61
61
  };
62
62
  }
63
63
 
64
+ /** Platform app keys (setup does not modify `applications.<key>.proxy`; use `aifabrix run --proxy` / `--no-proxy`). */
65
+ const PLATFORM_APPLICATION_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane']);
66
+
67
+ /**
68
+ * Apply setup platform mode to config.yaml infra topology (`traefik`, `tlsEnabled`, env scope).
69
+ * Does **not** change per-app `applications.<key>.proxy` — that is owned by `aifabrix run`.
70
+ *
71
+ * @param {Object} cfg - Mutable config root
72
+ * @param {unknown} mode
73
+ * @returns {Object} Same cfg reference
74
+ */
75
+ function applyPlatformTopologyFlagsToConfig(cfg, mode) {
76
+ const normalized = normalizeSetupPlatformMode(mode);
77
+ const derived = flagsForSetupPlatformMode(normalized);
78
+ cfg.setupPlatformMode = normalized;
79
+ cfg.traefik = derived.traefik;
80
+ cfg.tlsEnabled = derived.tlsEnabled;
81
+ cfg.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
82
+ return cfg;
83
+ }
84
+
85
+ /**
86
+ * True when persisted traefik/tls/env-scope flags disagree with setupPlatformMode.
87
+ *
88
+ * @param {Object} cfg
89
+ * @returns {boolean}
90
+ */
91
+ function platformTopologyNeedsReconcile(cfg) {
92
+ if (!cfg || typeof cfg !== 'object' || !cfg.setupPlatformMode) {
93
+ return false;
94
+ }
95
+ const expected = flagsForSetupPlatformMode(cfg.setupPlatformMode || SETUP_PLATFORM_MODE.SINGLE);
96
+ if (Boolean(cfg.traefik) !== Boolean(expected.traefik)) return true;
97
+ if (Boolean(cfg.tlsEnabled) !== Boolean(expected.tlsEnabled)) return true;
98
+ if (Boolean(cfg.useEnvironmentScopedResources) !== Boolean(expected.useEnvironmentScopedResources)) {
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+
64
104
  /**
65
105
  * Interactive setup menu choices (full platform hidden until multi-env path ships).
66
106
  * Traefik front door requires `remote-server` (from `aifabrix dev init`); without it only direct ports is offered.
@@ -90,8 +130,11 @@ function getSetupPlatformModePromptChoices(opts = {}) {
90
130
  module.exports = {
91
131
  SETUP_PLATFORM_MODE,
92
132
  SETUP_PLATFORM_MODE_CLI_VALUES,
133
+ PLATFORM_APPLICATION_KEYS,
93
134
  normalizeSetupPlatformMode,
94
135
  flagsForSetupPlatformMode,
136
+ applyPlatformTopologyFlagsToConfig,
137
+ platformTopologyNeedsReconcile,
95
138
  getSetupPlatformModePromptChoices
96
139
  };
97
140
 
@@ -18,15 +18,37 @@ function isTerminalStatus(status) {
18
18
  return status === 'completed' || status === 'failed' || status === 'cancelled';
19
19
  }
20
20
 
21
+ /**
22
+ * True when pipeline calls should authenticate with a token header only (Bearer or x-client-token).
23
+ * @param {Object} authConfig
24
+ * @returns {boolean}
25
+ */
26
+ function isTokenOnlyPipelineAuth(authConfig) {
27
+ if (!authConfig || !authConfig.token) {
28
+ return false;
29
+ }
30
+ if (authConfig.clientId || authConfig.clientSecret) {
31
+ return false;
32
+ }
33
+ return authConfig.type === 'bearer' || authConfig.type === 'client-token';
34
+ }
35
+
21
36
  /**
22
37
  * Convert authConfig to pipeline auth config format
23
38
  * @param {Object} authConfig - Authentication configuration
24
39
  * @returns {Object} Pipeline auth config
25
40
  */
26
41
  function convertToPipelineAuthConfig(authConfig) {
27
- return authConfig.type === 'bearer'
28
- ? { type: 'bearer', token: authConfig.token }
29
- : { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
42
+ if (isTokenOnlyPipelineAuth(authConfig)) {
43
+ return authConfig.type === 'client-token'
44
+ ? { type: 'client-token', token: authConfig.token }
45
+ : { type: 'bearer', token: authConfig.token };
46
+ }
47
+ return {
48
+ type: 'client-credentials',
49
+ clientId: authConfig.clientId,
50
+ clientSecret: authConfig.clientSecret
51
+ };
30
52
  }
31
53
 
32
54
  /**
@@ -118,6 +140,7 @@ async function processDeploymentStatusResponse(
118
140
 
119
141
  module.exports = {
120
142
  isTerminalStatus,
143
+ isTokenOnlyPipelineAuth,
121
144
  convertToPipelineAuthConfig,
122
145
  handleDeploymentStatusError,
123
146
  extractDeploymentData,
@@ -20,7 +20,8 @@ const { getAuthUser } = require('../api/auth.api');
20
20
  const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
21
21
  const {
22
22
  convertToPipelineAuthConfig,
23
- processDeploymentStatusResponse
23
+ processDeploymentStatusResponse,
24
+ isTokenOnlyPipelineAuth
24
25
  } = require('./deployer-status');
25
26
  const { resolvePollIntervalFromController } = require('./poll-interval');
26
27
  const { createDeployPollHandlers } = require('./deploy-poll-ui');
@@ -54,8 +55,8 @@ async function buildValidationData(manifest, validatedEnvKey, authConfig, option
54
55
  manifest?.repository?.repositoryUrl ||
55
56
  `https://github.com/aifabrix/${manifest.key}`;
56
57
 
57
- if (authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId) {
58
- const pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
58
+ if (isTokenOnlyPipelineAuth(authConfig)) {
59
+ const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
59
60
  const validationData = {
60
61
  clientId: manifest.key,
61
62
  repositoryUrl,
@@ -181,15 +182,7 @@ function buildDeploymentData(validateToken, authConfig, options) {
181
182
  imageTag: imageTag
182
183
  };
183
184
 
184
- const pipelineAuthConfig = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId
185
- ? { type: 'bearer', token: authConfig.token }
186
- : {
187
- type: 'client-credentials',
188
- clientId: authConfig.clientId,
189
- clientSecret: authConfig.clientSecret
190
- };
191
-
192
- return { deployData, pipelineAuthConfig };
185
+ return { deployData, pipelineAuthConfig: convertToPipelineAuthConfig(authConfig) };
193
186
  }
194
187
 
195
188
  /**
@@ -254,8 +247,8 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
254
247
  const validatedEnvKey = validateEnvironmentKey(envKey);
255
248
  const maxRetries = options.maxRetries || 3;
256
249
 
257
- const useBearerOnly = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId;
258
- if (!useBearerOnly) {
250
+ const useTokenOnlyAuth = isTokenOnlyPipelineAuth(authConfig);
251
+ if (!useTokenOnlyAuth) {
259
252
  validateDeploymentCredentials(authConfig);
260
253
  }
261
254
 
@@ -16,6 +16,7 @@ const {
16
16
  } = require('../utils/cli-test-layout-chalk');
17
17
  const logger = require('../utils/logger');
18
18
  const { generateExternalReadmeContent } = require('../utils/external-readme');
19
+ const { getAuthCredentialTemplate } = require('../utils/authentication-shape');
19
20
 
20
21
  /**
21
22
  * Validates system type from downloaded application
@@ -159,7 +160,15 @@ function generateReadme(systemKey, application, dataSources) {
159
160
  };
160
161
  });
161
162
 
162
- const authType = application.authentication?.type || application.authentication?.method;
163
+ const auth = application.authentication;
164
+ let authType = auth?.authType;
165
+ if (auth?.authType === 'credential') {
166
+ authType = getAuthCredentialTemplate(auth)?.method || authType;
167
+ } else if (auth?.authType === 'apiProvider') {
168
+ authType = auth.apiProvider?.apiProviderKey || 'apiProvider';
169
+ } else {
170
+ authType = auth?.type || auth?.method || authType;
171
+ }
163
172
  return generateExternalReadmeContent({
164
173
  appName: systemKey,
165
174
  systemKey,
@@ -11,6 +11,9 @@
11
11
  const fs = require('fs');
12
12
  const handlebars = require('handlebars');
13
13
  const path = require('path');
14
+ const {
15
+ writeTraefikTlsDynamicConfig
16
+ } = require('./traefik-tls-dynamic');
14
17
 
15
18
  /**
16
19
  * Normalize a host path for Docker Compose bind mounts. Docker Desktop sends
@@ -91,6 +94,40 @@ function validateTraefikConfig(traefikConfig) {
91
94
  return { valid: errors.length === 0, errors };
92
95
  }
93
96
 
97
+ /**
98
+ * Normalize Traefik config for compose template (bind mounts + dynamic TLS file).
99
+ *
100
+ * @param {string} infraDir
101
+ * @param {Object|boolean} traefikConfig
102
+ * @returns {Object|boolean}
103
+ */
104
+ function buildTraefikComposeContext(infraDir, traefikConfig) {
105
+ if (!traefikConfig || typeof traefikConfig !== 'object') {
106
+ return traefikConfig;
107
+ }
108
+ const traefikForCompose = {
109
+ ...traefikConfig,
110
+ trustForwardedHeaders: !!traefikConfig.trustForwardedHeaders,
111
+ ...(traefikConfig.certFile
112
+ ? { certFile: toDockerBindMountSource(traefikConfig.certFile) }
113
+ : {}),
114
+ ...(traefikConfig.keyFile
115
+ ? { keyFile: toDockerBindMountSource(traefikConfig.keyFile) }
116
+ : {})
117
+ };
118
+ const tlsDynamic = writeTraefikTlsDynamicConfig(infraDir, traefikConfig);
119
+ if (!tlsDynamic) {
120
+ return traefikForCompose;
121
+ }
122
+ return {
123
+ ...traefikForCompose,
124
+ tlsDynamicBind: toDockerBindMountSource(tlsDynamic.hostPath),
125
+ tlsDynamicContainerPath: tlsDynamic.containerPath,
126
+ certContainerPath: tlsDynamic.certContainerPath,
127
+ keyContainerPath: tlsDynamic.keyContainerPath
128
+ };
129
+ }
130
+
94
131
  /**
95
132
  * Generate docker-compose file from template
96
133
  * @param {string} templatePath - Path to compose template
@@ -129,21 +166,12 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
129
166
  const redisCommanderConfig = options.redisCommander && typeof options.redisCommander.enabled === 'boolean'
130
167
  ? options.redisCommander
131
168
  : { enabled: true };
132
- const traefikForCompose = traefikConfig && typeof traefikConfig === 'object'
133
- ? {
134
- ...traefikConfig,
135
- trustForwardedHeaders: !!traefikConfig.trustForwardedHeaders,
136
- ...(traefikConfig.certFile
137
- ? { certFile: toDockerBindMountSource(traefikConfig.certFile) }
138
- : {}),
139
- ...(traefikConfig.keyFile
140
- ? { keyFile: toDockerBindMountSource(traefikConfig.keyFile) }
141
- : {})
142
- }
143
- : traefikConfig;
169
+ const traefikForCompose = buildTraefikComposeContext(infraDir, traefikConfig);
144
170
  const initScriptsBind = toDockerBindMountSource(path.join(infraDir, 'init-scripts'));
145
171
  const composeContent = template({
146
172
  devId: devId,
173
+ idNum,
174
+ traefikScoped: idNum > 0,
147
175
  postgresPort: ports.postgres,
148
176
  redisPort: ports.redis,
149
177
  pgadminPort: ports.pgadmin,
@@ -117,10 +117,12 @@ async function waitForServices(devId = null, opts = {}) {
117
117
  const maxAttempts = 30;
118
118
  const delay = 2000; // 2 seconds
119
119
  const { pgadmin = true, redisCommander = true, traefik = false } = opts;
120
+ let lastHealth = {};
120
121
 
121
122
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
122
123
  const health = await checkInfraHealth(devId, { pgadmin, redisCommander, traefik });
123
- const allHealthy = Object.values(health).every(status => status === 'healthy');
124
+ lastHealth = health;
125
+ const allHealthy = Object.values(health).every((status) => status === 'healthy');
124
126
 
125
127
  if (allHealthy) {
126
128
  return;
@@ -128,11 +130,21 @@ async function waitForServices(devId = null, opts = {}) {
128
130
 
129
131
  if (attempt < maxAttempts) {
130
132
  logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
131
- await new Promise(resolve => setTimeout(resolve, delay));
133
+ await new Promise((resolve) => setTimeout(resolve, delay));
132
134
  }
133
135
  }
134
136
 
135
- throw new Error('Services failed to become healthy within timeout period');
137
+ const timeoutSec = (maxAttempts * delay) / 1000;
138
+ const unhealthy = Object.entries(lastHealth)
139
+ .filter(([, status]) => status !== 'healthy')
140
+ .map(([name, status]) => `${name}=${status}`)
141
+ .join(', ');
142
+ const detail = unhealthy || 'unknown';
143
+ throw new Error(
144
+ `Infrastructure services did not become healthy within ${timeoutSec}s. ` +
145
+ `Unhealthy: ${detail}. ` +
146
+ 'Check container logs (e.g. `docker ps`, `aifabrix doctor`, or `aifabrix logs` for platform apps).'
147
+ );
136
148
  }
137
149
 
138
150
  /**