@aifabrix/builder 2.45.5 → 2.46.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 (231) hide show
  1. package/.cursor/rules/cli-layout.mdc +2 -0
  2. package/.cursor/rules/cli-product-neutral.mdc +43 -0
  3. package/.cursor/rules/project-rules.mdc +1 -0
  4. package/README.md +43 -17
  5. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  6. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  7. package/jest.config.default.js +2 -1
  8. package/jest.config.isolated.js +2 -1
  9. package/jest.global-live-fabrix-hooks.js +36 -0
  10. package/jest.isolated-projects.js +130 -0
  11. package/jest.projects.js +41 -136
  12. package/lib/api/api-response-errors.js +124 -0
  13. package/lib/api/governance-scenario-pack.api.js +159 -0
  14. package/lib/api/index.js +43 -3
  15. package/lib/api/lifecycle.api.js +77 -0
  16. package/lib/api/pipeline.api.js +3 -1
  17. package/lib/api/records-bulk.api.js +14 -1
  18. package/lib/api/validation-runner.js +3 -1
  19. package/lib/app/deploy-config.js +4 -1
  20. package/lib/app/down.js +20 -1
  21. package/lib/app/push.js +4 -5
  22. package/lib/app/register.js +7 -4
  23. package/lib/app/run-env-compose.js +84 -30
  24. package/lib/build/index.js +5 -0
  25. package/lib/certification/sync-system-certification.js +13 -6
  26. package/lib/cli/index.js +4 -0
  27. package/lib/cli/infra-guided-auth.js +109 -0
  28. package/lib/cli/infra-guided.js +84 -81
  29. package/lib/cli/setup-app.help.js +0 -31
  30. package/lib/cli/setup-app.js +28 -2
  31. package/lib/cli/setup-app.test-commands.js +5 -57
  32. package/lib/cli/setup-auth.js +25 -0
  33. package/lib/cli/setup-certification-commands.js +147 -0
  34. package/lib/cli/setup-certification.help.js +141 -0
  35. package/lib/cli/setup-environment.js +19 -2
  36. package/lib/cli/setup-external-system-help.js +62 -0
  37. package/lib/cli/setup-external-system.js +25 -3
  38. package/lib/cli/setup-infra.js +9 -1
  39. package/lib/cli/setup-platform.js +6 -1
  40. package/lib/cli/setup-secrets.js +33 -1
  41. package/lib/cli/setup-utility-help-after.js +22 -2
  42. package/lib/cli/setup-utility-repair.js +198 -0
  43. package/lib/cli/setup-utility-resolve.js +40 -4
  44. package/lib/cli/setup-utility-validate.js +197 -0
  45. package/lib/cli/setup-utility.js +4 -239
  46. package/lib/commands/auth-status-client-tokens.js +214 -0
  47. package/lib/commands/auth-status-dataplane-version.js +58 -20
  48. package/lib/commands/auth-status.js +41 -47
  49. package/lib/commands/datasource-test-trust-cli.js +27 -23
  50. package/lib/commands/datasource-unified-test-cli.js +6 -3
  51. package/lib/commands/datasource-unified-test-cli.options.js +5 -1
  52. package/lib/commands/datasource.js +7 -7
  53. package/lib/commands/dev-show-display.js +8 -1
  54. package/lib/commands/governance-cli-shared.js +94 -0
  55. package/lib/commands/governance-command-action.js +132 -0
  56. package/lib/commands/governance-download-external.js +34 -0
  57. package/lib/commands/governance-generate-external.js +71 -0
  58. package/lib/commands/governance-upload-external.js +36 -0
  59. package/lib/commands/governance-verify-external.js +64 -0
  60. package/lib/commands/governance.js +144 -0
  61. package/lib/commands/lifecycle-command-action.js +123 -0
  62. package/lib/commands/lifecycle-external.js +268 -0
  63. package/lib/commands/login.js +11 -2
  64. package/lib/commands/repair-auth-apply.js +127 -0
  65. package/lib/commands/repair-changed-files.js +41 -0
  66. package/lib/commands/repair-datasource-keys.js +4 -1
  67. package/lib/commands/repair-datasource-run.js +59 -0
  68. package/lib/commands/repair-datasource-test-payload.js +259 -0
  69. package/lib/commands/repair-datasource.js +1 -51
  70. package/lib/commands/repair-env-template.js +64 -13
  71. package/lib/commands/repair-external-run.js +216 -0
  72. package/lib/commands/repair-openapi-sync.js +23 -0
  73. package/lib/commands/repair-persist.js +4 -0
  74. package/lib/commands/repair-rbac.js +52 -1
  75. package/lib/commands/repair-steps.js +145 -0
  76. package/lib/commands/repair.js +29 -296
  77. package/lib/commands/secrets-get.js +184 -0
  78. package/lib/commands/secrets-remove-all.js +3 -1
  79. package/lib/commands/setup-image-refresh.js +146 -0
  80. package/lib/commands/setup-modes.js +158 -162
  81. package/lib/commands/setup-platform-auth.js +76 -0
  82. package/lib/commands/setup-prompts-admin.js +244 -0
  83. package/lib/commands/setup-prompts-platform-mode.js +67 -0
  84. package/lib/commands/setup-prompts-validators.js +36 -0
  85. package/lib/commands/setup-prompts.js +24 -76
  86. package/lib/commands/setup-run-context.js +45 -0
  87. package/lib/commands/setup-spinners.js +39 -0
  88. package/lib/commands/setup.js +27 -5
  89. package/lib/commands/teardown.js +39 -0
  90. package/lib/commands/test-e2e-external.js +24 -8
  91. package/lib/commands/test-governance-external.js +3 -24
  92. package/lib/commands/test-trust-external.js +20 -21
  93. package/lib/commands/up-common.js +25 -13
  94. package/lib/commands/up-dataplane.js +50 -13
  95. package/lib/commands/upload-publish-context.js +75 -0
  96. package/lib/commands/upload-sidecar-followups.js +89 -0
  97. package/lib/commands/upload.js +90 -108
  98. package/lib/commands/verify-governance-combined-external.js +61 -0
  99. package/lib/commands/verify-governance-command-action.js +64 -0
  100. package/lib/commands/verify-governance-db.js +102 -0
  101. package/lib/commands/verify-governance-external.js +77 -0
  102. package/lib/commands/verify-governance-result-builders.js +71 -0
  103. package/lib/commands/verify-operations-command-action.js +68 -0
  104. package/lib/commands/verify-operations-external.js +268 -0
  105. package/lib/commands/verify-operations-persist.js +75 -0
  106. package/lib/commands/verify-operations-step-plan.js +68 -0
  107. package/lib/commands/verify-operations-steps.js +111 -0
  108. package/lib/commands/verify-trust-command-action.js +70 -0
  109. package/lib/commands/verify-trust-external.js +67 -0
  110. package/lib/commands/wizard-config-normalizer.js +43 -23
  111. package/lib/commands/wizard-core.js +2 -0
  112. package/lib/core/admin-password-generator.js +38 -0
  113. package/lib/core/admin-secrets-bundle.js +111 -0
  114. package/lib/core/admin-secrets-env-overlay.js +62 -0
  115. package/lib/core/admin-secrets.js +32 -4
  116. package/lib/core/config-attach-extensions.js +3 -0
  117. package/lib/core/config-platform-controller.js +44 -0
  118. package/lib/core/config-setup-installation-profile.js +42 -0
  119. package/lib/core/config-setup-platform-mode.js +46 -0
  120. package/lib/core/config.js +46 -36
  121. package/lib/core/secrets-admin-env.js +6 -2
  122. package/lib/core/secrets-ensure.js +4 -0
  123. package/lib/core/secrets-env-content.js +54 -48
  124. package/lib/core/secrets-env-write.js +10 -1
  125. package/lib/core/secrets-infra-placeholder-sync.js +2 -0
  126. package/lib/core/setup-installation-profile.js +43 -0
  127. package/lib/core/setup-platform-mode.js +55 -0
  128. package/lib/datasource/agent-trust-debug-log.js +1 -1
  129. package/lib/datasource/agent-trust-run.js +5 -3
  130. package/lib/datasource/audit-evidence-extract-debug.js +30 -18
  131. package/lib/datasource/audit-evidence-extract.js +7 -4
  132. package/lib/datasource/audit-evidence-verify-matrix.js +54 -26
  133. package/lib/datasource/bulk-loader-run-batches.js +15 -3
  134. package/lib/datasource/datasource-load-export-context.js +3 -0
  135. package/lib/datasource/log-cleaner.js +2 -2
  136. package/lib/datasource/log-viewer.js +8 -8
  137. package/lib/datasource/test-e2e.js +4 -1
  138. package/lib/datasource/unified-validation-run-body.js +3 -1
  139. package/lib/datasource/unified-validation-run.js +5 -2
  140. package/lib/deployment/environment-poll-status.js +138 -0
  141. package/lib/deployment/environment.js +42 -34
  142. package/lib/external-system/download.js +21 -20
  143. package/lib/external-system/generator.js +5 -1
  144. package/lib/external-system/integration-auth-context.js +45 -0
  145. package/lib/external-system/test-auth.js +12 -5
  146. package/lib/external-system/test.js +6 -6
  147. package/lib/generator/external-controller-manifest.js +15 -0
  148. package/lib/governance/governance-cli-errors.js +96 -0
  149. package/lib/governance/governance-download-display.js +38 -0
  150. package/lib/governance/governance-run-display.js +215 -25
  151. package/lib/infrastructure/admin-secrets-apply.js +84 -0
  152. package/lib/infrastructure/admin-secrets-ensure.js +130 -0
  153. package/lib/infrastructure/admin-secrets-resolve.js +95 -0
  154. package/lib/infrastructure/admin-secrets-sync.js +91 -0
  155. package/lib/infrastructure/helpers.js +23 -129
  156. package/lib/infrastructure/index.js +5 -6
  157. package/lib/infrastructure/orphan-infra-docker-teardown.js +11 -5
  158. package/lib/lifecycle/certification-progress.js +231 -0
  159. package/lib/lifecycle/dataplane-ui-url.js +145 -0
  160. package/lib/lifecycle/exit-codes.js +24 -0
  161. package/lib/lifecycle/json-envelope.js +25 -0
  162. package/lib/lifecycle/lifecycle-governance-options.js +51 -0
  163. package/lib/lifecycle/logger-quiet.js +29 -0
  164. package/lib/lifecycle/open-api-published.js +59 -0
  165. package/lib/lifecycle/operations-readiness-metrics.js +101 -0
  166. package/lib/lifecycle/product-model.js +196 -0
  167. package/lib/lifecycle/report-display.js +471 -0
  168. package/lib/lifecycle/scenario-upload.js +191 -0
  169. package/lib/lifecycle/verify-step-progress.js +87 -0
  170. package/lib/parameters/infra-kv-discovery.js +27 -2
  171. package/lib/protection/auth-context.js +10 -4
  172. package/lib/schema/application-schema.json +1 -1
  173. package/lib/schema/datasource-test-run.schema.json +89 -6
  174. package/lib/schema/external-datasource.schema.json +2636 -2622
  175. package/lib/schema/external-system.schema.json +1089 -761
  176. package/lib/schema/infra.parameter.yaml +45 -0
  177. package/lib/schema/type/credential-field-registry.json +103 -97
  178. package/lib/utils/aifabrix-test-runtime-guard.js +125 -0
  179. package/lib/utils/api.js +55 -31
  180. package/lib/utils/app-relative-path-env.js +112 -0
  181. package/lib/utils/cli-secrets-error-format.js +83 -21
  182. package/lib/utils/cli-utils.js +16 -4
  183. package/lib/utils/compose-generate-docker-compose.js +4 -0
  184. package/lib/utils/compose-traefik-ingress-base.js +16 -1
  185. package/lib/utils/config-secrets-path.js +46 -0
  186. package/lib/utils/controller-url.js +88 -1
  187. package/lib/utils/credential-secrets-env.js +137 -9
  188. package/lib/utils/dataplane-resolver.js +129 -2
  189. package/lib/utils/declarative-url-ports.js +3 -3
  190. package/lib/utils/deployment-auth-mode.js +41 -0
  191. package/lib/utils/developer-volume-id.js +33 -0
  192. package/lib/utils/env-copy.js +14 -9
  193. package/lib/utils/env-kv-orphan-lines.js +43 -0
  194. package/lib/utils/error-formatters/network-errors.js +13 -4
  195. package/lib/utils/external-env-template.js +1 -0
  196. package/lib/utils/external-system-readiness-core.js +10 -0
  197. package/lib/utils/external-system-readiness-display.js +80 -23
  198. package/lib/utils/health-check.js +4 -1
  199. package/lib/utils/help-builder.js +11 -8
  200. package/lib/utils/keycloak-realm-ready.js +101 -0
  201. package/lib/utils/load-cip-capacity-display-config.js +61 -15
  202. package/lib/utils/manifest-source-emit.js +2 -0
  203. package/lib/utils/pipeline-upload-auth-retry.js +82 -0
  204. package/lib/utils/platform-controller-url.js +156 -0
  205. package/lib/utils/postgres-platform-bootstrap.js +199 -0
  206. package/lib/utils/postgres-wipe.js +9 -2
  207. package/lib/utils/resolve-env-mode.js +47 -0
  208. package/lib/utils/secrets-generator.js +31 -25
  209. package/lib/utils/secrets-missing-error.js +164 -0
  210. package/lib/utils/service-url-label.js +81 -0
  211. package/lib/utils/setup-quiet-output.js +25 -0
  212. package/lib/utils/template-helpers.js +15 -0
  213. package/lib/utils/token-manager-client-credentials.js +201 -0
  214. package/lib/utils/token-manager-messages.js +8 -0
  215. package/lib/utils/token-manager-refresh.js +12 -2
  216. package/lib/utils/token-manager.js +86 -105
  217. package/lib/utils/upload-pipeline-ui.js +12 -5
  218. package/lib/utils/upload-sync-options.js +42 -0
  219. package/lib/utils/url-declarative-public-base.js +21 -3
  220. package/lib/utils/url-declarative-vdir-inactive-env.js +14 -4
  221. package/lib/utils/validation-poll-ui.js +8 -0
  222. package/lib/utils/validation-run-stage.js +45 -0
  223. package/package.json +1 -1
  224. package/templates/applications/dataplane/application.yaml +2 -1
  225. package/templates/applications/dataplane/env.template +24 -1
  226. package/templates/applications/miso-controller/env.template +16 -0
  227. package/templates/external-system/README.md.hbs +74 -17
  228. package/templates/python/docker-compose.hbs +19 -18
  229. package/templates/typescript/docker-compose.hbs +18 -17
  230. package/lib/commands/test-governance-command-action.js +0 -55
  231. package/lib/commands/test-trust-command-action.js +0 -118
