@aifabrix/builder 2.47.4 → 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 (93) hide show
  1. package/jest.isolated-projects.js +7 -0
  2. package/jest.projects.js +16 -0
  3. package/lib/api/applications.api.js +23 -0
  4. package/lib/api/dev.api.js +1 -1
  5. package/lib/api/index.js +5 -2
  6. package/lib/api/types/dev.types.js +4 -3
  7. package/lib/app/deploy-config.js +6 -4
  8. package/lib/app/register.js +21 -19
  9. package/lib/app/rotate-secret.js +39 -16
  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/build/index.js +12 -49
  17. package/lib/build/integration-docker-staging.js +35 -0
  18. package/lib/build/standard-docker-build.js +113 -0
  19. package/lib/cli/infra-guided-auth.js +42 -6
  20. package/lib/cli/infra-guided-footers.js +164 -0
  21. package/lib/cli/infra-guided-spinners.js +57 -0
  22. package/lib/cli/infra-guided.js +41 -179
  23. package/lib/cli/setup-app.js +5 -1
  24. package/lib/cli/setup-dev.js +3 -2
  25. package/lib/cli/setup-infra.js +3 -2
  26. package/lib/cli/setup-platform.js +3 -0
  27. package/lib/commands/dev-init.js +18 -0
  28. package/lib/commands/login-device.js +6 -2
  29. package/lib/commands/login.js +4 -1
  30. package/lib/commands/setup-modes.js +19 -3
  31. package/lib/commands/setup-platform-auth.js +25 -0
  32. package/lib/commands/setup-prompts-admin.js +9 -0
  33. package/lib/commands/setup-prompts-platform-mode.js +11 -1
  34. package/lib/commands/setup.js +60 -7
  35. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  36. package/lib/commands/up-dataplane-credentials.js +104 -6
  37. package/lib/commands/up-dataplane.js +74 -11
  38. package/lib/core/secrets-env-declarative-expand.js +35 -23
  39. package/lib/core/setup-platform-mode.js +59 -9
  40. package/lib/deployment/deployer-status.js +26 -3
  41. package/lib/deployment/deployer.js +7 -14
  42. package/lib/external-system/download-helpers.js +10 -1
  43. package/lib/infrastructure/compose.js +40 -12
  44. package/lib/infrastructure/services.js +15 -3
  45. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  46. package/lib/parameters/infra-kv-discovery.js +4 -3
  47. package/lib/utils/aifabrix-runtime-config-dir.js +2 -1
  48. package/lib/utils/app-bootstrap-api.js +53 -0
  49. package/lib/utils/app-run-containers.js +14 -3
  50. package/lib/utils/applications-config-defaults.js +19 -3
  51. package/lib/utils/compose-generate-docker-compose.js +18 -6
  52. package/lib/utils/compose-generator.js +34 -15
  53. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  54. package/lib/utils/config-paths.js +8 -3
  55. package/lib/utils/dev-ca-docker-augment.js +54 -0
  56. package/lib/utils/dev-user-groups.js +1 -0
  57. package/lib/utils/docker-daemon-tls-ca.js +363 -0
  58. package/lib/utils/docker-reload-mount.js +12 -1
  59. package/lib/utils/ensure-dev-certs-for-remote-docker.js +5 -0
  60. package/lib/utils/external-env-template.js +53 -18
  61. package/lib/utils/health-check-poll-ui.js +84 -0
  62. package/lib/utils/health-check-url.js +54 -34
  63. package/lib/utils/health-check.js +43 -16
  64. package/lib/utils/infra-optional-service-flags.js +31 -2
  65. package/lib/utils/mutagen.js +7 -4
  66. package/lib/utils/platform-controller-url.js +23 -1
  67. package/lib/utils/promote-dev-docker-image.js +170 -0
  68. package/lib/utils/refresh-remote-settings.js +67 -0
  69. package/lib/utils/remote-docker-env.js +22 -7
  70. package/lib/utils/remote-host-for-front-door.js +43 -0
  71. package/lib/utils/run-cli-flags.js +37 -1
  72. package/lib/utils/secrets-helpers.js +9 -14
  73. package/lib/utils/secrets-kv-refs.js +47 -3
  74. package/lib/utils/secrets-missing-error.js +1 -9
  75. package/lib/utils/token-manager-client-credentials.js +11 -1
  76. package/lib/utils/url-declarative-public-base.js +11 -4
  77. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  78. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  79. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  80. package/lib/utils/url-declarative-resolve.js +2 -0
  81. package/lib/utils/urls-local-registry-scan.js +95 -10
  82. package/lib/utils/urls-local-registry.js +24 -1
  83. package/lib/utils/worker-sidecars.js +28 -1
  84. package/lib/validation/external-system-auth-rules.js +45 -7
  85. package/package.json +2 -1
  86. package/scripts/test-dataplane-bootstrap.js +331 -0
  87. package/templates/applications/dataplane/env.template +22 -5
  88. package/templates/applications/dataplane/rbac.yaml +11 -0
  89. package/templates/applications/miso-controller/application.yaml +1 -1
  90. package/templates/applications/miso-controller/env.template +3 -0
  91. package/templates/infra/compose.yaml.hbs +13 -9
  92. package/templates/python/docker-compose.hbs +3 -0
  93. package/templates/typescript/docker-compose.hbs +3 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Promote a developer-scoped local Docker image to the manifest base repository (tag only).
