@aifabrix/builder 2.45.0 → 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 (352) 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/docs-rules.mdc +3 -1
  4. package/.cursor/rules/project-rules.mdc +1 -0
  5. package/README.md +43 -17
  6. package/integration/hubspot-test/wizard-hubspot-e2e.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 +69 -121
  12. package/lib/api/agent-metadata-validation.api.js +93 -0
  13. package/lib/api/api-response-errors.js +124 -0
  14. package/lib/api/audit-api-helpers.js +68 -0
  15. package/lib/api/audit.api.js +119 -0
  16. package/lib/api/auth-cache.api.js +44 -0
  17. package/lib/api/certificates.api.js +4 -4
  18. package/lib/api/credential.api.js +2 -2
  19. package/lib/api/dataplane-health.api.js +137 -0
  20. package/lib/api/dataplane-sync.api.js +27 -0
  21. package/lib/api/datasources-core.api.js +21 -18
  22. package/lib/api/datasources-extended.api.js +22 -22
  23. package/lib/api/dimensions.api.js +3 -0
  24. package/lib/api/external-systems.api.js +13 -13
  25. package/lib/api/governance-scenario-pack.api.js +159 -0
  26. package/lib/api/governance-scenarios.api.js +39 -0
  27. package/lib/api/groups.api.js +79 -0
  28. package/lib/api/index.js +88 -5
  29. package/lib/api/lifecycle.api.js +77 -0
  30. package/lib/api/pipeline.api.js +7 -5
  31. package/lib/api/protection.api.js +157 -0
  32. package/lib/api/records-bulk.api.js +43 -0
  33. package/lib/api/records-search-parse.js +49 -0
  34. package/lib/api/records-search.api.js +29 -0
  35. package/lib/api/types/agent-metadata-validation.types.js +67 -0
  36. package/lib/api/types/audit.types.js +72 -0
  37. package/lib/api/types/dataplane-health.types.js +27 -0
  38. package/lib/api/types/dimensions.types.js +2 -0
  39. package/lib/api/types/governance-scenarios.types.js +39 -0
  40. package/lib/api/types/protection.types.js +69 -0
  41. package/lib/api/types/records-bulk.types.js +35 -0
  42. package/lib/api/types/records-search.types.js +28 -0
  43. package/lib/api/user-groups.api.js +50 -0
  44. package/lib/api/users.api.js +95 -0
  45. package/lib/api/validation-run.api.js +3 -3
  46. package/lib/api/validation-runner.js +5 -3
  47. package/lib/api/wizard-platform.api.js +3 -3
  48. package/lib/api/wizard.api.js +21 -21
  49. package/lib/app/deploy-config.js +4 -1
  50. package/lib/app/deploy.js +34 -19
  51. package/lib/app/down.js +20 -1
  52. package/lib/app/push.js +4 -5
  53. package/lib/app/register.js +7 -4
  54. package/lib/app/run-env-compose.js +84 -30
  55. package/lib/build/index.js +5 -0
  56. package/lib/certification/sync-system-certification.js +13 -6
  57. package/lib/cli/index.js +8 -2
  58. package/lib/cli/infra-guided-auth.js +109 -0
  59. package/lib/cli/infra-guided.js +91 -69
  60. package/lib/cli/setup-app.help.js +19 -2
  61. package/lib/cli/setup-app.js +58 -23
  62. package/lib/cli/setup-app.test-commands.js +28 -21
  63. package/lib/cli/setup-auth.js +55 -3
  64. package/lib/cli/setup-certification-commands.js +147 -0
  65. package/lib/cli/setup-certification.help.js +141 -0
  66. package/lib/cli/setup-environment.js +19 -2
  67. package/lib/cli/setup-external-system-help.js +62 -0
  68. package/lib/cli/setup-external-system.js +62 -6
  69. package/lib/cli/setup-infra.js +9 -1
  70. package/lib/cli/setup-platform.js +6 -1
  71. package/lib/cli/setup-secrets.js +33 -1
  72. package/lib/cli/setup-utility-help-after.js +65 -0
  73. package/lib/cli/setup-utility-repair.js +198 -0
  74. package/lib/cli/setup-utility-resolve.js +42 -6
  75. package/lib/cli/setup-utility-validate.js +197 -0
  76. package/lib/cli/setup-utility.js +41 -258
  77. package/lib/commands/auth-status-client-tokens.js +214 -0
  78. package/lib/commands/auth-status-dataplane-version.js +269 -0
  79. package/lib/commands/auth-status-display.js +94 -0
  80. package/lib/commands/auth-status.js +111 -53
  81. package/lib/commands/credential-push.js +10 -5
  82. package/lib/commands/datasource-load-export-cli.js +178 -0
  83. package/lib/commands/datasource-test-trust-cli.js +118 -0
  84. package/lib/commands/datasource-unified-test-cli.js +31 -20
  85. package/lib/commands/datasource-unified-test-cli.options.js +22 -3
  86. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +5 -5
  87. package/lib/commands/datasource-verify-audit-cli.js +96 -0
  88. package/lib/commands/datasource.js +68 -1
  89. package/lib/commands/dev-show-display.js +8 -1
  90. package/lib/commands/dimension-value.js +43 -13
  91. package/lib/commands/dimension.js +38 -7
  92. package/lib/commands/governance-cli-shared.js +94 -0
  93. package/lib/commands/governance-command-action.js +132 -0
  94. package/lib/commands/governance-download-external.js +34 -0
  95. package/lib/commands/governance-generate-external.js +71 -0
  96. package/lib/commands/governance-upload-external.js +36 -0
  97. package/lib/commands/governance-verify-external.js +64 -0
  98. package/lib/commands/governance.js +144 -0
  99. package/lib/commands/identity-apply-sync.js +131 -0
  100. package/lib/commands/identity-group-cli.js +146 -0
  101. package/lib/commands/identity-membership-role-cache.js +175 -0
  102. package/lib/commands/identity-shared.js +94 -0
  103. package/lib/commands/identity-user-cli.js +158 -0
  104. package/lib/commands/identity.js +43 -0
  105. package/lib/commands/lifecycle-command-action.js +123 -0
  106. package/lib/commands/lifecycle-external.js +268 -0
  107. package/lib/commands/login.js +56 -17
  108. package/lib/commands/protection-cli-leaves.js +286 -0
  109. package/lib/commands/protection.js +50 -0
  110. package/lib/commands/repair-auth-apply.js +127 -0
  111. package/lib/commands/repair-changed-files.js +41 -0
  112. package/lib/commands/repair-datasource-keys.js +4 -1
  113. package/lib/commands/repair-datasource-run.js +59 -0
  114. package/lib/commands/repair-datasource-test-payload.js +259 -0
  115. package/lib/commands/repair-datasource.js +1 -51
  116. package/lib/commands/repair-env-template.js +64 -13
  117. package/lib/commands/repair-external-run.js +216 -0
  118. package/lib/commands/repair-openapi-sync.js +23 -0
  119. package/lib/commands/repair-persist.js +4 -0
  120. package/lib/commands/repair-rbac.js +52 -1
  121. package/lib/commands/repair-steps.js +145 -0
  122. package/lib/commands/repair.js +29 -286
  123. package/lib/commands/secrets-get.js +184 -0
  124. package/lib/commands/secrets-remove-all.js +3 -1
  125. package/lib/commands/setup-image-refresh.js +146 -0
  126. package/lib/commands/setup-modes.js +158 -162
  127. package/lib/commands/setup-platform-auth.js +76 -0
  128. package/lib/commands/setup-prompts-admin.js +244 -0
  129. package/lib/commands/setup-prompts-platform-mode.js +67 -0
  130. package/lib/commands/setup-prompts-validators.js +36 -0
  131. package/lib/commands/setup-prompts.js +24 -76
  132. package/lib/commands/setup-run-context.js +45 -0
  133. package/lib/commands/setup-spinners.js +39 -0
  134. package/lib/commands/setup.js +27 -5
  135. package/lib/commands/teardown.js +39 -0
  136. package/lib/commands/test-e2e-external.js +61 -14
  137. package/lib/commands/test-governance-cli-options.js +47 -0
  138. package/lib/commands/test-governance-external.js +67 -0
  139. package/lib/commands/test-trust-cli-options.js +32 -0
  140. package/lib/commands/test-trust-external.js +126 -0
  141. package/lib/commands/up-common.js +25 -13
  142. package/lib/commands/up-dataplane.js +50 -13
  143. package/lib/commands/upload-publish-context.js +75 -0
  144. package/lib/commands/upload-sidecar-followups.js +89 -0
  145. package/lib/commands/upload.js +108 -105
  146. package/lib/commands/verify-governance-combined-external.js +61 -0
  147. package/lib/commands/verify-governance-command-action.js +64 -0
  148. package/lib/commands/verify-governance-db.js +102 -0
  149. package/lib/commands/verify-governance-external.js +77 -0
  150. package/lib/commands/verify-governance-result-builders.js +71 -0
  151. package/lib/commands/verify-operations-command-action.js +68 -0
  152. package/lib/commands/verify-operations-external.js +268 -0
  153. package/lib/commands/verify-operations-persist.js +75 -0
  154. package/lib/commands/verify-operations-step-plan.js +68 -0
  155. package/lib/commands/verify-operations-steps.js +111 -0
  156. package/lib/commands/verify-trust-command-action.js +70 -0
  157. package/lib/commands/verify-trust-external.js +67 -0
  158. package/lib/commands/wizard-config-normalizer.js +43 -23
  159. package/lib/commands/wizard-core.js +5 -0
  160. package/lib/core/admin-password-generator.js +38 -0
  161. package/lib/core/admin-secrets-bundle.js +111 -0
  162. package/lib/core/admin-secrets-env-overlay.js +62 -0
  163. package/lib/core/admin-secrets.js +32 -4
  164. package/lib/core/config-attach-extensions.js +3 -0
  165. package/lib/core/config-device-dataplane.js +185 -0
  166. package/lib/core/config-platform-controller.js +44 -0
  167. package/lib/core/config-setup-installation-profile.js +42 -0
  168. package/lib/core/config-setup-platform-mode.js +46 -0
  169. package/lib/core/config.js +46 -36
  170. package/lib/core/secrets-admin-env.js +6 -2
  171. package/lib/core/secrets-ensure.js +4 -0
  172. package/lib/core/secrets-env-content.js +54 -48
  173. package/lib/core/secrets-env-write.js +10 -1
  174. package/lib/core/secrets-infra-placeholder-sync.js +2 -0
  175. package/lib/core/setup-installation-profile.js +43 -0
  176. package/lib/core/setup-platform-mode.js +55 -0
  177. package/lib/datasource/agent-trust-debug-log.js +45 -0
  178. package/lib/datasource/agent-trust-fetch.js +115 -0
  179. package/lib/datasource/agent-trust-map.js +101 -0
  180. package/lib/datasource/agent-trust-mock.js +44 -0
  181. package/lib/datasource/agent-trust-run.js +95 -0
  182. package/lib/datasource/agent-trust-timeout.js +25 -0
  183. package/lib/datasource/audit-evidence-display.js +80 -0
  184. package/lib/datasource/audit-evidence-extract-debug.js +115 -0
  185. package/lib/datasource/audit-evidence-extract.js +180 -0
  186. package/lib/datasource/audit-evidence-matrix-rows.js +158 -0
  187. package/lib/datasource/audit-evidence-operation-aliases.js +63 -0
  188. package/lib/datasource/audit-evidence-poll.js +109 -0
  189. package/lib/datasource/audit-evidence-run.js +177 -0
  190. package/lib/datasource/audit-evidence-verify-helpers.js +167 -0
  191. package/lib/datasource/audit-evidence-verify-matrix.js +205 -0
  192. package/lib/datasource/audit-evidence-verify.js +108 -0
  193. package/lib/datasource/bulk-loader-run-batches.js +100 -0
  194. package/lib/datasource/bulk-loader-service-retry.js +52 -0
  195. package/lib/datasource/bulk-loader-service.js +127 -0
  196. package/lib/datasource/capability/run-capability-dimension.js +2 -1
  197. package/lib/datasource/datasource-exporter-resolve.js +82 -0
  198. package/lib/datasource/datasource-exporter-service.js +144 -0
  199. package/lib/datasource/datasource-identifier.js +148 -0
  200. package/lib/datasource/datasource-load-export-context.js +95 -0
  201. package/lib/datasource/integration-context.js +6 -4
  202. package/lib/datasource/local-data-paths.js +145 -0
  203. package/lib/datasource/log-cleaner.js +241 -0
  204. package/lib/datasource/log-viewer.js +111 -6
  205. package/lib/datasource/record-file-parser.js +85 -0
  206. package/lib/datasource/record-mapper.js +129 -0
  207. package/lib/datasource/resolve-app.js +74 -55
  208. package/lib/datasource/test-e2e.js +29 -2
  209. package/lib/datasource/unified-validation-run-body.js +3 -1
  210. package/lib/datasource/unified-validation-run-resolve.js +45 -12
  211. package/lib/datasource/unified-validation-run.js +47 -18
  212. package/lib/datasource/validate.js +37 -20
  213. package/lib/deployment/environment-poll-status.js +138 -0
  214. package/lib/deployment/environment.js +42 -34
  215. package/lib/external-system/download.js +21 -20
  216. package/lib/external-system/generator.js +5 -1
  217. package/lib/external-system/integration-auth-context.js +45 -0
  218. package/lib/external-system/test-auth.js +12 -5
  219. package/lib/external-system/test-system-level.js +25 -4
  220. package/lib/external-system/test.js +9 -7
  221. package/lib/generator/external-controller-manifest.js +15 -0
  222. package/lib/governance/governance-cli-errors.js +96 -0
  223. package/lib/governance/governance-download-display.js +38 -0
  224. package/lib/governance/governance-pack-loader.js +70 -0
  225. package/lib/governance/governance-run-display.js +309 -0
  226. package/lib/identity/identity-apply-core.js +108 -0
  227. package/lib/identity/identity-apply-phases.js +244 -0
  228. package/lib/identity/identity-apply-service.js +77 -0
  229. package/lib/identity/identity-csv-parser.js +114 -0
  230. package/lib/infrastructure/admin-secrets-apply.js +84 -0
  231. package/lib/infrastructure/admin-secrets-ensure.js +130 -0
  232. package/lib/infrastructure/admin-secrets-resolve.js +95 -0
  233. package/lib/infrastructure/admin-secrets-sync.js +91 -0
  234. package/lib/infrastructure/helpers.js +23 -129
  235. package/lib/infrastructure/index.js +5 -6
  236. package/lib/infrastructure/orphan-infra-docker-teardown.js +11 -5
  237. package/lib/internal/fs-real-sync.js +29 -1
  238. package/lib/lifecycle/certification-progress.js +231 -0
  239. package/lib/lifecycle/dataplane-ui-url.js +145 -0
  240. package/lib/lifecycle/exit-codes.js +24 -0
  241. package/lib/lifecycle/json-envelope.js +25 -0
  242. package/lib/lifecycle/lifecycle-governance-options.js +51 -0
  243. package/lib/lifecycle/logger-quiet.js +29 -0
  244. package/lib/lifecycle/open-api-published.js +59 -0
  245. package/lib/lifecycle/operations-readiness-metrics.js +101 -0
  246. package/lib/lifecycle/product-model.js +196 -0
  247. package/lib/lifecycle/report-display.js +471 -0
  248. package/lib/lifecycle/scenario-upload.js +191 -0
  249. package/lib/lifecycle/verify-step-progress.js +87 -0
  250. package/lib/parameters/infra-kv-discovery.js +27 -2
  251. package/lib/protection/auth-context.js +51 -0
  252. package/lib/protection/convert-batch.js +90 -0
  253. package/lib/protection/load.js +41 -0
  254. package/lib/protection/paths.js +158 -0
  255. package/lib/protection/preflight-datasource-ready.js +46 -0
  256. package/lib/protection/protection-create-probe-lines.js +142 -0
  257. package/lib/protection/protection-create-probes.js +95 -0
  258. package/lib/protection/protection-create-scaffold.js +281 -0
  259. package/lib/protection/protection-display-helpers.js +445 -0
  260. package/lib/protection/protection-display.js +215 -0
  261. package/lib/protection/protection-preset-registry.js +97 -0
  262. package/lib/protection/report-exit.js +62 -0
  263. package/lib/protection/resolve.js +202 -0
  264. package/lib/protection/run-commands-validate.js +130 -0
  265. package/lib/protection/run-commands.js +216 -0
  266. package/lib/protection/run-protection-create-helpers.js +134 -0
  267. package/lib/protection/run-protection-create-probes-flow.js +43 -0
  268. package/lib/protection/run-protection-create.js +191 -0
  269. package/lib/protection/scope.js +22 -0
  270. package/lib/protection/sync-after-upload.js +64 -0
  271. package/lib/protection/upload-batch.js +127 -0
  272. package/lib/protection/validate-batch.js +113 -0
  273. package/lib/protection/validate-local.js +65 -0
  274. package/lib/resolvers/dimension-file.js +29 -4
  275. package/lib/schema/application-schema.json +1 -1
  276. package/lib/schema/datasource-test-run.schema.json +89 -6
  277. package/lib/schema/external-datasource.schema.json +2636 -2588
  278. package/lib/schema/external-system.schema.json +1089 -761
  279. package/lib/schema/flag-map-validation-run.json +2 -2
  280. package/lib/schema/infra.parameter.yaml +45 -0
  281. package/lib/schema/protection.schema.json +374 -0
  282. package/lib/schema/type/credential-field-registry.json +642 -0
  283. package/lib/schema/wizard-config.schema.json +1 -1
  284. package/lib/utils/agent-trust-run-display.js +231 -0
  285. package/lib/utils/agent-trust-run-exit.js +59 -0
  286. package/lib/utils/agent-trust-wait-ui.js +94 -0
  287. package/lib/utils/aifabrix-test-runtime-guard.js +125 -0
  288. package/lib/utils/api.js +55 -31
  289. package/lib/utils/app-relative-path-env.js +112 -0
  290. package/lib/utils/cli-exit-codes.js +30 -0
  291. package/lib/utils/cli-secrets-error-format.js +83 -21
  292. package/lib/utils/cli-sync-options.js +33 -0
  293. package/lib/utils/cli-utils.js +16 -4
  294. package/lib/utils/compose-generate-docker-compose.js +4 -0
  295. package/lib/utils/compose-traefik-ingress-base.js +16 -1
  296. package/lib/utils/config-secrets-path.js +46 -0
  297. package/lib/utils/controller-url.js +88 -1
  298. package/lib/utils/credential-push-ui.js +90 -0
  299. package/lib/utils/credential-secrets-env.js +137 -9
  300. package/lib/utils/dataplane-cli-version-gate.js +202 -0
  301. package/lib/utils/dataplane-cli-version-help.js +74 -0
  302. package/lib/utils/dataplane-resolver.js +129 -2
  303. package/lib/utils/datasource-export-display.js +137 -0
  304. package/lib/utils/datasource-load-display.js +174 -0
  305. package/lib/utils/datasource-load-export-ui.js +120 -0
  306. package/lib/utils/datasource-test-run-capability-scope.js +3 -3
  307. package/lib/utils/datasource-test-run-display.js +3 -3
  308. package/lib/utils/datasource-test-run-tty-log.js +5 -1
  309. package/lib/utils/declarative-url-ports.js +3 -3
  310. package/lib/utils/deployment-auth-mode.js +41 -0
  311. package/lib/utils/developer-volume-id.js +33 -0
  312. package/lib/utils/env-copy.js +14 -9
  313. package/lib/utils/env-kv-orphan-lines.js +43 -0
  314. package/lib/utils/error-formatters/network-errors.js +13 -4
  315. package/lib/utils/external-env-template.js +1 -0
  316. package/lib/utils/external-system-readiness-core.js +10 -0
  317. package/lib/utils/external-system-readiness-display.js +80 -23
  318. package/lib/utils/health-check.js +4 -1
  319. package/lib/utils/help-builder.js +35 -26
  320. package/lib/utils/keycloak-realm-ready.js +101 -0
  321. package/lib/utils/load-cip-capacity-display-config.js +61 -15
  322. package/lib/utils/manifest-source-emit.js +24 -0
  323. package/lib/utils/pipeline-upload-auth-retry.js +82 -0
  324. package/lib/utils/platform-controller-url.js +156 -0
  325. package/lib/utils/postgres-platform-bootstrap.js +199 -0
  326. package/lib/utils/postgres-wipe.js +9 -2
  327. package/lib/utils/resolve-env-mode.js +47 -0
  328. package/lib/utils/secrets-generator.js +31 -25
  329. package/lib/utils/secrets-missing-error.js +164 -0
  330. package/lib/utils/semver-compare.js +127 -0
  331. package/lib/utils/service-url-label.js +81 -0
  332. package/lib/utils/setup-quiet-output.js +25 -0
  333. package/lib/utils/template-helpers.js +15 -0
  334. package/lib/utils/token-manager-client-credentials.js +201 -0
  335. package/lib/utils/token-manager-messages.js +8 -0
  336. package/lib/utils/token-manager-refresh.js +12 -2
  337. package/lib/utils/token-manager.js +86 -105
  338. package/lib/utils/upload-pipeline-ui.js +98 -0
  339. package/lib/utils/upload-sync-options.js +42 -0
  340. package/lib/utils/url-declarative-public-base.js +21 -3
  341. package/lib/utils/url-declarative-vdir-inactive-env.js +14 -4
  342. package/lib/utils/validation-poll-ui.js +22 -7
  343. package/lib/utils/validation-run-stage.js +45 -0
  344. package/lib/validation/wizard-config-validator.js +58 -19
  345. package/package.json +1 -1
  346. package/templates/applications/dataplane/application.yaml +2 -1
  347. package/templates/applications/dataplane/env.template +36 -1
  348. package/templates/applications/dataplane/rbac.yaml +4 -0
  349. package/templates/applications/miso-controller/env.template +16 -0
  350. package/templates/external-system/README.md.hbs +116 -11
  351. package/templates/python/docker-compose.hbs +19 -18
  352. package/templates/typescript/docker-compose.hbs +18 -17
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @fileoverview TTY display for agent metadata trust runs (plan 143).
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('./logger');
11
+ const { sectionTitle, headerKeyValue, metadata: metaGray } = require('./cli-test-layout-chalk');
12
+ const { statusGlyph } = require('./datasource-test-run-display');
13
+ const { trustDecisionToRowStatus } = require('../datasource/agent-trust-map');
14
+
15
+ const SEP = '────────────────────────────────';
16
+
17
+ /** @type {{ datasource: number, trust: number, conf: number, status: number, cache: number }} */
18
+ const TRUST_TABLE_COLS = {
19
+ datasource: 30,
20
+ trust: 24,
21
+ conf: 6,
22
+ status: 8,
23
+ cache: 8
24
+ };
25
+
26
+ function trustGlyph(trustDecision) {
27
+ return statusGlyph(trustDecisionToRowStatus(trustDecision));
28
+ }
29
+
30
+ function publishGateLine(trustDecision, strict) {
31
+ if (trustDecision === 'notTrusted') return chalk.red('blocked (trustDecision notTrusted)');
32
+ if (strict) {
33
+ return trustDecision === 'trusted'
34
+ ? chalk.green('allowed (strict: trusted)')
35
+ : chalk.yellow('blocked (strict requires trusted)');
36
+ }
37
+ return chalk.green('allowed (warning-only); use --strict to require trusted');
38
+ }
39
+
40
+ function logTrustHeader(run, env) {
41
+ logger.log(SEP);
42
+ logger.log(sectionTitle(`Semantic trust — ${run.datasourceKey}`));
43
+ logger.log(headerKeyValue('Environment:', env));
44
+ logger.log(headerKeyValue('System:', run.systemKey || '—'));
45
+ logger.log(SEP);
46
+ logger.log(metaGray(' Layer checked: business metadata (404.5 agent validation)'));
47
+ logger.log(metaGray(' Not checked here: vendor connectivity, sync, record counts (use test-e2e)'));
48
+ logger.log('');
49
+ }
50
+
51
+ function logTrustSummaryBlock(run) {
52
+ const confPct = Math.round((run.confidence || 0) * 100);
53
+ logger.log(
54
+ headerKeyValue(
55
+ 'Trust:',
56
+ `${trustGlyph(run.trustDecision)} ${run.trustDecision} · confidence ${confPct}% · ${run.validationStatus}`
57
+ )
58
+ );
59
+ if (!run.validatedAt) return;
60
+ const hashShort = run.inputHash ? `${String(run.inputHash).slice(0, 8)}…` : '—';
61
+ logger.log(
62
+ metaGray(
63
+ ` Validated: ${run.validatedAt} · inputHash ${hashShort} · cache: ${run.cacheHit ? 'hit' : 'miss'}`
64
+ )
65
+ );
66
+ }
67
+
68
+ function logTrustWarningsAndFindings(run, opts) {
69
+ if (run.summary) {
70
+ logger.log('');
71
+ logger.log(chalk.white(' Summary'));
72
+ logger.log(` ${run.summary}`);
73
+ }
74
+ const warnings = Array.isArray(run.highLevelWarnings) ? run.highLevelWarnings : [];
75
+ const findings = Array.isArray(run.findings) ? run.findings : [];
76
+ const showVerboseFindings = opts.verbose === true && findings.length > 0;
77
+ if (warnings.length && !showVerboseFindings) {
78
+ logger.log('');
79
+ logger.log(chalk.yellow(' Warnings (high level)'));
80
+ warnings.forEach(w => logger.log(` • ${w}`));
81
+ }
82
+ if (!showVerboseFindings) return;
83
+ logger.log('');
84
+ logger.log(chalk.white(' Findings (verbose, max 10)'));
85
+ findings.slice(0, 10).forEach(f => {
86
+ logger.log(` • [${f.severity || 'info'}] ${f.code || 'FINDING'}: ${f.message || ''}`);
87
+ });
88
+ }
89
+
90
+ function logTrustFooter(run, opts) {
91
+ logger.log('');
92
+ logger.log(metaGray(' ── Integration reality (informative) ──'));
93
+ logger.log(metaGray(` Local manifest: ${opts.noSync ? 'not uploaded (--no-sync)' : 'uploaded before run'}`));
94
+ logger.log(metaGray(` Publish gate: ${publishGateLine(run.trustDecision, opts.strict === true)}`));
95
+ logger.log(SEP);
96
+ }
97
+
98
+ function displayAgentTrustRunTTY(run, opts = {}) {
99
+ if (!run || opts.json === true) return;
100
+ const env = opts.environment ? String(opts.environment) : 'dev';
101
+ logTrustHeader(run, env);
102
+ logTrustSummaryBlock(run);
103
+ logTrustWarningsAndFindings(run, opts);
104
+ logTrustFooter(run, opts);
105
+ }
106
+
107
+ function trustCacheLabel(trustRun) {
108
+ if (!trustRun || typeof trustRun !== 'object') return '—';
109
+ if (trustRun.readLatest === true) return 'latest';
110
+ if (trustRun.cacheHit === true) return 'hit';
111
+ if (trustRun.cacheHit === false) return 'miss';
112
+ return '—';
113
+ }
114
+
115
+ /**
116
+ * @param {boolean} showCache
117
+ * @returns {number}
118
+ */
119
+ function trustTableLineWidth(showCache) {
120
+ const { datasource, trust, conf, status, cache } = TRUST_TABLE_COLS;
121
+ let w = 2 + datasource + 1 + trust + 1 + conf + 1 + status;
122
+ if (showCache) w += 1 + cache;
123
+ return w;
124
+ }
125
+
126
+ /**
127
+ * @param {string} key
128
+ * @returns {string}
129
+ */
130
+ function formatTrustTableDatasourceCol(key) {
131
+ return String(key || '').padEnd(TRUST_TABLE_COLS.datasource).slice(0, TRUST_TABLE_COLS.datasource);
132
+ }
133
+
134
+ /**
135
+ * @param {Object|null|undefined} tr
136
+ * @returns {string}
137
+ */
138
+ function formatTrustTableTrustCol(tr) {
139
+ const w = TRUST_TABLE_COLS.trust;
140
+ if (!tr || !tr.trustDecision) return '—'.padEnd(w);
141
+ const decision = String(tr.trustDecision);
142
+ const cell = `${trustGlyph(decision)} ${decision}`;
143
+ return cell.length > w ? cell.slice(0, w) : cell.padEnd(w);
144
+ }
145
+
146
+ /**
147
+ * @param {Object|null|undefined} tr
148
+ * @returns {string}
149
+ */
150
+ function formatTrustTableConfCol(tr) {
151
+ const w = TRUST_TABLE_COLS.conf;
152
+ if (!tr || typeof tr.confidence !== 'number') return '—'.padStart(w);
153
+ return `${Math.round(tr.confidence * 100)}%`.padStart(w);
154
+ }
155
+
156
+ /**
157
+ * @param {Object|null|undefined} tr
158
+ * @param {Object} row
159
+ * @returns {string}
160
+ */
161
+ function formatTrustTableStatusCol(tr, row) {
162
+ const w = TRUST_TABLE_COLS.status;
163
+ const raw = tr ? String(tr.validationStatus || '—') : row.error || 'fail';
164
+ return raw.padEnd(w).slice(0, w);
165
+ }
166
+
167
+ /**
168
+ * @param {boolean} showCache
169
+ */
170
+ function logSystemTrustTableHeader(showCache) {
171
+ const { datasource, trust, conf, status, cache } = TRUST_TABLE_COLS;
172
+ let header = ` ${'Datasource'.padEnd(datasource)} ${'Trust'.padEnd(trust)} ${'Conf.'.padStart(conf)} ${'Status'.padEnd(status)}`;
173
+ if (showCache) header += ` ${'Cache'.padEnd(cache)}`;
174
+ logger.log(chalk.white(header));
175
+ logger.log(chalk.gray(` ${'─'.repeat(trustTableLineWidth(showCache))}`));
176
+ }
177
+
178
+ /**
179
+ * @param {Object} row
180
+ * @param {boolean} showCache
181
+ * @returns {string}
182
+ */
183
+ function formatTrustTableRow(row, showCache) {
184
+ const tr = row.trustRun;
185
+ let line = ` ${formatTrustTableDatasourceCol(row.key)} ${formatTrustTableTrustCol(tr)} ${formatTrustTableConfCol(tr)} ${formatTrustTableStatusCol(tr, row)}`;
186
+ if (showCache) {
187
+ line += ` ${trustCacheLabel(tr).padEnd(TRUST_TABLE_COLS.cache).slice(0, TRUST_TABLE_COLS.cache)}`;
188
+ }
189
+ return line;
190
+ }
191
+
192
+ function logSystemTrustTable(list, opts = {}) {
193
+ const showCache = opts.showCache === true;
194
+ logSystemTrustTableHeader(showCache);
195
+ for (const row of list) {
196
+ logger.log(formatTrustTableRow(row, showCache));
197
+ }
198
+ }
199
+
200
+ function displaySystemTrustRollupTTY(systemKey, rows, opts = {}) {
201
+ if (opts.json === true) return;
202
+ const list = Array.isArray(rows) ? rows : [];
203
+ logger.log(SEP);
204
+ logger.log(sectionTitle(`Semantic trust — system ${systemKey} (${list.length} datasources)`));
205
+ const { deriveSystemStatus } = require('./external-system-system-test-tty');
206
+ const worst = deriveSystemStatus(
207
+ list.map(r => ({
208
+ success: r.success !== false,
209
+ datasourceTestRun: {
210
+ status: r.trustRun ? trustDecisionToRowStatus(r.trustRun.trustDecision) : 'fail'
211
+ }
212
+ }))
213
+ );
214
+ logger.log(
215
+ headerKeyValue('System trust:', `${statusGlyph(worst)} ${worst} (worst-of rollup)`)
216
+ );
217
+ logger.log('');
218
+ logSystemTrustTable(list, opts);
219
+ logger.log('');
220
+ logger.log(metaGray(' ── What this run proved ──'));
221
+ logger.log(metaGray(` ✔ Semantic metadata reviewed for ${list.length} datasource(s) on dataplane`));
222
+ logger.log(metaGray(' ✖ Did not call external APIs or run CIP (not E2E)'));
223
+ logger.log(SEP);
224
+ }
225
+
226
+ module.exports = {
227
+ displayAgentTrustRunTTY,
228
+ displaySystemTrustRollupTTY,
229
+ formatTrustTableRow,
230
+ TRUST_TABLE_COLS
231
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview Exit codes for agent metadata trust CLI runs (plan 143).
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { trustDecisionToRowStatus } = require('../datasource/agent-trust-map');
10
+ const { deriveSystemStatus } = require('./external-system-system-test-tty');
11
+ const { computeExitCodeFromDatasourceTestRun } = require('./datasource-test-run-exit');
12
+
13
+ /**
14
+ * @param {Object|null|undefined} run
15
+ * @param {Object} [opts]
16
+ * @param {boolean} [opts.warningsAsErrors]
17
+ * @param {boolean} [opts.strict]
18
+ * @returns {number}
19
+ */
20
+ function computeExitCodeFromTrustRun(run, opts = {}) {
21
+ if (!run || typeof run !== 'object') return 3;
22
+ const td = run.trustDecision;
23
+ if (run.validationStatus === 'failed' || td === 'notTrusted') return 1;
24
+ if (opts.strict === true && td !== 'trusted') return 1;
25
+ if (opts.warningsAsErrors === true && td === 'usableWithWarnings') return 1;
26
+ if (td === 'trusted' || td === 'usableWithWarnings' || td === 'pending') return 0;
27
+ return 3;
28
+ }
29
+
30
+ /**
31
+ * @param {Array<{ success?: boolean, trustRun?: Object|null }>} rows
32
+ * @param {Object} [opts]
33
+ * @returns {number}
34
+ */
35
+ function computeSystemExitCodeFromTrustRows(rows, opts = {}) {
36
+ const list = Array.isArray(rows) ? rows : [];
37
+ let worst = 0;
38
+ for (const row of list) {
39
+ const code = computeExitCodeFromTrustRun(row && row.trustRun, opts);
40
+ if (code === 3) return 3;
41
+ if (code > worst) worst = code;
42
+ }
43
+ if (worst > 0) return worst;
44
+ const synthetic = list.map(r => {
45
+ const tr = r && r.trustRun;
46
+ const st = tr ? trustDecisionToRowStatus(tr.trustDecision) : 'fail';
47
+ return { success: st !== 'fail', datasourceTestRun: { status: st } };
48
+ });
49
+ const rollup = deriveSystemStatus(synthetic);
50
+ return computeExitCodeFromDatasourceTestRun(
51
+ { status: rollup },
52
+ { warningsAsErrors: opts.warningsAsErrors === true }
53
+ );
54
+ }
55
+
56
+ module.exports = {
57
+ computeExitCodeFromTrustRun,
58
+ computeSystemExitCodeFromTrustRows
59
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * TTY ora spinner for slow agent metadata validation POST (non-cached path).
3
+ *
4
+ * @fileoverview Agent trust wait presentation (aligned with validation-poll-ui)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ function shouldUseAgentTrustWaitSpinner() {
15
+ return Boolean(process && process.stdout && process.stdout.isTTY);
16
+ }
17
+
18
+ /**
19
+ * @param {string} [datasourceKey]
20
+ * @param {number} deadlineMs
21
+ * @returns {string}
22
+ */
23
+ function buildAgentTrustWaitSpinnerText(datasourceKey, deadlineMs) {
24
+ const remainingSec = Math.max(0, Math.ceil((deadlineMs - Date.now()) / 1000));
25
+ const key =
26
+ datasourceKey && String(datasourceKey).trim() ? String(datasourceKey).trim() : 'datasource';
27
+ return `Waiting for semantic trust validation… ${key} (~${remainingSec}s left)`;
28
+ }
29
+
30
+ /**
31
+ * Run a blocking validate POST behind a TTY spinner (skipped when not a TTY).
32
+ *
33
+ * @async
34
+ * @param {Function} work - Zero-arg async function performing the HTTP call
35
+ * @param {Object} opts
36
+ * @param {string} [opts.datasourceKey]
37
+ * @param {number} opts.timeoutMs
38
+ * @returns {Promise<*>}
39
+ */
40
+ async function runWithAgentTrustWaitSpinner(work, opts = {}) {
41
+ const timeoutMs =
42
+ Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 120000;
43
+ const deadlineMs = Date.now() + timeoutMs;
44
+ const datasourceKey = opts.datasourceKey;
45
+
46
+ if (!shouldUseAgentTrustWaitSpinner()) {
47
+ return work();
48
+ }
49
+
50
+ const ora = require('ora');
51
+ const spinner = ora({
52
+ text: buildAgentTrustWaitSpinnerText(datasourceKey, deadlineMs),
53
+ spinner: 'dots'
54
+ }).start();
55
+
56
+ const tick = setInterval(() => {
57
+ spinner.text = buildAgentTrustWaitSpinnerText(datasourceKey, deadlineMs);
58
+ }, 1000);
59
+
60
+ try {
61
+ return await work();
62
+ } finally {
63
+ clearInterval(tick);
64
+ spinner.stop();
65
+ }
66
+ }
67
+
68
+ const NON_TTY_LOG_MS = 5000;
69
+
70
+ /**
71
+ * Optional one-line hint on non-TTY before a slow validate (throttled by caller usage).
72
+ *
73
+ * @param {string} [datasourceKey]
74
+ * @param {number} timeoutMs
75
+ */
76
+ function logAgentTrustWaitHintOnce(datasourceKey, timeoutMs) {
77
+ if (shouldUseAgentTrustWaitSpinner()) return;
78
+ const budgetSec = Math.max(1, Math.ceil(timeoutMs / 1000));
79
+ const key =
80
+ datasourceKey && String(datasourceKey).trim() ? String(datasourceKey).trim() : 'datasource';
81
+ logger.log(
82
+ chalk.gray(
83
+ `Waiting for semantic trust validation… ${key} (may take up to ~${budgetSec}s; not cached)`
84
+ )
85
+ );
86
+ }
87
+
88
+ module.exports = {
89
+ shouldUseAgentTrustWaitSpinner,
90
+ buildAgentTrustWaitSpinnerText,
91
+ runWithAgentTrustWaitSpinner,
92
+ logAgentTrustWaitHintOnce,
93
+ NON_TTY_LOG_MS
94
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Refuse writes to live ~/.aifabrix paths during Jest runs.
3
+ *
4
+ * @fileoverview Test-runtime write guard for secrets and admin-secrets files
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const ALLOW_ENV = 'ALLOW_AIFABRIX_TEST_WRITE_REAL_CONFIG';
16
+
17
+ /**
18
+ * @returns {boolean}
19
+ */
20
+ function isJestTestProcess() {
21
+ return Boolean(process.env.JEST_WORKER_ID) || process.env.NODE_ENV === 'test';
22
+ }
23
+
24
+ /** Legacy mock Fabrix dir used across unit tests (not the operator tree when homedir differs). */
25
+ const JEST_MOCK_FABRIX_ROOT = path.join('/home', 'test', '.aifabrix');
26
+
27
+ /** Additional protected trees in cloud / monorepo dev (see plan 185 Phase 2.5). */
28
+ const EXTRA_PROTECTED_FABRIX_ROOTS = ['/workspace/.aifabrix'];
29
+
30
+ /**
31
+ * Resolve symlinks so `~/.aifabrix` → `/workspace/.aifabrix` matches both paths.
32
+ * @param {string} p
33
+ * @returns {string}
34
+ */
35
+ function resolveFabrixPathKey(p) {
36
+ const resolved = path.resolve(p);
37
+ try {
38
+ if (fs.existsSync(resolved)) {
39
+ return fs.realpathSync(resolved);
40
+ }
41
+ } catch {
42
+ // ignore
43
+ }
44
+ return resolved;
45
+ }
46
+
47
+ /**
48
+ * @returns {string[]} Absolute live config directory paths (deduped by realpath).
49
+ */
50
+ function getLiveFabrixConfigDirCandidates() {
51
+ const keys = new Set();
52
+ const dirs = [];
53
+ const add = (dir) => {
54
+ if (!dir || typeof dir !== 'string') return;
55
+ const key = resolveFabrixPathKey(dir);
56
+ if (keys.has(key)) return;
57
+ keys.add(key);
58
+ dirs.push(key);
59
+ };
60
+ add(path.join(path.resolve(os.homedir()), '.aifabrix'));
61
+ for (const root of EXTRA_PROTECTED_FABRIX_ROOTS) {
62
+ add(root);
63
+ }
64
+ return dirs;
65
+ }
66
+
67
+ /**
68
+ * True when the path is the operator's live ~/.aifabrix tree (not the legacy /home/test mock).
69
+ * @param {string} filePath
70
+ * @returns {boolean}
71
+ */
72
+ function isLiveFabrixConfigPath(filePath) {
73
+ if (!filePath || typeof filePath !== 'string') return false;
74
+ const normalized = resolveFabrixPathKey(filePath);
75
+ if (
76
+ normalized === JEST_MOCK_FABRIX_ROOT ||
77
+ normalized.startsWith(`${JEST_MOCK_FABRIX_ROOT}${path.sep}`)
78
+ ) {
79
+ return false;
80
+ }
81
+ for (const liveRoot of getLiveFabrixConfigDirCandidates()) {
82
+ if (normalized === liveRoot || normalized.startsWith(`${liveRoot}${path.sep}`)) {
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * During Jest, writes are allowed everywhere except the live ~/.aifabrix tree.
91
+ * @param {string} filePath
92
+ * @returns {boolean}
93
+ */
94
+ function isAllowedTestWritePath(filePath) {
95
+ if (!filePath || typeof filePath !== 'string') return true;
96
+ if (process.env[ALLOW_ENV] === '1' || process.env[ALLOW_ENV] === 'true') {
97
+ return true;
98
+ }
99
+ if (!isJestTestProcess()) return true;
100
+ return !isLiveFabrixConfigPath(filePath);
101
+ }
102
+
103
+ /**
104
+ * @param {string} filePath
105
+ * @throws {Error} When Jest would write outside sandbox
106
+ */
107
+ function assertWritableSecretsPathForTests(filePath) {
108
+ if (!isJestTestProcess()) return;
109
+ if (isAllowedTestWritePath(filePath)) return;
110
+ throw new Error(
111
+ `Refusing to write secrets to live config path during tests: ${filePath}. ` +
112
+ 'Use the Jest sandbox (default) or set ALLOW_AIFABRIX_TEST_WRITE_REAL_CONFIG=1.'
113
+ );
114
+ }
115
+
116
+ module.exports = {
117
+ ALLOW_ENV,
118
+ EXTRA_PROTECTED_FABRIX_ROOTS,
119
+ isJestTestProcess,
120
+ resolveFabrixPathKey,
121
+ getLiveFabrixConfigDirCandidates,
122
+ isLiveFabrixConfigPath,
123
+ isAllowedTestWritePath,
124
+ assertWritableSecretsPathForTests
125
+ };
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
  }