@aifabrix/builder 2.47.0 → 2.47.2

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 (36) hide show
  1. package/lib/build/docker-build-args.js +83 -0
  2. package/lib/build/index.js +1 -29
  3. package/lib/cli/index.js +2 -0
  4. package/lib/cli/infra-guided-auth.js +36 -12
  5. package/lib/cli/setup-npm.js +59 -0
  6. package/lib/cli/setup-secrets.js +1 -1
  7. package/lib/commands/login-device.js +11 -3
  8. package/lib/commands/login.js +8 -3
  9. package/lib/commands/npm-config.js +103 -0
  10. package/lib/utils/api.js +2 -1
  11. package/lib/utils/controller-url.js +4 -1
  12. package/lib/utils/dev-platform-tls.js +148 -0
  13. package/lib/utils/health-check-request.js +114 -0
  14. package/lib/utils/health-check-url.js +3 -1
  15. package/lib/utils/health-check.js +1 -113
  16. package/lib/utils/help-builder.js +1 -0
  17. package/lib/utils/keycloak-realm-ready.js +5 -1
  18. package/lib/utils/localhost-public-host.js +31 -0
  19. package/lib/utils/npm-user-config.js +145 -0
  20. package/lib/utils/platform-controller-url.js +56 -3
  21. package/lib/utils/url-declarative-public-base.js +33 -25
  22. package/package.json +1 -1
  23. package/.npmrc.token +0 -1
  24. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +0 -1
  25. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +0 -17
  26. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +0 -1
  27. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +0 -1
  28. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +0 -1
  29. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +0 -17
  30. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +0 -17
  31. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +0 -17
  32. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +0 -17
  33. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +0 -17
  34. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +0 -17
  35. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +0 -17
  36. package/.temp/validation/summary.txt +0 -5
