@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,83 @@
1
+ /**
2
+ * Docker build-arg resolution from resolved env and shell overlays.
3
+ *
4
+ * @fileoverview Build-time args for `aifabrix build` / docker build
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const secretsEnvWrite = require('../core/secrets-env-write');
13
+ const { assertMisoControllerNpmSdkDeps } = require('./miso-controller-npm-sdk-guard');
14
+
15
+ const MISO_CONTROLLER_UI_BUILD_ARG_KEYS = [
16
+ 'MISO_RELATIVE_PATH',
17
+ 'MISO_WEB_SERVER_URL',
18
+ 'VITE_BASE_PATH',
19
+ 'KEYCLOAK_SERVER_URL',
20
+ 'KEYCLOAK_REALM',
21
+ 'KEYCLOAK_CLIENT_ID'
22
+ ];
23
+
24
+ /**
25
+ * @param {Object.<string, string>} buildArgs
26
+ * @param {Object.<string, string>} envMap
27
+ */
28
+ function applyMisoControllerUiBuildArgs(buildArgs, envMap) {
29
+ for (const key of MISO_CONTROLLER_UI_BUILD_ARG_KEYS) {
30
+ const value = envMap[key];
31
+ if (typeof value === 'string' && value.trim() && !value.includes('://')) {
32
+ buildArgs[key] = value.trim();
33
+ }
34
+ }
35
+ if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_RELATIVE_PATH) {
36
+ buildArgs.VITE_BASE_PATH = buildArgs.MISO_RELATIVE_PATH;
37
+ }
38
+ if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_WEB_SERVER_URL) {
39
+ try {
40
+ const pathname = new URL(buildArgs.MISO_WEB_SERVER_URL).pathname.replace(/\/+$/, '');
41
+ if (pathname && pathname !== '/') {
42
+ buildArgs.VITE_BASE_PATH = pathname;
43
+ }
44
+ } catch {
45
+ // ignore invalid public URL
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve Docker build-args from resolved env map and merged BASH_* secrets.
52
+ *
53
+ * @param {string} appName
54
+ * @param {string} [contextPath] Docker build context (repo root for miso-controller)
55
+ * @returns {Promise<Object.<string, string>>}
56
+ */
57
+ async function resolveDockerBuildArgsForApp(appName, contextPath) {
58
+ const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
59
+ const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
60
+ const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
61
+ const buildArgs = {};
62
+ if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
63
+ if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
64
+ for (const [k, v] of Object.entries(bashOverlay)) {
65
+ if (v && buildArgs[k] === undefined) buildArgs[k] = v;
66
+ }
67
+ const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
68
+ const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
69
+ if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
70
+ if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
71
+ if (appName === 'miso-controller') {
72
+ applyMisoControllerUiBuildArgs(buildArgs, envMap);
73
+ }
74
+ if (appName === 'miso-controller' && contextPath) {
75
+ assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
76
+ }
77
+ return buildArgs;
78
+ }
79
+
80
+ module.exports = {
81
+ resolveDockerBuildArgsForApp,
82
+ applyMisoControllerUiBuildArgs
83
+ };
@@ -35,9 +35,8 @@ const dockerBuild = require('../utils/docker-build');
35
35
  const buildCopy = require('../utils/build-copy');
36
36
  const { resolveBuildDockerTagNames } = require('../utils/build-resolve-image');
37
37
  const buildHelpers = require('../utils/build-helpers');
38
- const secretsEnvWrite = require('../core/secrets-env-write');
39
38
  const { ensureRunSecretsForApp } = require('../app/run-env-compose');
40
- const { assertMisoControllerNpmSdkDeps } = require('./miso-controller-npm-sdk-guard');
39
+ const { resolveDockerBuildArgsForApp } = require('./docker-build-args');
41
40
 
42
41
  /**
43
42
  * Loads application config for an application
@@ -394,33 +393,6 @@ async function handleDockerfileGeneration(appName, params, options, buildHelpers
394
393
  }, generateDockerfile);
395
394
  }
396
395
 
397
- /**
398
- * Resolve Docker build-args from resolved env map and merged BASH_* secrets.
399
- * @param {string} appName
400
- * @param {string} [contextPath] Docker build context (repo root for miso-controller)
401
- * @returns {Promise<Object.<string, string>>}
402
- */
403
- async function resolveDockerBuildArgsForApp(appName, contextPath) {
404
- const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
405
- const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
406
- const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
407
- const buildArgs = {};
408
- if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
409
- if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
410
- for (const [k, v] of Object.entries(bashOverlay)) {
411
- if (v && buildArgs[k] === undefined) buildArgs[k] = v;
412
- }
413
- // Exported shell tokens win (local dev: export NPM_TOKEN=... before af build / docker build).
414
- const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
415
- const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
416
- if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
417
- if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
418
- if (appName === 'miso-controller' && contextPath) {
419
- assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
420
- }
421
- return buildArgs;
422
- }
423
-
424
396
  /**
425
397
  * Standard Docker build path after CLI header is printed (non-external apps).
426
398
  * @param {string} appName
package/lib/cli/index.js CHANGED
@@ -31,6 +31,7 @@ const { setupPlatformCommands } = require('./setup-platform');
31
31
  const { setupCertificationCommands } = require('./setup-certification-commands');
32
32
  const { setupGovernanceCommands } = require('../commands/governance');
33
33
  const { setupChannelCommands } = require('../commands/channel');
34
+ const { setupNpmCommands } = require('./setup-npm');
34
35
 
35
36
  /**
36
37
  * Dev, secrets, parameters, and platform commands (keeps setupCommands under statement limit).
@@ -39,6 +40,7 @@ const { setupChannelCommands } = require('../commands/channel');
39
40
  function setupDevAndPlatformCommands(program) {
40
41
  setupDevCommands(program);
41
42
  setupSecretsCommands(program);
43
+ setupNpmCommands(program);
42
44
  setupParametersCommands(program);
43
45
  setupPlatformCommands(program);
44
46
  }
@@ -11,7 +11,7 @@
11
11
  const chalk = require('chalk');
12
12
  const logger = require('../utils/logger');
13
13
  const config = require('../core/config');
14
- const { resolvePlatformControllerUrl } = require('../utils/platform-controller-url');
14
+ const { resolvePlatformControllerUrl, resolveCliPlatformControllerUrl } = require('../utils/platform-controller-url');
15
15
  const { withMutedLogger } = require('../utils/with-muted-logger');
16
16
  const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
17
17
  const { isAuthenticationError } = require('../utils/cli-utils');
@@ -19,29 +19,45 @@ const { resolveControllerUrl, isPlatformAuthValidForController } = require('../u
19
19
 
20
20
  /**
21
21
  * @param {Object} authOpts
22
- * @returns {Promise<string>}
22
+ * @returns {Promise<{ apiUrl: string, tokenUrl: string }>}
23
23
  */
24
- async function resolveGuidedAuthControllerUrl(authOpts = {}) {
25
- return (
24
+ async function resolveGuidedAuthControllerUrls(authOpts = {}) {
25
+ const tokenUrl =
26
26
  authOpts.platformControllerUrl ||
27
27
  (await config.getPlatformControllerUrl()) ||
28
- (await resolvePlatformControllerUrl())
29
- );
28
+ (await resolvePlatformControllerUrl());
29
+ const remoteServer = await config.getRemoteServer();
30
+ let apiUrl = tokenUrl;
31
+ if (remoteServer && String(remoteServer).trim()) {
32
+ apiUrl = await resolveCliPlatformControllerUrl();
33
+ }
34
+ return { apiUrl, tokenUrl };
35
+ }
36
+
37
+ /**
38
+ * @param {Object} authOpts
39
+ * @returns {Promise<string>}
40
+ */
41
+ async function resolveGuidedAuthControllerUrl(authOpts = {}) {
42
+ const { apiUrl } = await resolveGuidedAuthControllerUrls(authOpts);
43
+ return apiUrl;
30
44
  }
31
45
 
32
46
  /**
33
47
  * @async
34
48
  * @param {Function} handleLoginFn
35
- * @param {string} controllerUrl
36
- * @param {{ skipDataplaneVersionRefresh?: boolean }} [opts]
49
+ * @param {string} controllerUrl - Controller API base URL (may be localhost on builder host)
50
+ * @param {{ skipDataplaneVersionRefresh?: boolean, tokenControllerUrl?: string }} [opts]
37
51
  * @returns {Promise<void>}
38
52
  */
39
53
  async function runGuidedDeviceLogin(handleLoginFn, controllerUrl, opts = {}) {
40
54
  await handleLoginFn({
41
55
  method: 'device',
42
56
  controller: controllerUrl,
57
+ tokenControllerUrl: opts.tokenControllerUrl,
43
58
  environment: 'dev',
44
59
  compact: true,
60
+ skipControllerUrlPersist: Boolean(opts.tokenControllerUrl),
45
61
  skipDataplaneVersionRefresh: opts.skipDataplaneVersionRefresh === true,
46
62
  exitOnFailure: false
47
63
  });
@@ -56,20 +72,27 @@ async function runGuidedDeviceLogin(handleLoginFn, controllerUrl, opts = {}) {
56
72
  */
57
73
  async function runGuidedAuthStep(handleLoginFn, authOpts, spinners) {
58
74
  const spin = spinners.startSpinner('Authenticating...');
59
- const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
75
+ const { apiUrl, tokenUrl } = await resolveGuidedAuthControllerUrls(authOpts);
76
+ const loginOpts = { skipDataplaneVersionRefresh: true, tokenControllerUrl: tokenUrl };
60
77
  const skipLogin =
61
78
  authOpts.skipLoginIfAuthenticated === true ||
62
- (await isPlatformAuthValidForController(controllerUrl));
79
+ (await isPlatformAuthValidForController(tokenUrl));
63
80
  try {
64
81
  if (skipLogin) {
65
82
  spinners.stopSpinnerSuccess(spin, 'Already authenticated');
66
83
  return;
67
84
  }
68
- await runGuidedDeviceLogin(handleLoginFn, controllerUrl, { skipDataplaneVersionRefresh: true });
85
+ await runGuidedDeviceLogin(handleLoginFn, apiUrl, loginOpts);
69
86
  } catch (authErr) {
70
87
  if (spin) spin.fail('Authentication required');
88
+ const retriable =
89
+ isAuthenticationError(authErr) ||
90
+ /device code|authorization_pending|complete sign-in|browser/i.test(String(authErr.message || ''));
91
+ if (!retriable) {
92
+ throw authErr;
93
+ }
71
94
  logger.error(formatBlockingError('Authentication required. Complete sign-in in your browser.'));
72
- await runGuidedDeviceLogin(handleLoginFn, controllerUrl, { skipDataplaneVersionRefresh: true });
95
+ await runGuidedDeviceLogin(handleLoginFn, apiUrl, loginOpts);
73
96
  }
74
97
  spinners.stopSpinnerSuccess(spin, 'Authenticated');
75
98
  }
@@ -102,6 +125,7 @@ async function runGuidedDataplaneWithAuthRecovery(options, handleUpDataplane, ha
102
125
  }
103
126
 
104
127
  module.exports = {
128
+ resolveGuidedAuthControllerUrls,
105
129
  resolveGuidedAuthControllerUrl,
106
130
  runGuidedDeviceLogin,
107
131
  runGuidedAuthStep,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * CLI npm command setup (user-global registry auth).
3
+ *
4
+ * @fileoverview aifabrix npm config command registration
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const { handleCommandError } = require('../utils/cli-utils');
10
+ const { handleNpmConfig } = require('../commands/npm-config');
11
+
12
+ const NPM_CONFIG_HELP_AFTER = `
13
+ Writes @aifabrix scope registry auth to your user npm config (~/.npmrc by default).
14
+ Does not modify repo-level .npmrc files.
15
+
16
+ Token source (default): merged secrets key BASH_NPM_TOKEN (user or --shared store).
17
+ Docker/build: BASH_NPM_TOKEN is also mapped to NPM_TOKEN for aifabrix build.
18
+
19
+ Examples:
20
+ $ aifabrix secret set BASH_NPM_TOKEN "<token>" --shared
21
+ $ aifabrix npm config
22
+ $ aifabrix npm config --check
23
+ $ aifabrix npm config --dry-run
24
+ $ NPM_TOKEN="<token>" aifabrix npm config --from-env
25
+
26
+ Migration from repo .npmrc.token:
27
+ 1. aifabrix secret remove npm-token-secretKeyVault --shared
28
+ 2. aifabrix secret set BASH_NPM_TOKEN "<token>" --shared
29
+ 3. aifabrix npm config
30
+ 4. npm whoami --registry=https://registry.npmjs.org/
31
+ `;
32
+
33
+ /**
34
+ * @param {import('commander').Command} program
35
+ */
36
+ function setupNpmCommands(program) {
37
+ const npm = program.command('npm').description('User-global npm registry configuration');
38
+
39
+ npm
40
+ .command('config')
41
+ .description('Apply @aifabrix npm auth from BASH_NPM_TOKEN to user ~/.npmrc')
42
+ .option('--dry-run', 'Show what would be written without changing files')
43
+ .option('--check', 'Exit 0 when BASH_NPM_TOKEN is set and npm whoami succeeds')
44
+ .option('--from-env', 'Use NPM_TOKEN from the environment once (not merged secrets)')
45
+ .addHelpText('after', NPM_CONFIG_HELP_AFTER)
46
+ .action(async(opts) => {
47
+ try {
48
+ await handleNpmConfig({
49
+ dryRun: Boolean(opts.dryRun),
50
+ check: Boolean(opts.check),
51
+ fromEnv: Boolean(opts.fromEnv)
52
+ });
53
+ } catch (error) {
54
+ handleCommandError(error, 'npm config');
55
+ }
56
+ });
57
+ }
58
+
59
+ module.exports = { setupNpmCommands };
@@ -101,7 +101,7 @@ Examples:
101
101
  const SECRET_SET_SECRETS_FILE_HELP_AFTER = `
102
102
  Examples:
103
103
  $ aifabrix secret set-secrets-file ./shared-secrets.yaml
104
- $ aifabrix secret set-secrets-file https://dev.example.com/api/secrets
104
+ $ aifabrix secret set-secrets-file https://builder01.local/api/dev/secrets
105
105
  $ aifabrix secret set-secrets-file ""
106
106
  `;
107
107
 
@@ -81,8 +81,11 @@ async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expires
81
81
  * @returns {Promise<void>}
82
82
  */
83
83
  async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey, opts = {}) {
84
- await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
85
- await setControllerUrl(controllerUrl);
84
+ const storageUrl = opts.tokenControllerUrl
85
+ ? String(opts.tokenControllerUrl).trim().replace(/\/+$/, '')
86
+ : controllerUrl;
87
+ await saveDeviceLoginConfig(storageUrl, token, refreshToken, expiresAt);
88
+ await setControllerUrl(storageUrl);
86
89
  if (envKey) {
87
90
  await setCurrentEnvironment(envKey);
88
91
  }
@@ -219,6 +222,8 @@ function convertDeviceCodeResponse(apiResponse) {
219
222
  device_code: deviceCodeData.deviceCode || deviceCodeData.device_code,
220
223
  user_code: deviceCodeData.userCode || deviceCodeData.user_code,
221
224
  verification_uri: deviceCodeData.verificationUri || deviceCodeData.verification_uri,
225
+ verification_uri_complete:
226
+ deviceCodeData.verificationUriComplete || deviceCodeData.verification_uri_complete,
222
227
  expires_in: deviceCodeData.expiresIn || deviceCodeData.expires_in || 600,
223
228
  interval: deviceCodeData.interval || 5
224
229
  };
@@ -246,8 +251,11 @@ function logDeviceFlowError(deviceError, opts) {
246
251
  }
247
252
 
248
253
  function printCompactDeviceUrl(deviceCodeResponse) {
254
+ const visitUrl =
255
+ deviceCodeResponse.verification_uri_complete ||
256
+ `${deviceCodeResponse.verification_uri}?user_code=${deviceCodeResponse.user_code}`;
249
257
  logger.log('\nOpen browser to authenticate:');
250
- logger.log(chalk.yellow(`${deviceCodeResponse.verification_uri}?user_code=${deviceCodeResponse.user_code}\n`));
258
+ logger.log(chalk.yellow(`${visitUrl}\n`));
251
259
  }
252
260
 
253
261
  /**
@@ -86,8 +86,12 @@ async function normalizeControllerUrl(options) {
86
86
  controllerUrl = await resolveControllerUrl();
87
87
  }
88
88
  controllerUrl = String(controllerUrl).trim().replace(/\/+$/, '');
89
- // Save controller URL to config
90
- await setControllerUrl(controllerUrl);
89
+ if (!options.skipControllerUrlPersist) {
90
+ const persistUrl = options.tokenControllerUrl
91
+ ? String(options.tokenControllerUrl).trim().replace(/\/+$/, '')
92
+ : controllerUrl;
93
+ await setControllerUrl(persistUrl);
94
+ }
91
95
  if (!options.compact) {
92
96
  logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
93
97
  }
@@ -158,7 +162,8 @@ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
158
162
  async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
159
163
  return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope, {
160
164
  compact: Boolean(options.compact),
161
- exitOnFailure: options.exitOnFailure !== false
165
+ exitOnFailure: options.exitOnFailure !== false,
166
+ tokenControllerUrl: options.tokenControllerUrl
162
167
  });
163
168
  }
164
169
 
@@ -0,0 +1,103 @@
1
+ /**
2
+ * aifabrix npm config — apply user-global npm auth from BASH_NPM_TOKEN.
3
+ *
4
+ * @fileoverview npm config command handler
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { execFile } = require('child_process');
12
+ const { promisify } = require('util');
13
+ const chalk = require('chalk');
14
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
15
+ const logger = require('../utils/logger');
16
+ const secretsLoad = require('../core/secrets-load');
17
+ const { collectBashPrefixedEnv } = require('../utils/bash-secret-env');
18
+ const {
19
+ REGISTRY_URL,
20
+ getUserNpmrcPath,
21
+ writeUserNpmrcFromToken
22
+ } = require('../utils/npm-user-config');
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ /**
27
+ * Resolve npm token from merged secrets or --from-env.
28
+ *
29
+ * @param {{ fromEnv?: boolean }} options
30
+ * @returns {Promise<string>}
31
+ */
32
+ async function resolveNpmToken(options = {}) {
33
+ if (options.fromEnv) {
34
+ const fromProcess = (process.env.NPM_TOKEN || '').trim();
35
+ if (!fromProcess) {
36
+ throw new Error('NPM_TOKEN is not set in the environment (--from-env)');
37
+ }
38
+ return fromProcess;
39
+ }
40
+ const secrets = await secretsLoad.loadSecrets();
41
+ const bashEnv = collectBashPrefixedEnv(secrets);
42
+ const token = bashEnv.NPM_TOKEN;
43
+ if (!token) {
44
+ throw new Error(
45
+ 'BASH_NPM_TOKEN is not set in merged secrets. Run: aifabrix secret set BASH_NPM_TOKEN "<token>" --shared'
46
+ );
47
+ }
48
+ return token;
49
+ }
50
+
51
+ /**
52
+ * Run npm whoami against registry.npmjs.org using the given token.
53
+ *
54
+ * @param {string} token
55
+ * @returns {Promise<string>} username
56
+ */
57
+ async function npmWhoamiWithToken(token) {
58
+ const { stdout } = await execFileAsync(
59
+ 'npm',
60
+ ['whoami', `--registry=${REGISTRY_URL}`, `--//registry.npmjs.org/:_authToken=${token}`],
61
+ { env: { ...process.env }, timeout: 30000 }
62
+ );
63
+ return String(stdout).trim();
64
+ }
65
+
66
+ /**
67
+ * @param {{ dryRun?: boolean, check?: boolean, fromEnv?: boolean }} options
68
+ * @returns {Promise<{ ok: boolean, npmrcPath?: string, username?: string, dryRun?: boolean }>}
69
+ */
70
+ async function handleNpmConfig(options = {}) {
71
+ const dryRun = Boolean(options.dryRun);
72
+ const checkOnly = Boolean(options.check);
73
+ const token = await resolveNpmToken({ fromEnv: Boolean(options.fromEnv) });
74
+ const npmrcPath = getUserNpmrcPath();
75
+
76
+ if (checkOnly) {
77
+ const username = await npmWhoamiWithToken(token);
78
+ return { ok: true, npmrcPath, username };
79
+ }
80
+
81
+ const result = writeUserNpmrcFromToken(token, { dryRun, npmrcPath });
82
+
83
+ if (dryRun) {
84
+ logger.log(chalk.blue('Dry run — would update user npm config:'));
85
+ logger.log(` path: ${npmrcPath}`);
86
+ for (const key of result.managedKeys) {
87
+ const suffix = key.includes('_authToken') ? '=<redacted>' : '';
88
+ logger.log(` ${key}${suffix}`);
89
+ }
90
+ return { ok: true, npmrcPath, dryRun: true };
91
+ }
92
+
93
+ const username = await npmWhoamiWithToken(token);
94
+ logger.log(formatSuccessLine(`Updated user npm config: ${npmrcPath}`));
95
+ logger.log(chalk.gray(`npm whoami: ${username}`));
96
+ return { ok: true, npmrcPath, username };
97
+ }
98
+
99
+ module.exports = {
100
+ resolveNpmToken,
101
+ npmWhoamiWithToken,
102
+ handleNpmConfig
103
+ };
package/lib/utils/api.js CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  const { parseErrorResponse, formatNetworkError } = require('./api-error-handler');
14
14
  const { inferServiceLabelFromUrl } = require('./service-url-label');
15
+ const { fetchWithOptionalDevPlatformTls } = require('./dev-platform-tls');
15
16
  const auditLogger = require('../core/audit-logger');
16
17
 
17
18
  /** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
@@ -350,7 +351,7 @@ async function makeApiCall(url, options = {}) {
350
351
  const { fetchOptions, singleRequestTimeoutMs } = prepareFetchOptionsForRequest(options);
351
352
 
352
353
  try {
353
- const response = await fetch(url, fetchOptions);
354
+ const response = await fetchWithOptionalDevPlatformTls(url, fetchOptions);
354
355
  const duration = Date.now() - startTime;
355
356
 
356
357
  if (!response.ok) {
@@ -145,7 +145,10 @@ async function isControllerHealthReachable(controllerUrl) {
145
145
  return false;
146
146
  }
147
147
  try {
148
- const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(2500) });
148
+ const { fetchWithOptionalDevPlatformTls } = require('./dev-platform-tls');
149
+ const res = await fetchWithOptionalDevPlatformTls(`${base}/health`, {
150
+ signal: AbortSignal.timeout(2500)
151
+ });
149
152
  return res.ok;
150
153
  } catch {
151
154
  return false;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Dev platform HTTPS: builder-server nginx front door uses TLS while infra `tlsEnabled` may be off.
3
+ *
4
+ * @fileoverview Self-signed TLS allowance for remote dev platform URLs
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const https = require('https');
12
+ const config = require('../core/config');
13
+ const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
14
+
15
+ /**
16
+ * @param {string} url
17
+ * @returns {boolean}
18
+ */
19
+ function isHttpsNonLocalhostUrl(url) {
20
+ try {
21
+ const u = new URL(url);
22
+ return u.protocol === 'https:' && !isLocalhostPublicSchemeHostname(u.hostname);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {string} requestHostname
30
+ * @param {string} remoteServerUrl
31
+ * @returns {boolean}
32
+ */
33
+ function hostnameMatchesRemoteDevPlatform(requestHostname, remoteServerUrl) {
34
+ let remoteHost = '';
35
+ try {
36
+ remoteHost = new URL(remoteServerUrl).hostname.toLowerCase();
37
+ } catch {
38
+ return false;
39
+ }
40
+ const host = String(requestHostname || '').trim().toLowerCase();
41
+ if (!host || !remoteHost) {
42
+ return false;
43
+ }
44
+ return host === remoteHost || host.endsWith(`.${remoteHost}`);
45
+ }
46
+
47
+ /**
48
+ * True when URL is a remote dev platform HTTPS endpoint (builder-server nginx / Traefik front door).
49
+ *
50
+ * @async
51
+ * @param {string} url
52
+ * @returns {Promise<boolean>}
53
+ */
54
+ async function shouldAllowInsecureDevPlatformTls(url) {
55
+ if (!isHttpsNonLocalhostUrl(url)) {
56
+ return false;
57
+ }
58
+ try {
59
+ const remote = await config.getRemoteServer();
60
+ const remoteTrimmed = remote && String(remote).trim();
61
+ if (!remoteTrimmed) {
62
+ return false;
63
+ }
64
+ const hostname = new URL(url).hostname;
65
+ return hostnameMatchesRemoteDevPlatform(hostname, remoteTrimmed);
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Minimal fetch for self-signed dev platform HTTPS (Node global fetch cannot set rejectUnauthorized).
73
+ *
74
+ * @param {string} url
75
+ * @param {Object} options
76
+ * @returns {Promise<{ status: number, statusText: string, headers: { get: Function }, text: Function, json: Function }>}
77
+ */
78
+ function nodeHttpsFetchInsecure(url, options = {}) {
79
+ return new Promise((resolve, reject) => {
80
+ const u = new URL(url);
81
+ const method = String(options.method || 'GET').toUpperCase();
82
+ const headers = { ...(options.headers || {}) };
83
+ const body = options.body;
84
+ const reqOpts = {
85
+ hostname: u.hostname,
86
+ port: u.port || 443,
87
+ path: `${u.pathname}${u.search}`,
88
+ method,
89
+ headers,
90
+ rejectUnauthorized: false
91
+ };
92
+
93
+ const req = https.request(reqOpts, (res) => {
94
+ const chunks = [];
95
+ res.on('data', (chunk) => chunks.push(chunk));
96
+ res.on('end', () => {
97
+ const raw = Buffer.concat(chunks).toString('utf8');
98
+ const status = res.statusCode || 0;
99
+ resolve({
100
+ ok: status >= 200 && status < 300,
101
+ status,
102
+ statusText: res.statusMessage || '',
103
+ headers: {
104
+ get: (name) => res.headers[String(name || '').toLowerCase()] || null
105
+ },
106
+ text: async() => raw,
107
+ json: async() => JSON.parse(raw)
108
+ });
109
+ });
110
+ });
111
+
112
+ req.on('error', reject);
113
+ if (options.signal) {
114
+ const onAbort = () => {
115
+ req.destroy(new Error('The operation was aborted'));
116
+ };
117
+ if (options.signal.aborted) {
118
+ onAbort();
119
+ return;
120
+ }
121
+ options.signal.addEventListener('abort', onAbort, { once: true });
122
+ }
123
+ if (body !== undefined && body !== null) {
124
+ req.write(typeof body === 'string' ? body : String(body));
125
+ }
126
+ req.end();
127
+ });
128
+ }
129
+
130
+ /**
131
+ * @async
132
+ * @param {string} url
133
+ * @param {Object} options
134
+ * @returns {Promise<Response>}
135
+ */
136
+ async function fetchWithOptionalDevPlatformTls(url, options = {}) {
137
+ if (await shouldAllowInsecureDevPlatformTls(url)) {
138
+ return nodeHttpsFetchInsecure(url, options);
139
+ }
140
+ return fetch(url, options);
141
+ }
142
+
143
+ module.exports = {
144
+ isHttpsNonLocalhostUrl,
145
+ hostnameMatchesRemoteDevPlatform,
146
+ shouldAllowInsecureDevPlatformTls,
147
+ fetchWithOptionalDevPlatformTls
148
+ };