@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
@@ -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
  /**
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Traefik v3 dynamic TLS configuration (file provider).
3
+ * Static CLI flags for certificate stores were removed in Traefik v3.
4
+ *
5
+ * @fileoverview Write traefik-tls.yaml for infra compose
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const TRAEFIK_TLS_DYNAMIC_BASENAME = 'traefik-tls.yaml';
16
+ const TRAEFIK_TLS_CONTAINER_PATH = '/etc/traefik/dynamic/tls.yaml';
17
+ const TRAEFIK_CERT_CONTAINER_PATH = '/etc/traefik/certs/tls.crt';
18
+ const TRAEFIK_KEY_CONTAINER_PATH = '/etc/traefik/certs/tls.key';
19
+
20
+ /**
21
+ * @param {string} storeName
22
+ * @returns {string}
23
+ */
24
+ function buildTraefikTlsDynamicYaml(storeName) {
25
+ const store = String(storeName || 'default').trim() || 'default';
26
+ const certLine = ` certFile: ${TRAEFIK_CERT_CONTAINER_PATH}`;
27
+ const keyLine = ` keyFile: ${TRAEFIK_KEY_CONTAINER_PATH}`;
28
+ const blocks = [
29
+ 'tls:',
30
+ ' stores:',
31
+ ' default:',
32
+ ' defaultCertificate:',
33
+ certLine,
34
+ keyLine
35
+ ];
36
+ if (store !== 'default') {
37
+ blocks.push(` ${store}:`);
38
+ blocks.push(' defaultCertificate:');
39
+ blocks.push(certLine);
40
+ blocks.push(keyLine);
41
+ }
42
+ return `${blocks.join('\n')}\n`;
43
+ }
44
+
45
+ /**
46
+ * @param {Object} traefikConfig
47
+ * @returns {boolean}
48
+ */
49
+ function traefikConfigHasTlsFiles(traefikConfig) {
50
+ return Boolean(
51
+ traefikConfig &&
52
+ traefikConfig.certStore &&
53
+ traefikConfig.certFile &&
54
+ traefikConfig.keyFile
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Write dynamic TLS config for Traefik file provider when cert store + files are set.
60
+ *
61
+ * @param {string} infraDir
62
+ * @param {Object} traefikConfig
63
+ * @returns {{ hostPath: string, containerPath: string, certContainerPath: string, keyContainerPath: string }|null}
64
+ */
65
+ function writeTraefikTlsDynamicConfig(infraDir, traefikConfig) {
66
+ if (!traefikConfigHasTlsFiles(traefikConfig)) {
67
+ return null;
68
+ }
69
+ const hostPath = path.join(infraDir, TRAEFIK_TLS_DYNAMIC_BASENAME);
70
+ const content = buildTraefikTlsDynamicYaml(traefikConfig.certStore);
71
+ fs.writeFileSync(hostPath, content, { mode: 0o600 });
72
+ return {
73
+ hostPath,
74
+ containerPath: TRAEFIK_TLS_CONTAINER_PATH,
75
+ certContainerPath: TRAEFIK_CERT_CONTAINER_PATH,
76
+ keyContainerPath: TRAEFIK_KEY_CONTAINER_PATH
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ TRAEFIK_TLS_DYNAMIC_BASENAME,
82
+ TRAEFIK_TLS_CONTAINER_PATH,
83
+ TRAEFIK_CERT_CONTAINER_PATH,
84
+ TRAEFIK_KEY_CONTAINER_PATH,
85
+ buildTraefikTlsDynamicYaml,
86
+ traefikConfigHasTlsFiles,
87
+ writeTraefikTlsDynamicConfig
88
+ };
@@ -0,0 +1,53 @@
1
+ const { formatBlockingError } = require('./cli-test-layout-chalk');
2
+ /**
3
+ * Bootstrap API utilities for install-time application provisioning.
4
+ *
5
+ * @fileoverview Bootstrap API call helpers
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const logger = require('./logger');
12
+ const { bootstrapApplication } = require('../api/applications.api');
13
+ const { formatApiError } = require('./api-error-handler');
14
+
15
+ /**
16
+ * Call bootstrap provisioning API.
17
+ * @async
18
+ * @param {string} apiUrl - Controller URL
19
+ * @param {string} encryptionKey - Shared ENCRYPTION_KEY (never log)
20
+ * @param {string} environment - Environment key
21
+ * @param {Object} registrationData - Registration payload
22
+ * @returns {Promise<Object>} Application data (credentials only when newly created)
23
+ */
24
+ async function callBootstrapApi(apiUrl, encryptionKey, environment, registrationData) {
25
+ try {
26
+ const response = await bootstrapApplication(apiUrl, environment, encryptionKey, registrationData);
27
+
28
+ if (!response.success) {
29
+ const formattedError = response.formattedError || formatApiError(response, apiUrl);
30
+ logger.error(formattedError);
31
+ logger.error(chalk.gray(`\nController URL: ${apiUrl}`));
32
+ throw new Error(formattedError);
33
+ }
34
+
35
+ const apiResponse = response.data;
36
+ if (apiResponse && apiResponse.data) {
37
+ return apiResponse.data;
38
+ }
39
+ if (apiResponse && (apiResponse.application || apiResponse.status)) {
40
+ return apiResponse;
41
+ }
42
+
43
+ logger.error(formatBlockingError('Invalid bootstrap response: missing application data'));
44
+ throw new Error('Invalid bootstrap response');
45
+ } catch (error) {
46
+ logger.error(formatBlockingError('Bootstrap API call failed'));
47
+ logger.error(chalk.gray(`Controller URL: ${apiUrl}`));
48
+ logger.error(chalk.gray(`Error: ${error.message}`));
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ module.exports = { callBootstrapApi };
@@ -136,10 +136,19 @@ async function checkContainerRunning(appName, developerId, debug = false, scopeO
136
136
  * @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
137
137
  * @param {boolean} [debug=false] - Enable debug logging
138
138
  */
139
- async function stopAndRemoveContainer(appName, developerId, debug = false, scopeOpts = null) {
139
+ async function stopAndRemoveContainer(
140
+ appName,
141
+ developerId,
142
+ debug = false,
143
+ scopeOpts = null,
144
+ opts = {}
145
+ ) {
146
+ const suppressLog = opts && opts.suppressLog === true;
140
147
  try {
141
148
  const containerName = getContainerName(appName, developerId, scopeOpts);
142
- logger.log(formatProgress(`Stopping container ${containerName}…`));
149
+ if (!suppressLog) {
150
+ logger.log(formatProgress(`Stopping container ${containerName}…`));
151
+ }
143
152
  const stopCmd = `docker stop ${containerName}`;
144
153
  if (debug) {
145
154
  logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
@@ -150,7 +159,9 @@ async function stopAndRemoveContainer(appName, developerId, debug = false, scope
150
159
  logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
151
160
  }
152
161
  await execWithDockerEnv(rmCmd);
153
- logger.log(formatSuccessLine(`Container ${containerName} stopped and removed`));
162
+ if (!suppressLog) {
163
+ logger.log(formatSuccessLine(`Container ${containerName} stopped and removed`));
164
+ }
154
165
  } catch (error) {
155
166
  if (debug) {
156
167
  logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
@@ -120,8 +120,8 @@ function coerceTriStateBool(v) {
120
120
  }
121
121
 
122
122
  /**
123
- * Whether `aifabrix run` / resolve should use Traefik-style public URL hints for this app when infra allows.
124
- * Default **false** when unset. Legacy `noProxy: true` ⇒ false; legacy `noProxy: false` with no `proxy` ⇒ true.
123
+ * Stored per-app `proxy` preference from config (may be stale when `traefik: false`).
124
+ * Use {@link getEffectiveApplicationsRunProxyHint} for runtime / display.
125
125
  *
126
126
  * @param {Object|null|undefined} userConfig - Parsed config.yaml root
127
127
  * @param {string} appKey - Application key
@@ -183,6 +183,21 @@ async function persistApplicationRunProxyFlag(appKey, proxyEnabled) {
183
183
  await config.saveConfig(cfg);
184
184
  }
185
185
 
186
+ /**
187
+ * Effective proxy hint for URL expansion and CLI display.
188
+ * Always **false** when infra Traefik is off — stored `applications.<app>.proxy` is ignored.
189
+ *
190
+ * @param {Object|null|undefined} userConfig
191
+ * @param {string} appKey
192
+ * @returns {boolean}
193
+ */
194
+ function getEffectiveApplicationsRunProxyHint(userConfig, appKey) {
195
+ if (!userConfig || userConfig.traefik !== true) {
196
+ return false;
197
+ }
198
+ return getApplicationsRunProxyHint(userConfig, appKey);
199
+ }
200
+
186
201
  /**
187
202
  * Whether declarative `url://*` expansion should treat Traefik as on (infra Traefik + per-app `proxy: true`).
188
203
  *
@@ -191,11 +206,12 @@ async function persistApplicationRunProxyFlag(appKey, proxyEnabled) {
191
206
  * @returns {boolean}
192
207
  */
193
208
  function isDeclarativeTraefikUrlsEnabled(userConfig, appKey) {
194
- return Boolean(userConfig && userConfig.traefik === true) && getApplicationsRunProxyHint(userConfig, appKey);
209
+ return getEffectiveApplicationsRunProxyHint(userConfig, appKey);
195
210
  }
196
211
 
197
212
  module.exports = {
198
213
  getApplicationsRunProxyHint,
214
+ getEffectiveApplicationsRunProxyHint,
199
215
  isApplicationsReloadDefaultOn,
200
216
  isDeclarativeTraefikUrlsEnabled,
201
217
  isPreferLocalEnvOutputPath,
@@ -144,6 +144,8 @@ function buildComposeTemplateContext(ctx) {
144
144
  dbInitEnvFile: ctx.dbInitEnvFileAbsolutePath,
145
145
  databasePasswords: ctx.databasePasswords,
146
146
  devId: ctx.idNum,
147
+ idNum: ctx.idNum,
148
+ traefikScoped: ctx.idNum > 0,
147
149
  networkName: ctx.networkName,
148
150
  containerName: ctx.containerName,
149
151
  misoEnvironment: resolveMisoEnvironment(ctx.options),
@@ -201,7 +203,12 @@ async function readDatabasePasswordsIfNeeded(readDatabasePasswords, requiresData
201
203
  * @returns {Promise<object>}
202
204
  */
203
205
  async function loadComposeHead(deps, appName, appConfig, options) {
204
- const { loadDockerComposeTemplate, resolveComposeImageOverrideString, getDeveloperIdAndNumeric, getRemoteServerForCompose } = deps;
206
+ const {
207
+ loadDockerComposeTemplate,
208
+ resolveComposeImageOverrideString,
209
+ getDeveloperIdAndNumeric,
210
+ getFrontDoorHostContextForCompose
211
+ } = deps;
205
212
  const language = appConfig.build?.language || appConfig.language || 'typescript';
206
213
  const template = loadDockerComposeTemplate(language);
207
214
  const port = options.port || appConfig.port || 3000;
@@ -209,10 +216,13 @@ async function loadComposeHead(deps, appName, appConfig, options) {
209
216
  const { devId, idNum } = await getDeveloperIdAndNumeric();
210
217
  const { scoped } = resolveScopedComposeOpts(options);
211
218
  let remoteServer = null;
212
- if (typeof getRemoteServerForCompose === 'function') {
213
- remoteServer = await getRemoteServerForCompose();
219
+ let developerRootDomain = null;
220
+ if (typeof getFrontDoorHostContextForCompose === 'function') {
221
+ const frontDoorCtx = await getFrontDoorHostContextForCompose();
222
+ remoteServer = frontDoorCtx.remoteServer;
223
+ developerRootDomain = frontDoorCtx.developerRootDomain;
214
224
  }
215
- return { template, port, imageOverride, devId, idNum, scoped, remoteServer };
225
+ return { template, port, imageOverride, devId, idNum, scoped, remoteServer, developerRootDomain };
216
226
  }
217
227
 
218
228
  /**
@@ -224,12 +234,13 @@ async function loadComposeHead(deps, appName, appConfig, options) {
224
234
  */
225
235
  function buildComposeLayouts(deps, appName, appConfig, head, options) {
226
236
  const { buildNetworkAndContainerNames, buildServiceConfig, buildVolumesConfig, buildNetworksConfig } = deps;
227
- const { port, imageOverride, devId, idNum, scoped, remoteServer } = head;
237
+ const { port, imageOverride, devId, idNum, scoped, remoteServer, developerRootDomain } = head;
228
238
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum, scoped);
229
239
  const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, {
230
240
  imageOverride,
231
241
  scopeOpts: scoped,
232
242
  remoteServer,
243
+ developerRootDomain,
233
244
  omitAppTraefikLabels: options && options.omitAppTraefikLabels === true
234
245
  });
235
246
  const volumesConfig = buildVolumesConfig(appName);