@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
@@ -116,7 +116,7 @@ function buildImageConfig(config, appName, imageOverride) {
116
116
  * @param {string|null|undefined} [readinessSuffix] - Path after vdir (default /health/ready or healthCheck.readinessPathSuffix)
117
117
  * @returns {string} Absolute path starting with /
118
118
  */
119
- function computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, readinessSuffix) {
119
+ function computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, readinessSuffix, developerRootDomain) {
120
120
  const hc = config.healthCheck || {};
121
121
  let suffix = '/health/ready';
122
122
  if (
@@ -128,7 +128,7 @@ function computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, r
128
128
  } else if (typeof hc.readinessPathSuffix === 'string' && hc.readinessPathSuffix.startsWith('/')) {
129
129
  suffix = hc.readinessPathSuffix;
130
130
  }
131
- const traefik = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer);
131
+ const traefik = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, developerRootDomain);
132
132
  if (traefik.enabled === true) {
133
133
  const p = traefik.path;
134
134
  if (!p || p === '/') {
@@ -158,7 +158,7 @@ function computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, r
158
158
  * @param {{ skipVdirMergeWhenPathIsBareHealth?: boolean }} [opts] - When `skipVdirMergeWhenPathIsBareHealth`, path `/health` is left as-is (in-container probe; ingress strips vdir for miso/dataplane). Deployment JSON health paths do **not** use this helper — see `buildHealthCheck` in `builders.js` (YAML paths only).
159
159
  * @returns {string} Absolute path starting with /
160
160
  */
161
- function resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remoteServer, opts) {
161
+ function resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remoteServer, opts, developerRootDomain) {
162
162
  const hc = config.healthCheck || {};
163
163
  const raw = typeof hc.path === 'string' && hc.path.startsWith('/') ? hc.path : '/health';
164
164
  const fd = config.frontDoorRouting;
@@ -168,7 +168,7 @@ function resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remot
168
168
  if (opts && opts.skipVdirMergeWhenPathIsBareHealth === true && raw === '/health') {
169
169
  return raw;
170
170
  }
171
- const traefik = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer);
171
+ const traefik = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, developerRootDomain);
172
172
  if (traefik.enabled !== true) {
173
173
  return raw;
174
174
  }
@@ -178,7 +178,7 @@ function resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remot
178
178
  ? ''
179
179
  : String(vdirRaw).replace(/\/+$/, '');
180
180
  if (!vdir) {
181
- return computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, raw);
181
+ return computeAlignedHealthCheckPath(config, devId, scopeOpts, remoteServer, raw, developerRootDomain);
182
182
  }