@@ -0,0 +1,114 @@
1
+ /**
2
+ * HTTP(S) health probe request helpers.
3
+ *
4
+ * @fileoverview Low-level health endpoint HTTP client
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const http = require('http');
12
+ const https = require('https');
13
+ const chalk = require('chalk');
14
+ const logger = require('./logger');
15
+ const { shouldAllowInsecureDevPlatformTls } = require('./dev-platform-tls');
16
+
17
+ /**
18
+ * @param {string} data
19
+ * @param {number} statusCode
20
+ * @returns {boolean}
21
+ */
22
+ function parseHealthResponse(data, statusCode) {
23
+ try {
24
+ const health = JSON.parse(data);
25
+ if (health.status === 'UP') {
26
+ return true;
27
+ }
28
+ if (health.status === 'ok') {
29
+ return health.database === 'connected' || !health.database;
30
+ }
31
+ if (health.status === 'healthy') {
32
+ return true;
33
+ }
34
+ if (health.success === true) {
35
+ return true;
36
+ }
37
+ return false;
38
+ } catch {
39
+ return statusCode === 200;
40
+ }
41
+ }
42
+
43
+ function handleHealthResponse(res, data, debug, resolve) {
44
+ const isHealthy = parseHealthResponse(data, res.statusCode);
45
+ if (debug) {
46
+ const truncatedData = data.length > 200 ? data.substring(0, 200) + '...' : data;
47
+ logger.log(chalk.gray(`[DEBUG] Response body: ${truncatedData}`));
48
+ logger.log(chalk.gray(`[DEBUG] Health check result: ${isHealthy ? 'healthy' : 'unhealthy'}`));
49
+ }
50
+ resolve(isHealthy);
51
+ }
52
+
53
+ function doHealthCheckRequest(healthCheckUrl, debug, resolve, reject, allowInsecureTls = false) {
54
+ try {
55
+ const urlObj = new URL(healthCheckUrl);
56
+ const transport = urlObj.protocol === 'https:' ? https : http;
57
+ const options = {
58
+ hostname: urlObj.hostname,
59
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
60
+ path: urlObj.pathname + urlObj.search,
61
+ method: 'GET',
62
+ ...(allowInsecureTls ? { rejectUnauthorized: false } : {})
63
+ };
64
+ if (debug) {
65
+ logger.log(chalk.gray(`[DEBUG] Health check request: ${healthCheckUrl}`));
66
+ logger.log(chalk.gray(`[DEBUG] Request options: ${JSON.stringify(options, null, 2)}`));
67
+ }
68
+ const timer = { id: null };
69
+ const req = transport.request(options, (res) => {
70
+ clearTimeout(timer.id);
71
+ let data = '';
72
+ if (debug) {
73
+ logger.log(chalk.gray(`[DEBUG] Response status code: ${res.statusCode}`));
74
+ logger.log(chalk.gray(`[DEBUG] Response headers: ${JSON.stringify(res.headers, null, 2)}`));
75
+ }
76
+ res.on('data', (chunk) => {
77
+ data += chunk;
78
+ });
79
+ res.on('end', () => handleHealthResponse(res, data, debug, resolve));
80
+ });
81
+ timer.id = setTimeout(() => {
82
+ if (debug) logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
83
+ req.destroy();
84
+ resolve(false);
85
+ }, 5000);
86
+ req.on('error', (error) => {
87
+ clearTimeout(timer.id);
88
+ if (debug) logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
89
+ resolve(false);
90
+ });
91
+ req.end();
92
+ } catch (error) {
93
+ if (debug) logger.log(chalk.gray(`[DEBUG] Health check exception: ${error.message}`));
94
+ reject(error);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @async
100
+ * @param {string} healthCheckUrl
101
+ * @param {boolean} [debug=false]
102
+ * @returns {Promise<boolean>}
103
+ */
104
+ async function checkHealthEndpoint(healthCheckUrl, debug = false) {
105
+ const allowInsecureTls = await shouldAllowInsecureDevPlatformTls(healthCheckUrl);
106
+ return new Promise((resolve, reject) => {
107
+ doHealthCheckRequest(healthCheckUrl, debug, resolve, reject, allowInsecureTls);
108
+ });
109
+ }
110
+
111
+ module.exports = {
112
+ parseHealthResponse,
113
+ checkHealthEndpoint
114
+ };
@@ -92,6 +92,7 @@ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig)
92
92
  const remoteServer = await coreConfig.getRemoteServer();
93
93
  const developerIdRaw = await coreConfig.getDeveloperId();
94
94
  const developerIdNum = parseDeveloperIdNum(developerIdRaw);
95
+ const setupPlatformMode = await coreConfig.getSetupPlatformMode();
95
96
 
96
97
  // URLs are resolved for the CLI on the host, not from inside a container.
97
98
  const profile = 'local';
@@ -107,7 +108,8 @@ async function computeTraefikPublicAppUrl(_appName, _healthCheckPort, appConfig)
107
108
  profile,
108
109
  listenPort,
109
110
  developerIdNum,
110
- infraTlsEnabled
111
+ infraTlsEnabled,
112
+ setupPlatformMode
111
113
  });
112
114
 
113
115
  return joinUrlPath(publicBase, mountPath);
@@ -9,9 +9,8 @@ const { formatSuccessLine } = require('./cli-test-layout-chalk');
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const http = require('http');
13
- const https = require('https');
14
12
  const net = require('net');
13
+ const { checkHealthEndpoint } = require('./health-check-request');
15
14
  const dns = require('dns');
16
15
  const chalk = require('chalk');
17
16
  const logger = require('./logger');
@@ -192,117 +191,6 @@ async function getContainerPort(appName, debug = false) {
192
191
  return 3000;
193
192
  }
194
193
 