3
+ *
4
+ * @fileoverview Used by `aifabrix build --base --from-dev` and dev-promote workflows
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const config = require('../core/config');
12
+ const { loadAndValidateConfig } = require('./build-helpers');
13
+ const { resolveBuildDockerTagNames } = require('./build-resolve-image');
14
+ const { execWithDockerEnv } = require('./docker-exec');
15
+ const logger = require('./logger');
16
+ const { formatSuccessLine } = require('./cli-test-layout-chalk');
17
+
18
+ /**
19
+ * @param {string} value
20
+ * @returns {string}
21
+ */
22
+ function shellQuote(value) {
23
+ return `'${String(value).replace(/'/g, '\'\\\'\'')}'`;
24
+ }
25
+
26
+ /**
27
+ * @param {string} imageRef - repository:tag
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ async function dockerImageRefExists(imageRef) {
31
+ try {
32
+ await execWithDockerEnv(`docker image inspect ${shellQuote(imageRef)}`);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {string} imageRef
41
+ * @returns {Promise<string|null>}
42
+ */
43
+ async function readDockerImageId(imageRef) {
44
+ try {
45
+ const { stdout } = await execWithDockerEnv(
46
+ `docker image inspect --format '{{.Id}}' ${shellQuote(imageRef)}`
47
+ );
48
+ const id = stdout.trim();
49
+ return id || null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @param {string} sourceRef
57
+ * @param {string} targetRef
58
+ * @returns {Promise<void>}
59
+ */
60
+ async function dockerTagImageRef(sourceRef, targetRef) {
61
+ await execWithDockerEnv(`docker tag ${shellQuote(sourceRef)} ${shellQuote(targetRef)}`);
62
+ }
63
+
64
+ /**
65
+ * @param {string} appName
66
+ * @param {Object} variables
67
+ * @param {string|number} developerId
68
+ * @param {Object} options - `tag` (base target), `fromDevTag` (dev source; default `latest`)
69
+ * @returns {Promise<{ devRef: string, baseRef: string, devTag: string, baseTag: string }>}
70
+ */
71
+ async function resolveDevAndBaseImageRefs(appName, variables, developerId, options) {
72
+ const baseTag = options.tag || 'latest';
73
+ const devTag = options.fromDevTag || 'latest';
74
+ const { buildImageName: baseName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
75
+ ...options,
76
+ base: true
77
+ });
78
+ const { buildImageName: devName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
79
+ ...options,
80
+ base: false
81
+ });
82
+ return {
83
+ devRef: `${devName}:${devTag}`,
84
+ baseRef: `${baseName}:${baseTag}`,
85
+ devTag,
86
+ baseTag
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Find a local developer-scoped image to promote (dev-build usually tags `latest`).
92
+ * @param {string} appName
93
+ * @param {Object} appConfig
94
+ * @param {string|number} developerId
95
+ * @param {Object} options
96
+ * @returns {Promise<{ devRef: string, baseRef: string }|null>}
97
+ */
98
+ async function resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options) {
99
+ const baseTag = options.tag || 'latest';
100
+ const devTagCandidates = ['latest'];
101
+ if (baseTag !== 'latest' && !devTagCandidates.includes(baseTag)) {
102
+ devTagCandidates.push(baseTag);
103
+ }
104
+ for (const fromDevTag of devTagCandidates) {
105
+ const refs = await resolveDevAndBaseImageRefs(appName, appConfig, developerId, {
106
+ ...options,
107
+ fromDevTag
108
+ });
109
+ if (await dockerImageRefExists(refs.devRef)) {
110
+ return { devRef: refs.devRef, baseRef: refs.baseRef };
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Tag developer-scoped image as manifest base when present locally.
118
+ * @param {string} appName
119
+ * @param {Object} options - build options (`base`, `tag`, `registry`, `fromDev`)
120
+ * @returns {Promise<string|null>} base image ref when promoted/skipped; null when dev image missing
121
+ */
122
+ async function tryPromoteDevImageToBase(appName, options = {}) {
123
+ if (options.base !== true) {
124
+ throw new Error('Promote-from-dev requires --base.');
125
+ }
126
+
127
+ const developerId = await config.getDeveloperId();
128
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : Number(developerId);
129
+ if (!Number.isFinite(idNum) || idNum <= 0) {
130
+ return null;
131
+ }
132
+
133
+ const { config: appConfig } = await loadAndValidateConfig(appName);
134
+ const resolved = await resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options);
135
+ if (!resolved) {
136
+ return null;
137
+ }
138
+ const { devRef, baseRef } = resolved;
139
+
140
+ const devId = await readDockerImageId(devRef);
141
+ if (await dockerImageRefExists(baseRef)) {
142
+ const baseId = await readDockerImageId(baseRef);
143
+ if (devId && baseId && devId === baseId) {
144
+ logger.log(formatSuccessLine(`Base image already matches dev build: ${baseRef}`));
145
+ return baseRef;
146
+ }
147
+ }
148
+
149
+ await dockerTagImageRef(devRef, baseRef);
150
+ logger.log(formatSuccessLine(`Promoted dev image to base (tag only): ${baseRef}`));
151
+ return baseRef;
152
+ }
153
+
154
+ /**
155
+ * @param {Object} options
156
+ * @returns {boolean}
157
+ */
158
+ function shouldAttemptDevPromote(options) {
159
+ return options.fromDev === true && options.base === true;
160
+ }
161
+
162
+ module.exports = {
163
+ tryPromoteDevImageToBase,
164
+ shouldAttemptDevPromote,
165
+ resolveDevAndBaseImageRefs,
166
+ resolvePromoteSourceAndTargetRefs,
167
+ dockerImageRefExists,
168
+ readDockerImageId,
169
+ dockerTagImageRef
170
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Refresh ~/.aifabrix/config.yaml from GET /api/dev/settings when remote dev auth is available.
3
+ *
4
+ * @fileoverview Merge builder-server settings (developer-root-domain, sync paths, etc.)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const config = require('../core/config');
13
+ const devApi = require('../api/dev.api');
14
+ const { getRemoteDevAuth } = require('./remote-dev-auth');
15
+ const { getCertDir, readClientKeyPem } = require('./dev-cert-helper');
16
+ const { getConfigDirForPaths } = require('./paths');
17
+ const logger = require('./logger');
18
+
19
+ /**
20
+ * @typedef {Object} RefreshRemoteSettingsResult
21
+ * @property {boolean} refreshed
22
+ * @property {string} [reason]
23
+ * @property {Object} [settings]
24
+ * @property {Error} [error]
25
+ */
26
+
27
+ /**
28
+ * Fetch GET /api/dev/settings and merge into config when remote-server + client cert exist.
29
+ *
30
+ * @async
31
+ * @param {{ quiet?: boolean }} [opts]
32
+ * @returns {Promise<RefreshRemoteSettingsResult>}
33
+ */
34
+ async function refreshRemoteSettingsFromBuilderIfPossible(opts = {}) {
35
+ const quiet = opts.quiet !== false;
36
+ const auth = await getRemoteDevAuth();
37
+ if (!auth) {
38
+ return { refreshed: false, reason: 'no-remote-auth' };
39
+ }
40
+
41
+ const devId = await config.getDeveloperId();
42
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
43
+ const clientKeyPem = readClientKeyPem(certDir);
44
+
45
+ try {
46
+ const settings = await devApi.getSettings(
47
+ auth.serverUrl,
48
+ auth.clientCertPem,
49
+ clientKeyPem || undefined,
50
+ auth.serverCaPem || undefined
51
+ );
52
+ await config.mergeRemoteSettings(settings);
53
+ if (!quiet) {
54
+ logger.log(chalk.gray('Synced developer settings from Builder Server'));
55
+ }
56
+ return { refreshed: true, settings };
57
+ } catch (error) {
58
+ if (!quiet) {
59
+ logger.warn(
60
+ chalk.yellow(`Could not refresh settings from Builder Server: ${error.message}`)
61
+ );
62
+ }
63
+ return { refreshed: false, reason: 'fetch-failed', error };
64
+ }
65
+ }
66
+
67
+ module.exports = { refreshRemoteSettingsFromBuilderIfPossible };
@@ -6,10 +6,12 @@
6
6
  * @version 2.0.0
7
7
  */
8
8
 
9
+ const fs = require('fs');
9
10
  const path = require('path');
10
11
  const config = require('../core/config');
11
- const { getCertDir } = require('./dev-cert-helper');
12
+ const { getCertDir, readServerCaPem } = require('./dev-cert-helper');
12
13
  const { getConfigDirForPaths } = require('./paths');
14
+ const { resolveRemoteDockerDaemonVerify } = require('./docker-daemon-tls-ca');
13
15
 
14
16
  function devTlsCertPaths(certDir) {
15
17
  return {
@@ -43,10 +45,13 @@ function missingCaError(trimmed, certDir) {
43
45
  * When docker-endpoint is set, we do not fall back to the local daemon without that endpoint
44
46
  * (avoids accidentally using Docker Desktop while the dev profile targets a remote engine).
45
47
  *
46
- * When **TLS skip-verify** is enabled (config or env) and **ca.pem is missing**, client cert/key are
47
- * optional: Docker can use DOCKER_TLS_VERIFY=0 with no client certs if the daemon allows it.
48
- * If **ca.pem is present** (e.g. from Builder Server issue-cert), the daemon certificate is always
49
- * verified (DOCKER_TLS_VERIFY=1) even when skip-verify is set — better security once a trust anchor exists.
48
+ * When **TLS skip-verify** is enabled (config or `AIFABRIX_DOCKER_TLS_SKIP_VERIFY`), Docker uses
49
+ * DOCKER_TLS_VERIFY=0 (client cert/key still sent when present). Skip-verify applies even when ca.pem
50
+ * exists — ca.pem may hold the internal issue-cert CA while the Docker daemon uses a public/wildcard cert.
51
+ *
52
+ * When skip-verify is off and ca.pem exists, the CLI verifies the daemon only if ca.pem includes the
53
+ * Docker daemon trust chain (merged at dev init/refresh or lazily on first Docker use). Otherwise it
54
+ * falls back to DOCKER_TLS_VERIFY=0 with client mTLS (public CA daemon on a trusted builder host).
50
55
  *
51
56
  * Without skip-verify, cert.pem, key.pem, and ca.pem are required in the dev cert directory.
52
57
  *
@@ -61,7 +66,6 @@ async function getRemoteDockerEnv() {
61
66
  const trimmed = endpoint.trim();
62
67
  const certDir = getCertDir(getConfigDirForPaths(), await config.getDeveloperId());
63
68
  const { certPath, keyPath, caPath } = devTlsCertPaths(certDir);
64
- const fs = require('fs');
65
69
  const skipVerify = await config.getDockerTlsSkipVerify();
66
70
  const hasClient = fs.existsSync(certPath) && fs.existsSync(keyPath);
67
71
  const hasCa = fs.existsSync(caPath);
@@ -71,7 +75,18 @@ async function getRemoteDockerEnv() {
71
75
  return { DOCKER_HOST: trimmed, DOCKER_TLS_VERIFY: '0' };
72
76
  }
73
77
  if (!hasCa && !skipVerify) throw missingCaError(trimmed, certDir);
74
- const verifyDaemon = hasCa;
78
+
79
+ let verifyDaemon = false;
80
+ if (!skipVerify && hasCa) {
81
+ verifyDaemon = await resolveRemoteDockerDaemonVerify({
82
+ certDir,
83
+ existingCaPem: readServerCaPem(certDir),
84
+ dockerEndpoint: trimmed,
85
+ remoteServerUrl: await config.getRemoteServer(),
86
+ skipVerify: false
87
+ });
88
+ }
89
+
75
90
  return {
76
91
  DOCKER_HOST: trimmed,
77
92
  DOCKER_TLS_VERIFY: verifyDaemon ? '1' : '0',
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Resolve hostname for `${REMOTE_HOST}` in frontDoorRouting.host templates.
3
+ * Prefers `developer-root-domain` from builder-server settings over `remote-server` hostname.
4
+ *
5
+ * @fileoverview App public hostname root (separate from builder API host)
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { hostnameFromUrl } = require('./config-paths');
13
+
14
+ /**
15
+ * Hostname for `${REMOTE_HOST}` expansion (Traefik labels, url:// public, setup health).
16
+ *
17
+ * @param {Object} [input]
18
+ * @param {string|null|undefined} input.remoteServer - Builder API base URL
19
+ * @param {string|null|undefined} input.developerRootDomain - App hostname root from GET /api/dev/settings
20
+ * @returns {string}
21
+ */
22
+ function resolveRemoteHostForFrontDoor(input = {}) {
23
+ const root = String(input.developerRootDomain || '').trim();
24
+ if (root) {
25
+ return root.replace(/^\.+|\.+$/g, '');
26
+ }
27
+ return hostnameFromUrl(input.remoteServer) || '';
28
+ }
29
+
30
+ /**
31
+ * Whether front-door host expansion can resolve `${REMOTE_HOST}` (root domain or remote-server).
32
+ *
33
+ * @param {Object} [input]
34
+ * @returns {boolean}
35
+ */
36
+ function hasFrontDoorRemoteHostContext(input = {}) {
37
+ return Boolean(resolveRemoteHostForFrontDoor(input));
38
+ }
39
+
40
+ module.exports = {
41
+ resolveRemoteHostForFrontDoor,
42
+ hasFrontDoorRemoteHostContext
43
+ };
@@ -24,6 +24,42 @@ function isRunCliNoProxy(options) {
24
24
  return options.noProxy === true;
25
25
  }
26
26
 
27
+ /**
28
+ * Whether `aifabrix run` should persist `applications.<app>.proxy: true`.
29
+ * Proxy / front-door URL hints require infra Traefik (`traefik: true` in config).
30
+ * Direct-port (`setupPlatformMode: single`) always persists false even though Commander defaults `--proxy` on.
31
+ *
32
+ * @param {Object} [options] - Commander action options
33
+ * @param {Object|null|undefined} [userCfg] - Parsed `~/.aifabrix/config.yaml`
34
+ * @returns {boolean}
35
+ */
36
+ function resolveRunProxyPersistEnabled(options, userCfg) {
37
+ if (isRunCliNoProxy(options)) {
38
+ return false;
39
+ }
40
+ return Boolean(userCfg && userCfg.traefik === true);
41
+ }
42
+
43
+ /**
44
+ * Effective no-proxy for env/url expansion during run.
45
+ *
46
+ * @param {Object} [options]
47
+ * @param {Object|null|undefined} [userCfg]
48
+ * @param {boolean} savedProxyHint - `applications.<app>.proxy` from config
49
+ * @returns {boolean}
50
+ */
51
+ function resolveRunEffectiveNoProxy(options, userCfg, savedProxyHint) {
52
+ if (isRunCliNoProxy(options)) {
53
+ return true;
54
+ }
55
+ if (!userCfg || userCfg.traefik !== true) {
56
+ return true;
57
+ }
58
+ return !savedProxyHint;
59
+ }
60
+
27
61
  module.exports = {
28
- isRunCliNoProxy
62
+ isRunCliNoProxy,
63
+ resolveRunProxyPersistEnabled,
64
+ resolveRunEffectiveNoProxy
29
65
  };
@@ -19,7 +19,7 @@ const { updateContainerPortInEnvFile } = require('./env-ports');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { getLocalPortFromPath } = require('./port-resolver');
21
21
  const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
22
- const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
22
+ const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan, envLineWithoutInlineComment } = require('./secrets-kv-refs');
23
23
 
24
24
  /**
25
25
  * Interpolate ${VAR} occurrences with values from envVars map
@@ -34,16 +34,6 @@ function interpolateEnvVars(content, envVars) {
34
34
  });
35
35
  }
36
36
 
37
- /**
38
- * Returns true if the line is a comment or empty (should be skipped for kv:// resolution)
39
- * @param {string} line - Single line
40
- * @returns {boolean}
41
- */
42
- function isCommentOrEmptyLine(line) {
43
- const t = line.trim();
44
- return t === '' || t.startsWith('#');
45
- }
46
-
47
37
  /** Regex for kv:// path (allows slashes, e.g. kv://hubspot/clientId) */
48
38
  const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
49
39
 
@@ -162,10 +152,11 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
162
152
  const seen = new Set();
163
153
  const missing = [];
164
154
  for (const line of content.split('\n')) {
165
- if (isCommentOrEmptyLine(line)) continue;
155
+ const scanLine = activeEnvLineForKvScan(line);
156
+ if (!scanLine) continue;
166
157
  let match;
167
158
  KV_REF_PATTERN.lastIndex = 0;
168
- while ((match = KV_REF_PATTERN.exec(line)) !== null) {
159
+ while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
169
160
  const pathStr = match[1];
170
161
  if (seen.has(pathStr)) continue;
171
162
  seen.add(pathStr);
@@ -222,7 +213,10 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
222
213
  const lines = content.split('\n');
223
214
  const result = lines.map(line => {
224
215
  if (isCommentOrEmptyLine(line)) return line;
225
- return line.replace(KV_REF_PATTERN, (match, pathStr) => {
216
+ const scanLine = envLineWithoutInlineComment(line);
217
+ if (!scanLine.trim()) return line;
218
+ const suffix = line.slice(scanLine.length);
219
+ const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
226
220
  let value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
227
221
  if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
228
222
  value = '';
@@ -234,6 +228,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
234
228
  }
235
229
  return value !== null && value !== undefined ? String(value) : match;
236
230
  });
231
+ return replaced + suffix;
237
232
  });
238
233
  return result.join('\n');
239
234
  }
@@ -7,11 +7,51 @@
7
7
 
8
8
  const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
9
9
 
10
+ /**
11
+ * True when the line is empty or a full-line comment (# after trim).
12
+ *
13
+ * @param {string} line
14
+ * @returns {boolean}
15
+ */
10
16
  function isCommentOrEmptyLine(line) {
11
- const t = line.trim();
17
+ const t = String(line).trim();
12
18
  return t === '' || t.startsWith('#');
13
19
  }
14
20
 
21
+ /**
22
+ * Active env line for kv:// scanning: drop inline `#` comments (not inside quotes).
23
+ *
24
+ * @param {string} line
25
+ * @returns {string}
26
+ */
27
+ function envLineWithoutInlineComment(line) {
28
+ if (isCommentOrEmptyLine(line)) {
29
+ return '';
30
+ }
31
+ let inSingle = false;
32
+ let inDouble = false;
33
+ const raw = String(line);
34
+ for (let i = 0; i < raw.length; i++) {
35
+ const c = raw[i];
36
+ if (c === '\'' && !inDouble) {
37
+ inSingle = !inSingle;
38
+ } else if (c === '"' && !inSingle) {
39
+ inDouble = !inDouble;
40
+ } else if (c === '#' && !inSingle && !inDouble) {
41
+ return raw.slice(0, i);
42
+ }
43
+ }
44
+ return raw;
45
+ }
46
+
47
+ /**
48
+ * @param {string} line
49
+ * @returns {string}
50
+ */
51
+ function activeEnvLineForKvScan(line) {
52
+ return envLineWithoutInlineComment(line).trim();
53
+ }
54
+
15
55
  /**
16
56
  * @param {string} content
17
57
  * @returns {string[]}
@@ -23,10 +63,11 @@ function collectUniqueKvPathStrings(content) {
23
63
  return out;
24
64
  }
25
65
  for (const line of content.split('\n')) {
26
- if (isCommentOrEmptyLine(line)) continue;
66
+ const scanLine = activeEnvLineForKvScan(line);
67
+ if (!scanLine) continue;
27
68
  let match;
28
69
  KV_REF_PATTERN.lastIndex = 0;
29
- while ((match = KV_REF_PATTERN.exec(line)) !== null) {
70
+ while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
30
71
  const pathStr = match[1];
31
72
  if (!seen.has(pathStr)) {
32
73
  seen.add(pathStr);
@@ -38,5 +79,8 @@ function collectUniqueKvPathStrings(content) {
38
79
  }
39
80
 
40
81
  module.exports = {
82
+ isCommentOrEmptyLine,
83
+ envLineWithoutInlineComment,
84
+ activeEnvLineForKvScan,
41
85
  collectUniqueKvPathStrings
42
86
  };
@@ -9,15 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
12
-
13
- /**
14
- * @param {string} line
15
- * @returns {boolean}
16
- */
17
- function isCommentOrEmptyLine(line) {
18
- const t = String(line).trim();
19
- return t === '' || t.startsWith('#');
20
- }
12
+ const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
21
13
 
22
14
  /**
23
15
  * @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
@@ -183,12 +183,22 @@ async function resolveCredentialAppForDeployment(appName) {
183
183
 
184
184
  /**
185
185
  * @param {string} appName
186
+ * @param {{ secretsFileOnly?: boolean }} [options]
186
187
  * @returns {Promise<{ clientId: string, clientSecret: string }|null>}
187
188
  */
188
- async function loadClientCredentials(appName) {
189
+ async function loadClientCredentials(appName, options = {}) {
189
190
  if (!appName || typeof appName !== 'string') {
190
191
  throw new Error('App name is required and must be a string');
191
192
  }
193
+ if (options.secretsFileOnly === true) {
194
+ for (const key of credentialAppKeysToTry(appName)) {
195
+ const fromFile = await readClientCredentialsFromSecretsFileDecrypted(key);
196
+ if (fromFile) {
197
+ return fromFile;
198
+ }
199
+ }
200
+ return null;
201
+ }
192
202
  const resolved = await resolveCredentialAppForDeployment(appName);
193
203
  return resolved.credentials;
194
204
  }
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
12
+ const { hasFrontDoorRemoteHostContext } = require('./remote-host-for-front-door');
12
13
  const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
13
14
  const {
14
15
  normalizeSetupPlatformMode,
@@ -23,11 +24,12 @@ const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
23
24
  * @param {Object} opts
24
25
  * @param {string|number|null|undefined} opts.developerIdRaw
25
26
  * @param {string|null|undefined} opts.remoteServer
27
+ * @param {string|null|undefined} [opts.developerRootDomain]
26
28
  * @returns {string}
27
29
  */
28
30
  function expandFrontDoorHostTemplateForUrls(template, opts) {
29
- const { developerIdRaw, remoteServer } = opts || {};
30
- return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer);
31
+ const { developerIdRaw, remoteServer, developerRootDomain } = opts || {};
32
+ return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer, developerRootDomain);
31
33
  }
32
34
 
33
35
  function hostPortForProfile(profile, listenPort, developerIdNum) {
@@ -174,6 +176,7 @@ function buildTraefikPublicBaseIfApplicable(opts) {
174
176
  hostTemplate,
175
177
  developerIdRaw,
176
178
  remoteServer,
179
+ developerRootDomain,
177
180
  profile,
178
181
  developerIdNum
179
182
  } = opts;
@@ -181,7 +184,10 @@ function buildTraefikPublicBaseIfApplicable(opts) {
181
184
  if (!traefik || !pathActive || !hostTemplate || !String(hostTemplate).trim()) {
182
185
  return null;
183
186
  }
184
- if (templateUsesRemoteHostPlaceholder(hostTemplate) && !String(remoteServer || '').trim()) {
187
+ if (
188
+ templateUsesRemoteHostPlaceholder(hostTemplate) &&
189
+ !hasFrontDoorRemoteHostContext({ remoteServer, developerRootDomain })
190
+ ) {
185
191
  // Local full-platform (no remote domain): use the single localhost front door port (3000 + devId×100).
186
192
  return localhostFrontDoorBaseForMissingRemote({
187
193
  profile,
@@ -190,7 +196,8 @@ function buildTraefikPublicBaseIfApplicable(opts) {
190
196
  }
191
197
  const expanded = expandFrontDoorHostTemplateForUrls(hostTemplate, {
192
198
  developerIdRaw,
193
- remoteServer
199
+ remoteServer,
200
+ developerRootDomain
194
201
  });
195
202
  if (!expanded) {
196
203
  return null;