183
183
  if (raw === vdir || raw.startsWith(`${vdir}/`)) {
184
184
  return raw;
@@ -192,13 +192,14 @@ function resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remot
192
192
  * @param {string|number} devId - Developer id
193
193
  * @param {Object|null} scopeOpts - Env-scoped compose options
194
194
  * @param {string|null|undefined} remoteServer - Remote server URL
195
+ * @param {string|null|undefined} [developerRootDomain] - App hostname root
195
196
  * @returns {Object} Health check configuration
196
197
  */
197
- function buildHealthCheckConfig(config, devId, scopeOpts, remoteServer) {
198
+ function buildHealthCheckConfig(config, devId, scopeOpts, remoteServer, developerRootDomain) {
198
199
  const hc = config.healthCheck || {};
199
200
  const path = resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remoteServer, {
200
201
  skipVdirMergeWhenPathIsBareHealth: true
201
- });
202
+ }, developerRootDomain);
202
203
  return {
203
204
  path,
204
205
  interval: hc.interval || 30,
@@ -230,10 +231,11 @@ function buildDevUsername(devId) {
230
231
  * @param {Object|null} scopeOpts - Env-scoped Traefik path
231
232
  * @param {string|null|undefined} remoteServer - From config (for ${REMOTE_HOST})
232
233
  * @param {string|undefined} [resolvedHealthPathForStrip] - Compose health path; when omitted, derived via resolveHealthCheckPathWithFrontDoorVdir (same as probe)
234
+ * @param {string|null|undefined} [developerRootDomain] - App hostname root from settings
233
235
  * @returns {Object} Traefik configuration object
234
236
  */
235
- function buildTraefikConfig(config, devId, scopeOpts = null, remoteServer = null, resolvedHealthPathForStrip) {
236
- const base = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer);
237
+ function buildTraefikConfig(config, devId, scopeOpts = null, remoteServer = null, resolvedHealthPathForStrip, developerRootDomain) {
238
+ const base = buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, developerRootDomain);
237
239
  if (!base.enabled) {
238
240
  return { enabled: false };
239
241
  }
@@ -242,7 +244,7 @@ function buildTraefikConfig(config, devId, scopeOpts = null, remoteServer = null
242
244
  ? String(resolvedHealthPathForStrip)
243
245
  : resolveHealthCheckPathWithFrontDoorVdir(config, devId, scopeOpts, remoteServer, {
244
246
  skipVdirMergeWhenPathIsBareHealth: true
245
- });
247
+ }, developerRootDomain);
246
248
  const devNum = parseDeveloperIdNum(devId);
247
249
  // Shared builder host: nginx terminates TLS on :443 and proxies HTTP to per-dev Traefik :80 (e.g. 280).
248
250
  // Traefik must only attach routers to `web` for developer-id > 0; `websecure`+tls labels without
@@ -282,6 +284,7 @@ function buildRequiresConfig(config) {
282
284
  * @param {string} [runExtras.imageOverride] - Full image reference for run (e.g. from --image)
283
285
  * @param {Object|null} [runExtras.scopeOpts] - Traefik / scoped compose options
284
286
  * @param {string|null|undefined} [runExtras.remoteServer] - For ${REMOTE_HOST} in frontDoorRouting.host
287
+ * @param {string|null|undefined} [runExtras.developerRootDomain] - App hostname root (overrides remote-server host)
285
288
  * @param {boolean} [runExtras.omitAppTraefikLabels] - When true (user config `traefik: false`), omit Traefik labels and BASE_PATH env from compose even if application.yaml enables frontDoorRouting
286
289
  * @returns {Object} Service configuration
287
290
  */
@@ -290,6 +293,7 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
290
293
  imageOverride = null,
291
294
  scopeOpts = null,
292
295
  remoteServer = null,
296
+ developerRootDomain = null,
293
297
  omitAppTraefikLabels = false
294
298
  } = runExtras;
295
299
  const containerPortValue = getContainerPort(config, 3000);
@@ -303,7 +307,7 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
303
307
  ? composeTraefikServiceKey(appName, scopeOpts.runEnvKey)
304
308
  : null;
305
309
  const scopeForHealthAndTraefik = useTraefikScope ? scopeOpts : null;
306
- const healthCheck = buildHealthCheckConfig(config, devId, scopeForHealthAndTraefik, remoteServer);
310
+ const healthCheck = buildHealthCheckConfig(config, devId, scopeForHealthAndTraefik, remoteServer, developerRootDomain);
307
311
  return {
308
312
  app: buildAppConfig(appName, config, composeAppKey),
309
313
  image: buildImageConfig(config, appName, imageOverride),
@@ -319,7 +323,8 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
319
323
  devId,
320
324
  scopeForHealthAndTraefik,
321
325
  remoteServer,
322
- healthCheck.path
326
+ healthCheck.path,
327
+ developerRootDomain
323
328
  ),
324
329
  ...buildRequiresConfig(config)
325
330
  };
@@ -379,20 +384,34 @@ function buildNetworkAndContainerNames(appName, devId, idNum, scopeOpts = null)
379
384
  }
380
385
 
381
386
  const { createGenerateDockerCompose } = require('./compose-generate-docker-compose');