195
- /**
196
- * Parses health check response
197
- * @function parseHealthResponse
198
- * @param {string} data - Response data
199
- * @param {number} statusCode - HTTP status code
200
- * @returns {boolean} True if healthy
201
- */
202
- function parseHealthResponse(data, statusCode) {
203
- try {
204
- const health = JSON.parse(data);
205
- if (health.status === 'UP') {
206
- return true;
207
- }
208
- if (health.status === 'ok') {
209
- return health.database === 'connected' || !health.database;
210
- }
211
- if (health.status === 'healthy') {
212
- return true;
213
- }
214
- if (health.success === true) {
215
- return true;
216
- }
217
- return false;
218
- } catch (error) {
219
- return statusCode === 200;
220
- }
221
- }
222
-
223
- function handleHealthResponse(res, data, debug, resolve) {
224
- const isHealthy = parseHealthResponse(data, res.statusCode);
225
- if (debug) {
226
- const truncatedData = data.length > 200 ? data.substring(0, 200) + '...' : data;
227
- logger.log(chalk.gray(`[DEBUG] Response body: ${truncatedData}`));
228
- logger.log(chalk.gray(`[DEBUG] Health check result: ${isHealthy ? 'healthy' : 'unhealthy'}`));
229
- }
230
- resolve(isHealthy);
231
- }
232
-
233
- function doHealthCheckRequest(healthCheckUrl, debug, resolve, reject) {
234
- try {
235
- const urlObj = new URL(healthCheckUrl);
236
- const transport = urlObj.protocol === 'https:' ? https : http;
237
- const options = {
238
- hostname: urlObj.hostname,
239
- port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
240
- path: urlObj.pathname + urlObj.search,
241
- method: 'GET'
242
- };
243
- if (debug) {
244
- logger.log(chalk.gray(`[DEBUG] Health check request: ${healthCheckUrl}`));
245
- logger.log(chalk.gray(`[DEBUG] Request options: ${JSON.stringify(options, null, 2)}`));
246
- }
247
- const timer = { id: null };
248
- const req = transport.request(options, (res) => {
249
- clearTimeout(timer.id);
250
- let data = '';
251
- if (debug) {
252
- logger.log(chalk.gray(`[DEBUG] Response status code: ${res.statusCode}`));
253
- logger.log(chalk.gray(`[DEBUG] Response headers: ${JSON.stringify(res.headers, null, 2)}`));
254
- }
255
- res.on('data', (chunk) => {
256
- data += chunk;
257
- });
258
- res.on('end', () => handleHealthResponse(res, data, debug, resolve));
259
- });
260
- timer.id = setTimeout(() => {
261
- if (debug) logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
262
- req.destroy();
263
- resolve(false);
264
- }, 5000);
265
- req.on('error', (error) => {
266
- clearTimeout(timer.id);
267
- if (debug) logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
268
- resolve(false);
269
- });
270
- req.end();
271
- } catch (error) {
272
- if (debug) logger.log(chalk.gray(`[DEBUG] Health check exception: ${error.message}`));
273
- reject(error);
274
- }
275
- }
276
-
277
- /**
278
- * Checks health endpoint
279
- * @async
280
- * @function checkHealthEndpoint
281
- * @param {string} healthCheckUrl - Health check URL
282
- * @param {boolean} [debug=false] - Enable debug logging
283
- * @returns {Promise<boolean>} True if healthy
284
- * @throws {Error} If request fails with exception
285
- */
286
- async function checkHealthEndpoint(healthCheckUrl, debug = false) {
287
- return new Promise((resolve, reject) => {
288
- doHealthCheckRequest(healthCheckUrl, debug, resolve, reject);
289
- });
290
- }
291
-
292
- /**
293
- * Waits for application health check to pass
294
- * Checks HTTP endpoint and waits for healthy response
295
- *
296
- * @async
297
- * @function waitForHealthCheck
298
- * @param {string} appName - Application name
299
- * @param {number} timeout - Timeout in seconds (default: 90)
300
- * @param {number} [port] - Application port (auto-detected if not provided)
301
- * @param {Object} [config] - Application configuration
302
- * @param {boolean} [debug=false] - Enable debug logging
303
- * @returns {Promise<void>} Resolves when health check passes
304
- * @throws {Error} If health check times out
305
- */
306
194
  /**
307
195
  * Determines health check port
308
196
  * @async
@@ -121,6 +121,7 @@ const CATEGORIES = [
121
121
  commands: [
122
122
  { name: 'dev' },
123
123
  { name: 'secret' },
124
+ { name: 'npm', term: 'npm config' },
124
125
  { name: 'secure' }
125
126
  ]
126
127
  }
@@ -31,7 +31,11 @@ async function probeRealmWellKnown(url) {
31
31
  const controller = new AbortController();
32
32
  const timer = setTimeout(() => controller.abort(), 8000);
33
33
  try {
34
- const res = await fetch(url, { method: 'GET', signal: controller.signal });
34
+ const { fetchWithOptionalDevPlatformTls } = require('./dev-platform-tls');
35
+ const res = await fetchWithOptionalDevPlatformTls(url, {
36
+ method: 'GET',
37
+ signal: controller.signal
38
+ });
35
39
  return res.ok;
36
40
  } catch {
37
41
  return false;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Loopback hostnames always use http for declarative public URLs.
3
+ *
4
+ * @fileoverview Localhost hostname detection for public URL scheme
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * @param {string} hostname
13
+ * @returns {boolean}
14
+ */
15
+ function isLocalhostPublicSchemeHostname(hostname) {
16
+ const h = String(hostname || '').trim().toLowerCase();
17
+ if (!h) {
18
+ return false;
19
+ }
20
+ if (h === 'localhost') {
21
+ return true;
22
+ }
23
+ if (h === '127.0.0.1') {
24
+ return true;
25
+ }
26
+ return h === '::1';
27
+ }
28
+
29
+ module.exports = {
30
+ isLocalhostPublicSchemeHostname
31
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Merge npm user-global auth lines from BASH_NPM_TOKEN / NPM_TOKEN.
3
+ *
4
+ * @fileoverview User-level ~/.npmrc auth for @aifabrix scope (not repo .npmrc)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Keys managed by aifabrix npm config (replaced or removed on each run).
17
+ * `always-auth` is legacy — npm 11+ warns at userconfig; auth uses scoped registry + _authToken.
18
+ */
19
+ const MANAGED_KEYS = new Set([
20
+ '@aifabrix:registry',
21
+ 'always-auth',
22
+ '//registry.npmjs.org/:_authToken'
23
+ ]);
24
+
25
+ const REGISTRY_URL = 'https://registry.npmjs.org/';
26
+
27
+ /**
28
+ * Resolve npm userconfig file path (~/.npmrc by default).
29
+ *
30
+ * @returns {string}
31
+ */
32
+ function getUserNpmrcPath() {
33
+ const fromEnv = process.env.npm_config_userconfig || process.env.NPM_CONFIG_USERCONFIG;
34
+ if (fromEnv && typeof fromEnv === 'string' && fromEnv.trim()) {
35
+ return path.resolve(fromEnv.trim());
36
+ }
37
+ return path.join(os.homedir(), '.npmrc');
38
+ }
39
+
40
+ /**
41
+ * Parse a single .npmrc line into key/value (ignores comments and blanks).
42
+ *
43
+ * @param {string} line
44
+ * @returns {{ key: string, value: string }|null}
45
+ */
46
+ function parseNpmrcLine(line) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
49
+ return null;
50
+ }
51
+ const eq = trimmed.indexOf('=');
52
+ if (eq === -1) {
53
+ return { key: trimmed, value: '' };
54
+ }
55
+ return {
56
+ key: trimmed.slice(0, eq).trim(),
57
+ value: trimmed.slice(eq + 1).trim()
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Build managed auth lines for @aifabrix private registry.
63
+ *
64
+ * @param {string} token - Bare npm automation token (never logged by callers)
65
+ * @returns {string[]}
66
+ */
67
+ function buildManagedLines(token) {
68
+ return [
69
+ `@aifabrix:registry=${REGISTRY_URL}`,
70
+ `//registry.npmjs.org/:_authToken=${token}`
71
+ ];
72
+ }
73
+
74
+ /**
75
+ * Merge managed lines into existing .npmrc content (idempotent).
76
+ *
77
+ * @param {string} existingContent
78
+ * @param {string} token
79
+ * @returns {string}
80
+ */
81
+ function mergeNpmrcContent(existingContent, token) {
82
+ const kept = [];
83
+ const lines = (existingContent || '').split(/\r?\n/);
84
+ for (const line of lines) {
85
+ const parsed = parseNpmrcLine(line);
86
+ if (parsed && MANAGED_KEYS.has(parsed.key)) {
87
+ continue;
88
+ }
89
+ kept.push(line);
90
+ }
91
+ while (kept.length > 0 && kept[kept.length - 1] === '') {
92
+ kept.pop();
93
+ }
94
+ const managed = buildManagedLines(token);
95
+ if (kept.length > 0) {
96
+ return `${kept.join('\n')}\n${managed.join('\n')}\n`;
97
+ }
98
+ return `${managed.join('\n')}\n`;
99
+ }
100
+
101
+ /**
102
+ * Write merged user-global npm auth from token.
103
+ *
104
+ * @param {string} token
105
+ * @param {{ dryRun?: boolean, npmrcPath?: string }} [options]
106
+ * @returns {{ npmrcPath: string, managedKeys: string[], dryRun: boolean, wrote: boolean }}
107
+ */
108
+ function writeUserNpmrcFromToken(token, options = {}) {
109
+ const npmrcPath = options.npmrcPath || getUserNpmrcPath();
110
+ const dryRun = Boolean(options.dryRun);
111
+ const managedKeys = [...MANAGED_KEYS];
112
+ if (!token || !String(token).trim()) {
113
+ throw new Error('NPM token is required to update user npm config');
114
+ }
115
+ const existing = fs.existsSync(npmrcPath) ? fs.readFileSync(npmrcPath, 'utf8') : '';
116
+ const merged = mergeNpmrcContent(existing, String(token).trim());
117
+ if (!dryRun) {
118
+ const dir = path.dirname(npmrcPath);
119
+ if (!fs.existsSync(dir)) {
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ }
122
+ fs.writeFileSync(npmrcPath, merged, { mode: 0o600 });
123
+ try {
124
+ fs.chmodSync(npmrcPath, 0o600);
125
+ } catch {
126
+ // best-effort on platforms that restrict chmod
127
+ }
128
+ }
129
+ return {
130
+ npmrcPath,
131
+ managedKeys,
132
+ dryRun,
133
+ wrote: !dryRun
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ MANAGED_KEYS,
139
+ REGISTRY_URL,
140
+ getUserNpmrcPath,
141
+ parseNpmrcLine,
142
+ buildManagedLines,
143
+ mergeNpmrcContent,
144
+ writeUserNpmrcFromToken
145
+ };
@@ -103,12 +103,12 @@ async function computeAppBaseUrl(appName) {
103
103
  const userCfg = await config.getConfig();
104
104
  const remoteServer = await config.getRemoteServer();
105
105
  const infraTlsEnabled = Boolean(userCfg && userCfg.tlsEnabled);
106
+ const setupPlatformMode = await config.getSetupPlatformMode();
106
107
 
107
108
  const fd = variables.frontDoorRouting || null;
108
109
  const frontDoorEnabled = Boolean(fd && fd.enabled === true);
109
110
  const traefikOn = Boolean(userCfg && userCfg.traefik);
110
111
  const pathActive = Boolean(traefikOn && frontDoorEnabled);
111
- const setupPlatformMode = await config.getSetupPlatformMode();
112
112
 
113
113
  const publicBase = computePublicUrlBaseString({
114
114
  traefik: traefikOn,
@@ -120,7 +120,8 @@ async function computeAppBaseUrl(appName) {
120
120
  profile: 'docker',
121
121
  listenPort: basePort,
122
122
  developerIdNum,
123
- infraTlsEnabled
123
+ infraTlsEnabled,
124
+ setupPlatformMode
124
125
  });
125
126
 
126
127
  if (pathActive) {
@@ -148,10 +149,62 @@ async function resolvePlatformControllerUrl() {
148
149
  return computeAppBaseUrl(MISO_CONTROLLER_APP);
149
150
  }
150
151
 
152
+ /**
153
+ * Direct localhost controller URL (published Docker port + optional front-door mount).
154
+ * Used when the CLI runs on the builder host but the public Traefik URL is unreachable.
155
+ *
156
+ * @async
157
+ * @param {string} [appName]
158
+ * @returns {Promise<string>}
159
+ */
160
+ async function resolveLocalhostPlatformControllerUrl(appName = MISO_CONTROLLER_APP) {
161
+ const variables = loadAppVariables(appName);
162
+ const developerIdRaw = await config.getDeveloperId();
163
+ const developerIdNum =
164
+ typeof developerIdRaw === 'string' ? parseInt(developerIdRaw, 10) : developerIdRaw;
165
+ const basePort = Number(variables.port || 3000);
166
+ const hostPort = basePort + (Number.isFinite(developerIdNum) ? developerIdNum : 0) * 100;
167
+ const fd = variables.frontDoorRouting;
168
+ if (fd && fd.enabled === true && fd.pattern) {
169
+ const mount = normalizeFrontDoorPatternForHealth(fd.pattern);
170
+ if (mount && mount !== '/') {
171
+ return joinUrlPath(`http://localhost:${hostPort}`, mount).replace(/\/+$/, '');
172
+ }
173
+ }
174
+ return `http://localhost:${hostPort}`;
175
+ }
176
+
177
+ /**
178
+ * Controller URL for CLI HTTP calls on a remote dev host (builder-server).
179
+ * Keeps the public Traefik URL when reachable; otherwise falls back to localhost.
180
+ *
181
+ * @async
182
+ * @returns {Promise<string>}
183
+ */
184
+ async function resolveCliPlatformControllerUrl() {
185
+ const publicUrl = await resolvePlatformControllerUrl();
186
+ const remoteServer = await config.getRemoteServer();
187
+ if (!remoteServer || !String(remoteServer).trim()) {
188
+ return publicUrl;
189
+ }
190
+ try {
191
+ const { checkHealthEndpoint } = require('./health-check-request');
192
+ const healthUrl = `${String(publicUrl).replace(/\/+$/, '')}/health`;
193
+ if (await checkHealthEndpoint(healthUrl)) {
194
+ return publicUrl;
195
+ }
196
+ } catch {
197
+ // fall through to localhost
198
+ }
199
+ return resolveLocalhostPlatformControllerUrl(MISO_CONTROLLER_APP);
200
+ }
201
+
151
202
  module.exports = {
152
203
  MISO_CONTROLLER_APP,
153
204
  loadAppVariables,
154
205
  loadBundledApplicationVariables,
155
206
  computeAppBaseUrl,
156
- resolvePlatformControllerUrl
207
+ resolvePlatformControllerUrl,
208
+ resolveLocalhostPlatformControllerUrl,
209
+ resolveCliPlatformControllerUrl
157
210
  };
@@ -10,26 +10,11 @@
10
10
 
11
11
  const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
12
12
  const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
13
-
14
- /**
15
- * Public URL authorities that must use `http://` (browsers hit loopback without TLS in practice).
16
- *
17
- * @param {string} hostname - Parsed URL hostname
18
- * @returns {boolean}
19
- */
20
- function isLocalhostPublicSchemeHostname(hostname) {
21
- const h = String(hostname || '').trim().toLowerCase();
22
- if (!h) {
23
- return false;
24
- }
25
- if (h === 'localhost') {
26
- return true;
27
- }
28
- if (h === '127.0.0.1') {
29
- return true;
30
- }
31
- return h === '::1';
32
- }
13
+ const {
14
+ normalizeSetupPlatformMode,
15
+ SETUP_PLATFORM_MODE
16
+ } = require('../core/setup-platform-mode');
17
+ const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
33
18
 
34
19
  /**
35
20
  * Expand frontDoorRouting.host placeholders for url:// (same rules as Traefik labels in compose-generator).
@@ -156,6 +141,28 @@ function templateUsesRemoteHostPlaceholder(hostTemplate) {
156
141
  return /\$\{REMOTE_HOST\}/.test(String(hostTemplate || ''));
157
142
  }
158
143
 
144
+ /**
145
+ * HTTPS for Traefik front-door public host when infra TLS is on, remote-server is https,
146
+ * or setup mode is single-traefik/full (builder-server nginx edge TLS).
147
+ *
148
+ * @param {Object} opts
149
+ * @param {boolean} [opts.infraTlsEnabled]
150
+ * @param {string|null|undefined} [opts.remoteServer]
151
+ * @param {string|null|undefined} [opts.setupPlatformMode]
152
+ * @returns {boolean}
153
+ */
154
+ function traefikFrontDoorUsesHttps(opts = {}) {
155
+ if (opts.infraTlsEnabled) {
156
+ return true;
157
+ }
158
+ const remote = String(opts.remoteServer || '').trim();
159
+ if (remote && /^https:\/\//i.test(remote)) {
160
+ return true;
161
+ }
162
+ const mode = normalizeSetupPlatformMode(opts.setupPlatformMode);
163
+ return mode === SETUP_PLATFORM_MODE.SINGLE_TRAEFIK || mode === SETUP_PLATFORM_MODE.FULL;
164
+ }
165
+
159
166
  /**
160
167
  * @param {Object} opts - same shape as computePublicUrlBaseString
161
168
  * @returns {string|null}
@@ -167,7 +174,6 @@ function buildTraefikPublicBaseIfApplicable(opts) {
167
174
  hostTemplate,
168
175
  developerIdRaw,
169
176
  remoteServer,
170
- infraTlsEnabled,
171
177
  profile,
172
178
  developerIdNum
173
179
  } = opts;
@@ -189,8 +195,7 @@ function buildTraefikPublicBaseIfApplicable(opts) {
189
195
  if (!expanded) {
190
196
  return null;
191
197
  }
192
- // Scheme follows global `tlsEnabled` (`up-infra --tls`) only — not `frontDoorRouting.tls` when infra TLS is off.
193
- const useHttps = Boolean(infraTlsEnabled);
198
+ const useHttps = traefikFrontDoorUsesHttps(opts);
194
199
  let scheme = useHttps ? 'https' : 'http';
195
200
  if (isLocalhostPublicSchemeHostname(expanded)) {
196
201
  scheme = 'http';
@@ -210,7 +215,8 @@ function buildTraefikPublicBaseIfApplicable(opts) {
210
215
  * @param {'docker'|'local'} opts.profile
211
216
  * @param {number} opts.listenPort
212
217
  * @param {number} opts.developerIdNum
213
- * @param {boolean} [opts.infraTlsEnabled] - `tlsEnabled` from ~/.aifabrix/config.yaml (`up-infra --tls`); Traefik front-door public bases use **`https`** only when this is **true** (even if `frontDoorRouting.tls` is **false**). When **false**, front-door bases use **`http`** regardless of `frontDoorRouting.tls`. Loopback hostnames stay **http** always.
218
+ * @param {boolean} [opts.infraTlsEnabled] - `tlsEnabled` from ~/.aifabrix/config.yaml (`up-infra --tls`). Traefik front-door public bases also use **`https`** when `remote-server` is **`https://`** or `setupPlatformMode` is **`single-traefik`** / **`full`** (builder-server nginx edge TLS). Loopback hostnames stay **http** always.
219
+ * @param {string|null|undefined} [opts.setupPlatformMode] - From config; used with remote-server for edge TLS scheme
214
220
  * @param {boolean} [opts.pathActive] - traefik ∧ frontDoorRouting.enabled; required for Traefik host branch (plan 124)
215
221
  * @param {boolean|undefined} [opts.declarativePublicUrlsUseLocalhost] - **`true`**: force localhost authority (+ port rules below). **`false`**: allow `remote-server` as authority when Traefik is on, or when global `traefik` is not `false` and per-app `proxy` opts in without Traefik. **`undefined`**: default — omit `remote-server` when Traefik is off so published services use `http://localhost:<port>` (e.g. `af setup` / device login URLs). When user config has **`traefik: false`**, expansion forces localhost for public bases regardless of per-app `proxy`.
216
222
  * @returns {string}
@@ -270,5 +276,7 @@ function computePublicUrlBaseString(opts) {
270
276
  module.exports = {
271
277
  expandFrontDoorHostTemplateForUrls,
272
278
  computePublicUrlBaseString,
273
- resolveHostPortForDeclarativePublic
279
+ resolveHostPortForDeclarativePublic,
280
+ isLocalhostPublicSchemeHostname,
281
+ traefikFrontDoorUsesHttps
274
282
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.47.0",
3
+ "version": "2.47.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/.npmrc.token DELETED
@@ -1 +0,0 @@
1
- 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
@@ -1 +0,0 @@
1
- {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
@@ -1,17 +0,0 @@
1
- apiVersion: dataplane.aifabrix.ai/v1
2
- kind: Protection
3
- metadata:
4
- key: hubspot-country-sales
5
- displayName: HubSpot Country Sales Access
6
- spec:
7
- enabled: true
8
- datasourceKey: hubspot-companies
9
- mode: replaceForSource
10
- rules:
11
- - key: sales-country-users
12
- principal:
13
- type: group
14
- expression: "Sales {{fk.country.metadata.iso3}} Users"
15
- grants:
16
- - dimensionKey: country
17
- valueExpression: "{{fk.country.metadata.iso2}}"
@@ -1 +0,0 @@
1
- {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}