package/lib/utils/api.js CHANGED
@@ -10,7 +10,8 @@
10
10
  * @version 2.0.0
11
11
  */
12
12
 
13
- const { parseErrorResponse } = require('./api-error-handler');
13
+ const { parseErrorResponse, formatNetworkError } = require('./api-error-handler');
14
+ const { inferServiceLabelFromUrl } = require('./service-url-label');
14
15
  const auditLogger = require('../core/audit-logger');
15
16
 
16
17
  /** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
@@ -172,7 +173,7 @@ async function handleSuccessResponse(response, url, options, duration) {
172
173
  * @throws {Error} If URL is empty, null, undefined, whitespace-only, or malformed
173
174
  */
174
175
  function validateUrl(url, urlType = 'URL') {
175
- if (!url || typeof url !== 'string') {
176
+ if (url === null || url === undefined || typeof url !== 'string') {
176
177
  throw new Error(`${urlType} is required and must be a string (received: ${JSON.stringify(url)})`);
177
178
  }
178
179
  const trimmedUrl = url.trim();
@@ -248,20 +249,18 @@ async function handleNetworkError(error, url, options, duration) {
248
249
  const flat = flattenNetworkErrorDetail(error);
249
250
  const errorMessage = withUrlContextForNetworkError(flat || error.message, url);
250
251
 
251
- const parsedError = parseErrorResponse(errorMessage, 0, true);
252
-
253
- // Extract controller URL from full URL for error data
254
- let controllerUrl = null;
252
+ let serviceBaseUrl = null;
255
253
  const endpointUrl = url;
256
254
  if (url && typeof url === 'string' && url.trim()) {
257
255
  try {
258
256
  const urlObj = new URL(url);
259
- controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
257
+ serviceBaseUrl = `${urlObj.protocol}//${urlObj.host}`;
260
258
  } catch {
261
- // If URL parsing fails, use the full URL as endpoint
262
- controllerUrl = null;
259
+ serviceBaseUrl = null;
263
260
  }
264
261
  }
262
+ const serviceLabel = inferServiceLabelFromUrl(endpointUrl);
263
+ const parsedError = parseErrorResponse(errorMessage, 0, true);
265
264
 
266
265
  await logApiPerformance({
267
266
  url,
@@ -276,24 +275,61 @@ async function handleNetworkError(error, url, options, duration) {
276
275
  }
277
276
  });
278
277
 
279
- // Include both controller URL and full endpoint URL in error data
280
278
  const errorData = {
281
- ...parsedError.data,
282
- controllerUrl: controllerUrl,
283
- endpointUrl: endpointUrl
279
+ controllerUrl: serviceBaseUrl,
280
+ endpointUrl: endpointUrl,
281
+ serviceLabel
284
282
  };
283
+ const formattedError = formatNetworkError(errorMessage, errorData);
285
284
 
286
285
  return {
287
286
  success: false,
288
- error: parsedError.message,
287
+ error: errorMessage,
289
288
  errorData: errorData,
290
- errorType: parsedError.type,
291
- formattedError: parsedError.formatted,
289
+ errorType: 'network',
290
+ formattedError,
292
291
  network: true,
293
292
  originalError: error
294
293
  };
295
294
  }
296
295
 
296
+ /**
297
+ * @param {Object} options - Fetch options passed to makeApiCall
298
+ * @returns {{ fetchOptions: Object, singleRequestTimeoutMs: number }}
299
+ */
300
+ function prepareFetchOptionsForRequest(options) {
301
+ const fetchOptions = { ...options };
302
+ const singleRequestTimeoutMs = resolveSingleRequestTimeoutMs(fetchOptions);
303
+ if (!fetchOptions.signal) {
304
+ fetchOptions.signal = AbortSignal.timeout(singleRequestTimeoutMs);
305
+ }
306
+ delete fetchOptions.timeoutMs;
307
+ delete fetchOptions.requestTimeoutMs;
308
+ return { fetchOptions, singleRequestTimeoutMs };
309
+ }
310
+
311
+ /**
312
+ * @param {Error} err
313
+ * @param {string} url
314
+ * @param {number} singleRequestTimeoutMs
315
+ * @returns {Error}
316
+ */
317
+ function normalizeFetchNetworkError(err, url, singleRequestTimeoutMs) {
318
+ if (err?.name !== 'AbortError') {
319
+ return err;
320
+ }
321
+ const serviceLabel = inferServiceLabelFromUrl(url);
322
+ const serviceName =
323
+ serviceLabel === 'dataplane'
324
+ ? 'dataplane'
325
+ : serviceLabel === 'controller'
326
+ ? 'controller'
327
+ : 'service';
328
+ return new Error(
329
+ `Request timed out after ${Math.round(singleRequestTimeoutMs / 1000)} seconds. The ${serviceName} may be unreachable. Check the URL and network.`
330
+ );
331
+ }
332
+
297
333
  /**
298
334
  * Make an API call with proper error handling
299
335
  * Uses a 30s timeout by default. Pass ``options.timeoutMs`` or ``options.requestTimeoutMs`` for
@@ -304,22 +340,14 @@ async function handleNetworkError(error, url, options, duration) {
304
340
  * @returns {Promise<Object>} Response object with success flag
305
341
  */
306
342
  async function makeApiCall(url, options = {}) {
307
- // Validate URL before attempting request
308
343
  try {
309
344
  validateUrl(url, 'API endpoint URL');
310
345
  } catch (error) {
311
- const duration = 0;
312
- return await handleNetworkError(error, url || '', options, duration);
346
+ return await handleNetworkError(error, url || '', options, 0);
313
347
  }
314
348
 
315
349
  const startTime = Date.now();
316
- const fetchOptions = { ...options };
317
- const singleRequestTimeoutMs = resolveSingleRequestTimeoutMs(fetchOptions);
318
- if (!fetchOptions.signal) {
319
- fetchOptions.signal = AbortSignal.timeout(singleRequestTimeoutMs);
320
- }
321
- delete fetchOptions.timeoutMs;
322
- delete fetchOptions.requestTimeoutMs;
350
+ const { fetchOptions, singleRequestTimeoutMs } = prepareFetchOptionsForRequest(options);
323
351
 
324
352
  try {
325
353
  const response = await fetch(url, fetchOptions);
@@ -332,11 +360,7 @@ async function makeApiCall(url, options = {}) {
332
360
  return await handleSuccessResponse(response, url, options, duration);
333
361
  } catch (err) {
334
362
  const duration = Date.now() - startTime;
335
- const error = err?.name === 'AbortError'
336
- ? new Error(
337
- `Request timed out after ${Math.round(singleRequestTimeoutMs / 1000)} seconds. The controller may be unreachable. Check the URL and network.`
338
- )
339
- : err;
363
+ const error = normalizeFetchNetworkError(err, url, singleRequestTimeoutMs);
340
364
  return await handleNetworkError(error, url, options, duration);
341
365
  }
342
366
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Per-app virtual-directory env var names ({@code <NAME>_RELATIVE_PATH}) for Builder CLI.
3
+ *
4
+ * @fileoverview {@code app.key} → {@code DATAPLANE_RELATIVE_PATH}, {@code MYAPP_RELATIVE_PATH}, etc.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /** Must match {@link module:lib/utils/url-declarative-vdir-inactive-env} token (avoid circular require). */
12
+ const URL_DECLARATIVE_VDIR_PUBLIC_TOKEN = 'url://vdir-public';
13
+
14
+ /** Suffix for runtime mount vars derived from {@code application.yaml} {@code app.key}. */
15
+ const APP_RELATIVE_PATH_ENV_SUFFIX = '_RELATIVE_PATH';
16
+
17
+ /**
18
+ * Apps whose runtime vdir env name is not {@code UPPER(appKey)_RELATIVE_PATH}.
19
+ * @type {Readonly<Record<string, string>>}
20
+ */
21
+ const APP_RELATIVE_PATH_ENV_KEY_OVERRIDES = Object.freeze({
22
+ keycloak: 'KC_HTTP_RELATIVE_PATH',
23
+ 'miso-controller': 'MISO_RELATIVE_PATH'
24
+ });
25
+
26
+ /** Always rewritten to "/" when front-door path is inactive (shared infra apps). */
27
+ const INACTIVE_VDIR_PUBLIC_STATIC_ENV_KEYS = Object.freeze([
28
+ 'KC_HTTP_RELATIVE_PATH',
29
+ 'MISO_RELATIVE_PATH'
30
+ ]);
31
+
32
+ /**
33
+ * @param {string} appKey - {@code application.yaml} {@code app.key}
34
+ * @returns {string} e.g. {@code dataplane} → {@code DATAPLANE_RELATIVE_PATH}, {@code myapp} → {@code MYAPP_RELATIVE_PATH}
35
+ */
36
+ function appKeyToRelativePathEnvKey(appKey) {
37
+ const key = String(appKey || '').trim();
38
+ if (!key) {
39
+ return '';
40
+ }
41
+ const override = APP_RELATIVE_PATH_ENV_KEY_OVERRIDES[key];
42
+ if (override) {
43
+ return override;
44
+ }
45
+ const upper = key.replace(/-/g, '_').toUpperCase();
46
+ return `${upper}${APP_RELATIVE_PATH_ENV_SUFFIX}`;
47
+ }
48
+
49
+ /**
50
+ * Whether {@code key} is a runtime vdir env name (static Keycloak/Miso or {@code NAME_RELATIVE_PATH}).
51
+ * @param {string} key
52
+ * @returns {boolean}
53
+ */
54
+ function isAppRelativePathEnvKey(key) {
55
+ const name = String(key || '').trim();
56
+ if (!name) {
57
+ return false;
58
+ }
59
+ if (INACTIVE_VDIR_PUBLIC_STATIC_ENV_KEYS.includes(name)) {
60
+ return true;
61
+ }
62
+ return name.endsWith(APP_RELATIVE_PATH_ENV_SUFFIX);
63
+ }
64
+
65
+ /**
66
+ * Env keys to rewrite when path is inactive: static Keycloak/Miso + current app vdir key.
67
+ * @param {string|null|undefined} appKey - {@code application.yaml} {@code app.key}
68
+ * @returns {ReadonlySet<string>}
69
+ */
70
+ function resolveInactiveVdirRewriteEnvKeys(appKey) {
71
+ const keys = new Set(INACTIVE_VDIR_PUBLIC_STATIC_ENV_KEYS);
72
+ const envKey = appKeyToRelativePathEnvKey(appKey);
73
+ if (envKey) {
74
+ keys.add(envKey);
75
+ }
76
+ return keys;
77
+ }
78
+
79
+ /**
80
+ * When an app is created from a product template, rename the template's {@code *_RELATIVE_PATH}
81
+ * line to match {@code app.key} (e.g. {@code DATAPLANE_RELATIVE_PATH} → {@code MYAPP_RELATIVE_PATH}).
82
+ * @param {string} content - {@code env.template} body
83
+ * @param {string} appKey
84
+ * @returns {string}
85
+ */
86
+ function renameAppRelativePathEnvKeyInTemplate(content, appKey) {
87
+ const targetKey = appKeyToRelativePathEnvKey(appKey);
88
+ if (!targetKey || !content || !content.includes(URL_DECLARATIVE_VDIR_PUBLIC_TOKEN)) {
89
+ return content;
90
+ }
91
+ const escapedToken = URL_DECLARATIVE_VDIR_PUBLIC_TOKEN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ const lineRe = new RegExp(
93
+ `^([ \\t]*)([A-Z][A-Z0-9_]*${APP_RELATIVE_PATH_ENV_SUFFIX})([ \\t]*=\\s*)${escapedToken}[ \\t]*$`,
94
+ 'gm'
95
+ );
96
+ return content.replace(lineRe, (match, indent, currentKey, eq) => {
97
+ if (currentKey === targetKey) {
98
+ return match;
99
+ }
100
+ return `${indent}${targetKey}${eq}${URL_DECLARATIVE_VDIR_PUBLIC_TOKEN}`;
101
+ });
102
+ }
103
+
104
+ module.exports = {
105
+ APP_RELATIVE_PATH_ENV_SUFFIX,
106
+ APP_RELATIVE_PATH_ENV_KEY_OVERRIDES,
107
+ INACTIVE_VDIR_PUBLIC_STATIC_ENV_KEYS,
108
+ appKeyToRelativePathEnvKey,
109
+ isAppRelativePathEnvKey,
110
+ resolveInactiveVdirRewriteEnvKeys,
111
+ renameAppRelativePathEnvKeyInTemplate
112
+ };
@@ -8,6 +8,7 @@
8
8
 
9
9
  const chalk = require('chalk');
10
10
  const { metadata, sectionTitle } = require('./cli-layout-chalk');
11
+ const { kvRefToSecretSetKey } = require('./secrets-missing-error');
11
12
 
12
13
  /**
13
14
  * @param {string[]} messages - Output lines
@@ -25,34 +26,72 @@ function pushMissingSecretsBodyLines(messages, missingSecretsMatch) {
25
26
  for (const ref of parts) {
26
27
  messages.push(` ${chalk.cyan('-')} ${chalk.white(ref)}`);
27
28
  }
28
- return;
29
+ return parts;
29
30
  }
30
31
  if (parts.length === 1) {
31
32
  messages.push(` ${sectionTitle('Missing secrets:')} ${chalk.white(parts[0])}`);
32
- return;
33
+ return parts;
33
34
  }
34
35
  messages.push(` ${sectionTitle('Missing secrets:')}`);
35
- return;
36
+ return [];
36
37
  }
37
38
  messages.push(` ${chalk.white('Missing secrets in secrets file.')}`);
39
+ return [];
38
40
  }
39
41
 
40
42
  /**
41
- * @param {string[]} messages - Output lines
42
- * @param {RegExpMatchArray|null} fileInfoMatch - File location capture
43
- * @param {RegExpMatchArray|null} resolveMatch - Resolve command capture
43
+ * @param {string} errorMsg
44
+ * @returns {Map<string, string>}
45
+ */
46
+ function parseEnvTemplateLineHints(errorMsg) {
47
+ const map = new Map();
48
+ const re = /^Env-template-line: (kv:\/\/[^\s|]+)\|(.+)$/gm;
49
+ let m;
50
+ while ((m = re.exec(errorMsg)) !== null) {
51
+ map.set(m[1].trim(), m[2].trim());
52
+ }
53
+ return map;
54
+ }
55
+
56
+ /**
57
+ * @param {string[]} messages
58
+ * @param {string[]} missingRefs
59
+ * @param {Map<string, string>} templateLinesByRef
60
+ * @param {string|null} envTemplatePath
44
61
  */
45
- function pushSecretsResolutionHintLines(messages, fileInfoMatch, resolveMatch) {
62
+ function pushRemediationHintLines(messages, missingRefs, templateLinesByRef, envTemplatePath) {
63
+ messages.push(` ${sectionTitle('You can fix this two ways:')}`);
64
+ messages.push(
65
+ ` ${chalk.cyan('1.')} ${chalk.white('Not used by your external system?')} Comment out or delete the line in env.template.`
66
+ );
67
+ if (envTemplatePath) {
68
+ messages.push(` ${metadata('env.template:')} ${chalk.white(envTemplatePath)}`);
69
+ }
70
+ for (const ref of missingRefs) {
71
+ const line = templateLinesByRef.get(ref);
72
+ if (line) {
73
+ messages.push(` ${metadata('Comment or delete:')} ${chalk.gray(line)}`);
74
+ }
75
+ }
76
+ messages.push(
77
+ ` ${chalk.cyan('2.')} ${chalk.white('Store the value:')} ${metadata('aifabrix secret set')} ${chalk.yellow('<key>')} ${chalk.gray('"<your-value>"')}`
78
+ );
79
+ for (const ref of missingRefs) {
80
+ const key = kvRefToSecretSetKey(ref);
81
+ if (key) {
82
+ messages.push(` ${metadata('Example:')} ${chalk.yellow(`aifabrix secret set ${key} "<your-value>"`)}`);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} messages
89
+ * @param {RegExpMatchArray|null} fileInfoMatch
90
+ */
91
+ function pushSecretsFileLocationLine(messages, fileInfoMatch) {
46
92
  if (fileInfoMatch) {
47
93
  messages.push(` ${metadata('Secrets file location:')} ${chalk.white(fileInfoMatch[1])}`);
48
94
  }
49
- if (resolveMatch) {
50
- messages.push(
51
- ` ${metadata('Run:')} ${chalk.yellow(`aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`)}`
52
- );
53
- return;
54
- }
55
- messages.push(` ${metadata('Run:')} ${chalk.yellow('aifabrix resolve <app-name> to generate missing secrets.')}`);
56
95
  }
57
96
 
58
97
  /**
@@ -66,13 +105,36 @@ function formatSecretsError(errorMsg) {
66
105
  }
67
106
 
68
107
  const messages = [];
69
- pushMissingSecretsBodyLines(messages, errorMsg.match(/Missing secrets: ([^\n]+)/));
70
- pushSecretsResolutionHintLines(
71
- messages,
72
- errorMsg.match(/Secrets file location: ([^\n]+)/),
73
- errorMsg.match(/Run "aifabrix resolve ([^"]+)"/)
74
- );
108
+ const missingRefs = pushMissingSecretsBodyLines(messages, errorMsg.match(/Missing secrets: ([^\n]+)/));
109
+ pushSecretsFileLocationLine(messages, errorMsg.match(/Secrets file location: ([^\n]+)/));
110
+ const envTemplatePathMatch = errorMsg.match(/Env-template-path: ([^\n]+)/);
111
+ const envTemplatePath = envTemplatePathMatch ? envTemplatePathMatch[1].trim() : null;
112
+ const templateLinesByRef = parseEnvTemplateLineHints(errorMsg);
113
+ const refs =
114
+ missingRefs.length > 0
115
+ ? missingRefs
116
+ : [...templateLinesByRef.keys()];
117
+ if (refs.length > 0) {
118
+ pushRemediationHintLines(messages, refs, templateLinesByRef, envTemplatePath);
119
+ }
120
+ const staleMatch = errorMsg.match(/Stale-template-hint: oauth-leftover \| repair ([^\n]+)/);
121
+ if (staleMatch) {
122
+ messages.push(
123
+ ` ${chalk.yellow('⚠')} ${chalk.white('These keys are often left over after switching to apikey/bearerToken auth.')}`
124
+ );
125
+ messages.push(
126
+ ` ${metadata('Run:')} ${chalk.yellow(`aifabrix repair ${staleMatch[1].trim()}`)} ${chalk.gray('to comment stale OAuth lines in env.template')}`
127
+ );
128
+ }
75
129
  return messages;
76
130
  }
77
131
 
78
- module.exports = { formatSecretsError };
132
+ /**
133
+ * @param {string} errorMsg
134
+ * @returns {boolean}
135
+ */
136
+ function isMissingSecretsErrorMessage(errorMsg) {
137
+ return typeof errorMsg === 'string' && errorMsg.includes('Missing secrets');
138
+ }
139
+
140
+ module.exports = { formatSecretsError, isMissingSecretsErrorMessage };
@@ -12,7 +12,7 @@ const path = require('path');
12
12
  const chalk = require('chalk');
13
13
  const logger = require('./logger');
14
14
  const { formatBlockingError, infoLine } = require('./cli-layout-chalk');
15
- const { formatSecretsError } = require('./cli-secrets-error-format');
15
+ const { formatSecretsError, isMissingSecretsErrorMessage } = require('./cli-secrets-error-format');
16
16
  const { getDockerDaemonStartHintSentence, getDockerApiOverTcpHintLines } = require('./docker-not-running-hint');
17
17
 
18
18
  /**
@@ -337,6 +337,15 @@ function formatError(error) {
337
337
  return formatFormattedError(error.formatted);
338
338
  }
339
339
 
340
+ if (error.network === true) {
341
+ const { formatNetworkError } = require('./api-error-handler');
342
+ const formatted = formatNetworkError(error.message || 'Network error', {
343
+ endpointUrl: error.endpointUrl,
344
+ serviceLabel: error.serviceLabel || 'service'
345
+ });
346
+ return formatFormattedError(formatted);
347
+ }
348
+
340
349
  const errorMsg = error.message || '';
341
350
 
342
351
  // Try different error formatters in order of specificity
@@ -355,10 +364,12 @@ function formatError(error) {
355
364
  * @param {string} command - Command that failed
356
365
  * @param {string[]} errorMessages - Error message lines
357
366
  */
358
- function logError(command, errorMessages) {
367
+ function logError(command, errorMessages, options = {}) {
359
368
  logger.error(`\n${formatBlockingError(`Error in ${command} command:`)}`);
360
369
  errorMessages.forEach(msg => logger.error(msg));
361
- logger.log(`\n${infoLine('ℹ Run "aifabrix doctor" for environment diagnostics.')}\n`);
370
+ if (!options.skipDoctor) {
371
+ logger.log(`\n${infoLine('ℹ Run "aifabrix doctor" for environment diagnostics.')}\n`);
372
+ }
362
373
  }
363
374
 
364
375
  /**
@@ -396,7 +407,8 @@ function isAuthenticationError(error) {
396
407
  */
397
408
  function handleCommandError(error, command) {
398
409
  const errorMessages = formatError(error);
399
- logError(command, errorMessages);
410
+ const skipDoctor = isMissingSecretsErrorMessage(error && error.message);
411
+ logError(command, errorMessages, { skipDoctor });
400
412
  if (error.wizardResumeMessage) {
401
413
  logger.log(error.wizardResumeMessage);
402
414
  }
@@ -61,6 +61,10 @@ function normalizePythonReloadForComposeMounted(raw) {
61
61
  if (/^python3?\s+-m\s+uvicorn\s+/i.test(s)) {
62
62
  return s;
63
63
  }
64
+ const envPrefixed = s.match(/^((?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*)(uvicorn\s+.*)$/);
65
+ if (envPrefixed) {
66
+ return `${envPrefixed[1]}python -m uvicorn ${envPrefixed[2].slice('uvicorn '.length)}`;
67
+ }
64
68
  if (s.startsWith('uvicorn ')) {
65
69
  return `python -m uvicorn ${s.slice('uvicorn '.length)}`;
66
70
  }
@@ -86,6 +86,19 @@ function expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer)
86
86
  return t;
87
87
  }
88
88
 
89
+ function templateUsesRemoteHostPlaceholder(hostTemplate) {
90
+ return /\$\{REMOTE_HOST\}/.test(String(hostTemplate || ''));
91
+ }
92
+
93
+ function resolveTraefikRouterHost(hostTemplate, developerIdRaw, remoteServer) {
94
+ const usesRemote = templateUsesRemoteHostPlaceholder(hostTemplate);
95
+ const hasRemote = Boolean(remoteServer && String(remoteServer).trim());
96
+ if (usesRemote && !hasRemote) {
97
+ return 'localhost';
98
+ }
99
+ return expandFrontDoorHostPlaceholders(hostTemplate, developerIdRaw, remoteServer);
100
+ }
101
+
89
102
  /**
90
103
  * Traefik PathPrefix / host / TLS from frontDoorRouting (public ingress). Does not include StripPrefix — that follows
91
104
  * the in-container health path: private URL is `http://<service>:<port>` plus the probe path; `/dev`, `/tst`, `/auth`,
@@ -105,7 +118,7 @@ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer) {
105
118
  if (!frontDoor.host || typeof frontDoor.host !== 'string') {
106
119
  throw new Error('frontDoorRouting.host is required when frontDoorRouting.enabled is true');
107
120
  }
108
- const host = expandFrontDoorHostPlaceholders(frontDoor.host, devId, remoteServer);
121
+ const host = resolveTraefikRouterHost(frontDoor.host, devId, remoteServer);
109
122
  const basePath = derivePathFromPattern(frontDoor.pattern);
110
123
  let pathOut = basePath;
111
124
  if (
@@ -153,6 +166,8 @@ module.exports = {
153
166
  resolveTraefikTlsEnabled,
154
167
  buildDevUsernameForFrontDoorHost,
155
168
  expandFrontDoorHostPlaceholders,
169
+ templateUsesRemoteHostPlaceholder,
170
+ resolveTraefikRouterHost,
156
171
  buildTraefikIngressBase,
157
172
  computeTraefikStripPathPrefix
158
173
  };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Secrets path config helpers.
3
+ *
4
+ * @fileoverview Keeps lib/core/config.js under max-lines
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Expand leading ~ to home directory so config paths like ~/.aifabrix/secrets.local.yaml resolve correctly.
14
+ * @param {string} filePath - Path that may start with ~ or ~/
15
+ * @returns {string} Path with ~ expanded, or unchanged if no leading ~
16
+ */
17
+ function expandTilde(filePath) {
18
+ if (!filePath || typeof filePath !== 'string') return filePath;
19
+ if (filePath === '~') return os.homedir();
20
+ if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
21
+ return path.join(os.homedir(), filePath.slice(2));
22
+ }
23
+ return filePath;
24
+ }
25
+
26
+ function createSecretsPathFunctions(getConfig, saveConfig) {
27
+ async function getSecretsPath() {
28
+ const cfg = await getConfig();
29
+ const raw = cfg['aifabrix-secrets'] || cfg['secrets-path'] || null;
30
+ return raw ? expandTilde(raw) : null;
31
+ }
32
+
33
+ async function setSecretsPath(secretsPath) {
34
+ if (typeof secretsPath !== 'string') {
35
+ throw new Error('Secrets path is required and must be a string');
36
+ }
37
+ const cfg = await getConfig();
38
+ cfg['aifabrix-secrets'] = secretsPath.trim() || undefined;
39
+ await saveConfig(cfg);
40
+ }
41
+
42
+ return { getSecretsPath, setSecretsPath };
43
+ }
44
+
45
+ module.exports = { createSecretsPathFunctions };
46
+
@@ -12,6 +12,7 @@
12
12
  const { getDeveloperIdNumber } = require('./env-map');
13
13
  const devConfig = require('./dev-config');
14
14
  const config = require('../core/config');
15
+ const { getDeviceToken, isTokenExpired } = require('./token-manager');
15
16
 
16
17
  /**
17
18
  * Calculate default controller URL based on developer ID
@@ -40,7 +41,6 @@ function normalizeUrl(url) {
40
41
  }
41
42
  return url.trim().replace(/\/+$/, '');
42
43
  }
43
-
44
44
  /**
45
45
  * True when `config.yaml` has a non-expired-looking device entry for this controller URL
46
46
  * (normalized host/path comparison).
@@ -68,6 +68,90 @@ async function hasStoredDeviceTokenForController(controllerUrl) {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * True when a stored, non-expired device token exists (local config only — no refresh HTTP).
73
+ * Setup uses this to decide whether to skip the device-login step; avoids refresh warnings
74
+ * when the controller is not up yet or `device:` is empty.
75
+ *
76
+ * @async
77
+ * @param {string} controllerUrl
78
+ * @returns {Promise<boolean>}
79
+ */
80
+ async function isDeviceTokenUsableForController(controllerUrl) {
81
+ if (!(await hasStoredDeviceTokenForController(controllerUrl))) {
82
+ return false;
83
+ }
84
+ const want = normalizeUrl(controllerUrl);
85
+ try {
86
+ const tokenInfo = await getDeviceToken(want);
87
+ return Boolean(tokenInfo && tokenInfo.token && !isTokenExpired(tokenInfo.expiresAt));
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * True when a stored device token can be refreshed (if needed) and validated against the controller API.
95
+ * Prefer this after the controller is reachable (guided platform auth step). Falls back to local expiry-only
96
+ * check when the controller is not reachable yet.
97
+ *
98
+ * @async
99
+ * @param {string} controllerUrl
100
+ * @returns {Promise<boolean>}
101
+ */
102
+ async function isPlatformAuthValidForController(controllerUrl) {
103
+ if (!(await hasStoredDeviceTokenForController(controllerUrl))) {
104
+ return false;
105
+ }
106
+ const want = normalizeUrl(controllerUrl);
107
+
108
+ if (!(await isControllerHealthReachable(want))) {
109
+ return isDeviceTokenUsableForController(controllerUrl);
110
+ }
111
+
112
+ const { getOrRefreshDeviceToken } = require('./token-manager');
113
+ const { getAuthUser } = require('../api/auth.api');
114
+
115
+ let tokenResult = null;
116
+ try {
117
+ tokenResult = await getOrRefreshDeviceToken(want);
118
+ } catch {
119
+ tokenResult = null;
120
+ }
121
+
122
+ if (!tokenResult || !tokenResult.token) {
123
+ return false;
124
+ }
125
+
126
+ try {
127
+ const response = await getAuthUser(want, { type: 'bearer', token: tokenResult.token });
128
+ return Boolean(response.success && response.data && response.data.authenticated !== false);
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Best-effort GET /health on the controller (no auth). Used to avoid token refresh HTTP
136
+ * during setup before Miso Controller is running.
137
+ *
138
+ * @async
139
+ * @param {string} controllerUrl
140
+ * @returns {Promise<boolean>}
141
+ */
142
+ async function isControllerHealthReachable(controllerUrl) {
143
+ const base = normalizeUrl(controllerUrl);
144
+ if (!base) {
145
+ return false;
146
+ }
147
+ try {
148
+ const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(2500) });
149
+ return res.ok;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
71
155
  /**
72
156
  * Get controller URL from logged-in user's device tokens.
73
157
  * Prefers the entry under {@link config.controller} when it matches a `device` key; otherwise a
@@ -152,6 +236,9 @@ async function resolveControllerUrl() {
152
236
  module.exports = {
153
237
  getDefaultControllerUrl,
154
238
  hasStoredDeviceTokenForController,
239
+ isDeviceTokenUsableForController,
240
+ isPlatformAuthValidForController,
241
+ isControllerHealthReachable,
155
242
  getControllerUrlFromLoggedInUser,
156
243
  getControllerFromConfig,
157
244
  resolveControllerUrl