382
- async function getRemoteServerForCompose() {
387
+ async function getFrontDoorHostContextForCompose() {
383
388
  try {
384
389
  const rs = await config.getRemoteServer();
385
- return rs && String(rs).trim() ? String(rs).trim() : null;
390
+ const remoteServer = rs && String(rs).trim() ? String(rs).trim() : null;
391
+ let developerRootDomain = null;
392
+ try {
393
+ const root = await config.getDeveloperRootDomain();
394
+ developerRootDomain = root && String(root).trim() ? String(root).trim() : null;
395
+ } catch {
396
+ developerRootDomain = null;
397
+ }
398
+ return { remoteServer, developerRootDomain };
386
399
  } catch {
387
- return null;
400
+ return { remoteServer: null, developerRootDomain: null };
388
401
  }
389
402
  }
390
403
 
404
+ async function getRemoteServerForCompose() {
405
+ const ctx = await getFrontDoorHostContextForCompose();
406
+ return ctx.remoteServer;
407
+ }
408
+
391
409
  const generateDockerCompose = createGenerateDockerCompose({
392
410
  loadDockerComposeTemplate,
393
411
  resolveComposeImageOverrideString,
394
412
  getDeveloperIdAndNumeric,
395
413
  getRemoteServerForCompose,
414
+ getFrontDoorHostContextForCompose,
396
415
  buildNetworkAndContainerNames,
397
416
  buildServiceConfig,
398
417
  buildVolumesConfig,
@@ -10,6 +10,10 @@ const {
10
10
  buildEnvScopedTraefikPath
11
11
  } = require('./environment-scoped-resources');
12
12
  const { parseDeveloperIdNum } = require('./declarative-url-ports');
13
+ const {
14
+ resolveRemoteHostForFrontDoor,
15
+ hasFrontDoorRemoteHostContext
16
+ } = require('./remote-host-for-front-door');
13
17
 
14
18
  /**
15
19
  * Derives base path from routing pattern by removing trailing wildcards
@@ -66,21 +70,15 @@ function buildDevUsernameForFrontDoorHost(devId) {
66
70
  * @param {string} template
67
71
  * @param {string|number|null|undefined} developerIdRaw
68
72
  * @param {string|null|undefined} remoteServer
73
+ * @param {string|null|undefined} [developerRootDomain] - App hostname root (overrides remote-server hostname for ${REMOTE_HOST})
69
74
  * @returns {string}
70
75
  */
71
- function expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer) {
76
+ function expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer, developerRootDomain) {
72
77
  let t = String(template || '');
73
78
  t = t.replace(/\$\{DEV_USERNAME\}\$\{REMOTE_HOST\}/g, '${DEV_USERNAME}.${REMOTE_HOST}');
74
79
  const devU = buildDevUsernameForFrontDoorHost(developerIdRaw);
75
80
  t = t.replace(/\$\{DEV_USERNAME\}/g, devU);
76
- let remoteHost = '';
77
- try {
78
- if (remoteServer && String(remoteServer).trim()) {
79
- remoteHost = new URL(String(remoteServer).trim()).hostname;
80
- }
81
- } catch {
82
- remoteHost = '';
83
- }
81
+ const remoteHost = resolveRemoteHostForFrontDoor({ remoteServer, developerRootDomain });
84
82
  t = t.replace(/\$\{REMOTE_HOST\}/g, remoteHost);
85
83
  t = t.replace(/^\.+/g, '').replace(/\.{2,}/g, '.').replace(/\.+$/g, '').trim();
86
84
  return t;
@@ -90,13 +88,13 @@ function templateUsesRemoteHostPlaceholder(hostTemplate) {
90
88
  return /\$\{REMOTE_HOST\}/.test(String(hostTemplate || ''));
91
89
  }
92
90
 
93
- function resolveTraefikRouterHost(hostTemplate, developerIdRaw, remoteServer) {
91
+ function resolveTraefikRouterHost(hostTemplate, developerIdRaw, remoteServer, developerRootDomain) {
94
92
  const usesRemote = templateUsesRemoteHostPlaceholder(hostTemplate);
95
- const hasRemote = Boolean(remoteServer && String(remoteServer).trim());
93
+ const hasRemote = hasFrontDoorRemoteHostContext({ remoteServer, developerRootDomain });
96
94
  if (usesRemote && !hasRemote) {
97
95
  return 'localhost';
98
96
  }
99
- return expandFrontDoorHostPlaceholders(hostTemplate, developerIdRaw, remoteServer);
97
+ return expandFrontDoorHostPlaceholders(hostTemplate, developerIdRaw, remoteServer, developerRootDomain);
100
98
  }
101
99
 
102
100
  /**
@@ -108,9 +106,10 @@ function resolveTraefikRouterHost(hostTemplate, developerIdRaw, remoteServer) {
108
106
  * @param {string|number} devId - Developer id for host expansion
109
107
  * @param {Object|null} scopeOpts - Env-scoped Traefik path (effectiveEnvironmentScopedResources, runEnvKey)
110
108
  * @param {string|null|undefined} remoteServer - For ${REMOTE_HOST}
109
+ * @param {string|null|undefined} [developerRootDomain] - App hostname root from settings
111
110
  * @returns {{ enabled: false } | { enabled: true, host: string, path: string, tls: boolean, certStore: string|null }}
112
111
  */
113
- function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer) {
112
+ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, developerRootDomain) {
114
113
  const frontDoor = config.frontDoorRouting;
115
114
  if (!frontDoor || frontDoor.enabled !== true) {
116
115
  return { enabled: false };
@@ -118,7 +117,7 @@ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer) {
118
117
  if (!frontDoor.host || typeof frontDoor.host !== 'string') {
119
118
  throw new Error('frontDoorRouting.host is required when frontDoorRouting.enabled is true');
120
119
  }
121
- const host = resolveTraefikRouterHost(frontDoor.host, devId, remoteServer);
120
+ const host = resolveTraefikRouterHost(frontDoor.host, devId, remoteServer, developerRootDomain);
122
121
  const basePath = derivePathFromPattern(frontDoor.pattern);
123
122
  let pathOut = basePath;
124
123
  if (
@@ -22,6 +22,7 @@ const SETTINGS_RESPONSE_KEYS = [
22
22
  'aifabrix-env-config',
23
23
  'remote-server',
24
24
  'docker-endpoint',
25
+ 'developer-root-domain',
25
26
  'docker-tls-skip-verify',
26
27
  'sync-ssh-user',
27
28
  'sync-ssh-host'
@@ -290,6 +291,9 @@ function createRemoteConfigGetters(getConfigFn) {
290
291
  },
291
292
  async getSyncSshHost() {
292
293
  return getPathConfig(getConfigFn, 'sync-ssh-host');
294
+ },
295
+ async getDeveloperRootDomain() {
296
+ return getPathConfig(getConfigFn, 'developer-root-domain');
293
297
  }
294
298
  };
295
299
  }
@@ -424,6 +428,7 @@ module.exports = {
424
428
  resolveEnvConfigPathToAbsolute,
425
429
  SETTINGS_RESPONSE_KEYS,
426
430
  applySecretsUrlFromRemote,
427
- applySyncAndDockerFromHost
431
+ applySyncAndDockerFromHost,
432
+ hostnameFromUrl
428
433
  };
429
434
 
@@ -8,6 +8,7 @@
8
8
  const ALLOWED_DEV_GROUPS = Object.freeze([
9
9
  'admin',
10
10
  'secret-manager',
11
+ 'secret-reader',
11
12
  'developer',
12
13
  'docker'
13
14
  ]);
@@ -51,6 +51,68 @@ function collectIssuerChainPems(peerCert) {
51
51
  return pems;
52
52
  }
53
53
 
54
+ /**
55
+ * @param {string} rest - Host[:port] after tcp://
56
+ * @returns {{ host: string, port: number }|null}
57
+ */
58
+ function parseBracketedTcpHostPort(rest) {
59
+ if (!rest.startsWith('[')) return null;
60
+ const end = rest.indexOf(']');
61
+ if (end === -1) return null;
62
+ const host = rest.slice(1, end);
63
+ let port = DEFAULT_DOCKER_TLS_PORT;
64
+ const after = rest.slice(end + 1);
65
+ if (after.startsWith(':')) {
66
+ const p = parseInt(after.slice(1), 10);
67
+ if (!Number.isNaN(p)) port = p;
68
+ }
69
+ return { host, port };
70
+ }
71
+
72
+ /**
73
+ * @param {string} rest - Host[:port] after tcp://
74
+ * @returns {{ host: string, port: number }}
75
+ */
76
+ function parsePlainTcpHostPort(rest) {
77
+ const colonIdx = rest.lastIndexOf(':');
78
+ if (colonIdx > 0 && /^\d+$/.test(rest.slice(colonIdx + 1))) {
79
+ return {
80
+ host: rest.slice(0, colonIdx),
81
+ port: parseInt(rest.slice(colonIdx + 1), 10)
82
+ };
83
+ }
84
+ return { host: rest, port: DEFAULT_DOCKER_TLS_PORT };
85
+ }
86
+
87
+ /**
88
+ * @param {string} trimmed
89
+ * @returns {{ host: string, port: number }|null}
90
+ */
91
+ function parseTcpSchemeEndpoint(trimmed) {
92
+ const rest = trimmed.slice(6).split('/')[0];
93
+ return parseBracketedTcpHostPort(rest) || parsePlainTcpHostPort(rest);
94
+ }
95
+
96
+ /**
97
+ * @param {string} trimmed
98
+ * @returns {{ host: string, port: number }|null}
99
+ */
100
+ function parseDockerEndpointViaUrl(trimmed) {
101
+ try {
102
+ const u = new URL(trimmed.includes('://') ? trimmed : `tcp://${trimmed}`);
103
+ const host = String(u.hostname || '').trim();
104
+ if (!host) return null;
105
+ let port = DEFAULT_DOCKER_TLS_PORT;
106
+ if (u.port) {
107
+ const p = parseInt(u.port, 10);
108
+ if (!Number.isNaN(p)) port = p;
109
+ }
110
+ return { host, port };
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
54
116
  /**
55
117
  * Parse docker-endpoint (tcp://host:port) into TLS connect parameters.
56
118
  * @param {string} endpoint
@@ -62,45 +124,11 @@ function parseDockerTlsEndpoint(endpoint) {
62
124
  const lower = trimmed.toLowerCase();
63
125
  if (lower.startsWith('unix:')) return null;
64
126
 
65
- let host = '';
66
- let port = DEFAULT_DOCKER_TLS_PORT;
67
-
68
- if (lower.startsWith('tcp://')) {
69
- const rest = trimmed.slice(6).split('/')[0];
70
- if (rest.startsWith('[')) {
71
- const end = rest.indexOf(']');
72
- if (end === -1) return null;
73
- host = rest.slice(1, end);
74
- const after = rest.slice(end + 1);
75
- if (after.startsWith(':')) {
76
- const p = parseInt(after.slice(1), 10);
77
- if (!Number.isNaN(p)) port = p;
78
- }
79
- } else {
80
- const colonIdx = rest.lastIndexOf(':');
81
- if (colonIdx > 0 && /^\d+$/.test(rest.slice(colonIdx + 1))) {
82
- port = parseInt(rest.slice(colonIdx + 1), 10);
83
- host = rest.slice(0, colonIdx);
84
- } else {
85
- host = rest;
86
- }
87
- }
88
- } else {
89
- try {
90
- const u = new URL(trimmed.includes('://') ? trimmed : `tcp://${trimmed}`);
91
- host = u.hostname;
92
- if (u.port) {
93
- const p = parseInt(u.port, 10);
94
- if (!Number.isNaN(p)) port = p;
95
- }
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- host = String(host || '').trim();
102
- if (!host) return null;
103
- return { host, port, servername: host };
127
+ const parsed = lower.startsWith('tcp://')
128
+ ? parseTcpSchemeEndpoint(trimmed)
129
+ : parseDockerEndpointViaUrl(trimmed);
130
+ if (!parsed || !parsed.host) return null;
131
+ return { host: parsed.host, port: parsed.port, servername: parsed.host };
104
132
  }
105
133
 
106
134
  /**
@@ -262,6 +290,32 @@ async function writeDockerDaemonCaMarker(certDir) {
262
290
  await fs.writeFile(markerPath, `augmentedAt=${new Date().toISOString()}\n`, { mode: 0o600 });
263
291
  }
264
292
 
293
+ /**
294
+ * @param {string} certDir
295
+ * @param {string} existingCaPem
296
+ * @param {string} dockerEndpoint
297
+ * @param {{ timeoutMs?: number }} fetchOpts
298
+ * @returns {Promise<boolean>}
299
+ */
300
+ async function augmentCaPemForDockerVerify(certDir, existingCaPem, dockerEndpoint, fetchOpts) {
301
+ const fs = require('fs');
302
+ const path = require('path');
303
+ if (await caPemTrustsDockerDaemon(existingCaPem, dockerEndpoint, fetchOpts)) {
304
+ await writeDockerDaemonCaMarker(certDir);
305
+ return true;
306
+ }
307
+ const augmented = await augmentCaPemWithDockerDaemonChain(existingCaPem, dockerEndpoint, fetchOpts);
308
+ if (!augmented || augmented === existingCaPem) return false;
309
+ const { normalizePemNewlines } = require('./dev-cert-helper');
310
+ await fs.promises.writeFile(
311
+ path.join(certDir, 'ca.pem'),
312
+ normalizePemNewlines(augmented),
313
+ { mode: 0o600 }
314
+ );
315
+ await writeDockerDaemonCaMarker(certDir);
316
+ return true;
317
+ }
318
+
265
319
  /**
266
320
  * Decide whether Docker CLI should verify the daemon certificate (DOCKER_TLS_VERIFY=1).
267
321
  * When ca.pem holds only the internal issue-cert CA but the daemon uses a public/wildcard cert,
@@ -288,24 +342,7 @@ async function resolveRemoteDockerDaemonVerify(ctx) {
288
342
  if (!dockerEndpoint) return true;
289
343
 
290
344
  const fetchOpts = { timeoutMs: DEFAULT_FETCH_TIMEOUT_MS };
291
- if (await caPemTrustsDockerDaemon(existingCaPem, dockerEndpoint, fetchOpts)) {
292
- await writeDockerDaemonCaMarker(certDir);
293
- return true;
294
- }
295
-
296
- const augmented = await augmentCaPemWithDockerDaemonChain(existingCaPem, dockerEndpoint, fetchOpts);
297
- if (augmented && augmented !== existingCaPem) {
298
- const { normalizePemNewlines } = require('./dev-cert-helper');
299
- await fs.promises.writeFile(
300
- path.join(certDir, 'ca.pem'),
301
- normalizePemNewlines(augmented),
302
- { mode: 0o600 }
303
- );
304
- await writeDockerDaemonCaMarker(certDir);
305
- return true;
306
- }
307
-
308
- return false;
345
+ return augmentCaPemForDockerVerify(certDir, existingCaPem, dockerEndpoint, fetchOpts);
309
346
  }
310
347
 
311
348
  module.exports = {
@@ -101,6 +101,7 @@ function hostnameMatchesDockerHost(dockerHost) {
101
101
  const hnShort = hn.split('.')[0];
102
102
  const hShort = h.split('.')[0];
103
103
  if (h === hnShort || hn === hShort) return true;
104
+ if (hShort && hnShort && hShort === hnShort) return true;
104
105
  return false;
105
106
  }
106
107
 
@@ -120,8 +121,18 @@ function isReloadBindMountOnEngineHost(dockerEndpoint) {
120
121
  return hostnameMatchesDockerHost(host);
121
122
  }
122
123
 
124
+ /**
125
+ * True when host refers to this machine (loopback, FQDN, or short hostname match).
126
+ * @param {string} host
127
+ * @returns {boolean}
128
+ */
129
+ function isHostOnThisMachine(host) {
130
+ return hostnameMatchesDockerHost(host);
131
+ }
132
+
123
133
  module.exports = {
124
134
  extractHostFromDockerEndpoint,
125
135
  isReloadBindMountOnEngineHost,
126
- isLocalhostSyncSshHost
136
+ isLocalhostSyncSshHost,
137
+ isHostOnThisMachine
127
138
  };
@@ -54,6 +54,36 @@ const FALLBACK_SECURITY_BY_AUTH = {
54
54
  none: []
55
55
  };
56
56
 
57
+ /**
58
+ * @param {Array<{name: string, value: string}>} authSecureVars
59
+ * @param {Record<string, unknown>} security
60
+ * @param {string} prefix
61
+ * @param {string} systemKey
62
+ */
63
+ function pushSecureVarsFromSecurityMap(authSecureVars, security, prefix, systemKey) {
64
+ for (const key of Object.keys(security)) {
65
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
66
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
67
+ authSecureVars.push({ name: envName, value: pathVal || `kv://${systemKey}/${key}` });
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {Array<{name: string, value: string}>} authSecureVars
73
+ * @param {string} authMethod
74
+ * @param {string} prefix
75
+ * @param {string} systemKey
76
+ */
77
+ function pushFallbackSecureVars(authSecureVars, authMethod, prefix, systemKey) {
78
+ const keys = FALLBACK_SECURITY_BY_AUTH[authMethod] || FALLBACK_SECURITY_BY_AUTH.apikey;
79
+ for (const key of keys) {
80
+ authSecureVars.push({
81
+ name: `KV_${prefix}_${securityKeyToVar(key)}`,
82
+ value: `kv://${systemKey}/${key}`
83
+ });
84
+ }
85
+ }
86
+
57
87
  /**
58
88
  * Builds authSecureVars array from system authentication.security (or fallback by auth type).
59
89
  * @param {Object} system - System object with key and authentication
@@ -65,6 +95,9 @@ function buildAuthSecureVarsFromSystem(system) {
65
95
  const prefix = systemKeyToKvPrefix(systemKey);
66
96
  if (!prefix) return authSecureVars;
67
97
  const authBlock = system?.authentication || system?.auth;
98
+ if (authBlock?.authType === 'apiProvider' || authBlock?.authType === 'none') {
99
+ return authSecureVars;
100
+ }
68
101
  const credential = getAuthCredentialTemplate(authBlock);
69
102
  const security = credential?.security || authBlock?.security;
70
103
  const authMethod = (
@@ -74,19 +107,9 @@ function buildAuthSecureVarsFromSystem(system) {
74
107
  'apikey'
75
108
  ).toLowerCase();
76
109
  if (security && typeof security === 'object' && Object.keys(security).length > 0) {
77
- for (const key of Object.keys(security)) {
78
- const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
79
- const pathVal = kvEnvKeyToPath(envName, systemKey);
80
- authSecureVars.push({ name: envName, value: pathVal || `kv://${systemKey}/${key}` });
81
- }
110
+ pushSecureVarsFromSecurityMap(authSecureVars, security, prefix, systemKey);
82
111
  } else {
83
- const keys = FALLBACK_SECURITY_BY_AUTH[authMethod] || FALLBACK_SECURITY_BY_AUTH.apikey;
84
- for (const key of keys) {
85
- authSecureVars.push({
86
- name: `KV_${prefix}_${securityKeyToVar(key)}`,
87
- value: `kv://${systemKey}/${key}`
88
- });
89
- }
112
+ pushFallbackSecureVars(authSecureVars, authMethod, prefix, systemKey);
90
113
  }
91
114
  return authSecureVars;
92
115
  }
@@ -116,13 +139,25 @@ function buildConfigurationEntries(system) {
116
139
  * @returns {{ authMethod: string, authSecureVars: Array<{name: string, value: string}>, authNonSecureVarNames: string[], configuration: Array<{name: string, value: string, comment: string}> }}
117
140
  */
118
141
  function buildExternalEnvTemplateContext(system) {
119
- const authMethod = (system?.authentication?.method ||
120
- system?.authentication?.type ||
121
- system?.auth?.method ||
122
- system?.auth?.type ||
123
- 'apikey').toLowerCase();
142
+ const authBlock = system?.authentication || system?.auth;
143
+ const credential = getAuthCredentialTemplate(authBlock);
144
+ let authMethod = 'apikey';
145
+ if (authBlock?.authType === 'apiProvider') {
146
+ authMethod = 'apiprovider';
147
+ } else if (authBlock?.authType === 'none') {
148
+ authMethod = 'none';
149
+ } else {
150
+ authMethod = (
151
+ credential?.method ||
152
+ authBlock?.method ||
153
+ authBlock?.type ||
154
+ 'apikey'
155
+ ).toLowerCase();
156
+ }
124
157
  const authSecureVars = buildAuthSecureVarsFromSystem(system);
125
- const authVars = system?.authentication?.variables || system?.auth?.variables || {};
158
+ const authVars =
159
+ (credential?.variables && typeof credential.variables === 'object' ? credential.variables : null) ||
160
+ (authBlock?.variables && typeof authBlock.variables === 'object' ? authBlock.variables : {});
126
161
  const authNonSecureVarNames = Object.keys(authVars);
127
162
  const configuration = buildConfigurationEntries(system);
128
163
  return {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * In-place health-check wait line (TTY ora or carriage-return refresh).
3
+ *
4
+ * @fileoverview Health check poll presentation helpers
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('./logger');
13
+
14
+ function shouldUseHealthCheckPollSpinner() {
15
+ return Boolean(process && process.stdout && process.stdout.isTTY);
16
+ }
17
+
18
+ /**
19
+ * @param {number} attemptIndex - Zero-based attempt index after a failed probe
20
+ * @param {number} maxAttempts
21
+ * @returns {string}
22
+ */
23
+ function buildHealthCheckPollText(attemptIndex, maxAttempts) {
24
+ return chalk.yellow(
25
+ `Waiting for health check… (${attemptIndex + 1}/${maxAttempts})`
26
+ );
27
+ }
28
+
29
+ /**
30
+ * @param {number} maxAttempts
31
+ * @returns {{ usesSpinner: boolean, onWaiting: (attemptIndex: number) => void, finish: () => void }}
32
+ */
33
+ function createHealthCheckPollHandlers(maxAttempts) {
34
+ if (shouldUseHealthCheckPollSpinner()) {
35
+ const ora = require('ora');
36
+ const spinner = ora({
37
+ text: buildHealthCheckPollText(0, maxAttempts),
38
+ spinner: 'dots'
39
+ });
40
+ let started = false;
41
+ return {
42
+ usesSpinner: true,
43
+ onWaiting: (attemptIndex) => {
44
+ if (!started) {
45
+ spinner.start();
46
+ started = true;
47
+ }
48
+ spinner.text = buildHealthCheckPollText(attemptIndex, maxAttempts);
49
+ },
50
+ finish: () => {
51
+ if (started) {
52
+ spinner.stop();
53
+ process.stdout.write('\n');
54
+ }
55
+ }
56
+ };
57
+ }
58
+
59
+ let lastLine = '';
60
+ return {
61
+ usesSpinner: false,
62
+ onWaiting: (attemptIndex) => {
63
+ const line = buildHealthCheckPollText(attemptIndex, maxAttempts);
64
+ if (process.stdout && process.stdout.isTTY) {
65
+ process.stdout.write(`\r${line}`);
66
+ lastLine = line;
67
+ return;
68
+ }
69
+ logger.log(line);
70
+ },
71
+ finish: () => {
72
+ if (lastLine && process.stdout && process.stdout.isTTY) {
73
+ process.stdout.write('\n');
74
+ lastLine = '';
75
+ }
76
+ }
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ buildHealthCheckPollText,
82
+ createHealthCheckPollHandlers,
83
+ shouldUseHealthCheckPollSpinner
84
+ };