@aifabrix/builder 2.43.0 → 2.44.1

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 (371) hide show
  1. package/.cursor/rules/anchor-docs.mdc +15 -0
  2. package/.cursor/rules/cli-layout.mdc +75 -0
  3. package/.cursor/rules/project-rules.mdc +8 -0
  4. package/.npmrc.token +1 -0
  5. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  6. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  7. package/.nyc_output/processinfo/index.json +1 -0
  8. package/README.md +1 -1
  9. package/anchor-docs/README.md +10 -0
  10. package/anchor-docs/_TEMPLATE +24 -0
  11. package/bin/aifabrix.js +13 -4
  12. package/integration/hubspot-test/README.md +31 -0
  13. package/integration/hubspot-test/create-hubspot.js +5 -5
  14. package/integration/hubspot-test/hubspot-test-datasource-company.json +58 -462
  15. package/integration/hubspot-test/hubspot-test-datasource-contact.json +61 -555
  16. package/integration/hubspot-test/hubspot-test-datasource-deal.json +63 -506
  17. package/integration/hubspot-test/hubspot-test-datasource-users.json +42 -83
  18. package/integration/hubspot-test/hubspot-test-deploy.json +3 -3
  19. package/integration/hubspot-test/test-dataplane-down-tests.js +1 -7
  20. package/integration/hubspot-test/test-dataplane-down.js +3 -3
  21. package/integration/hubspot-test/test.js +35 -43
  22. package/integration/hubspot-test/wizard-hubspot-test-headless.yaml +23 -0
  23. package/integration/roundtrip-test-local/README.md +144 -0
  24. package/integration/roundtrip-test-local/application.yaml +13 -0
  25. package/integration/roundtrip-test-local/env.template +15 -0
  26. package/integration/roundtrip-test-local/roundtrip-test-local-datasource-roundtrip-test-company.yaml +14 -0
  27. package/integration/roundtrip-test-local/roundtrip-test-local-deploy.json +61 -0
  28. package/integration/roundtrip-test-local/roundtrip-test-local-system.yaml +25 -0
  29. package/integration/roundtrip-test-local2/README.md +144 -0
  30. package/integration/roundtrip-test-local2/application.yaml +13 -0
  31. package/integration/roundtrip-test-local2/env.template +15 -0
  32. package/integration/roundtrip-test-local2/roundtrip-test-local2-datasource-company.yaml +31 -0
  33. package/integration/roundtrip-test-local2/roundtrip-test-local2-deploy.json +86 -0
  34. package/integration/roundtrip-test-local2/roundtrip-test-local2-system.yaml +25 -0
  35. package/integration/test/wizard.yaml +8 -0
  36. package/jest.config.default.js +10 -0
  37. package/jest.config.integration.fixtures.js +22 -0
  38. package/jest.config.integration.js +21 -18
  39. package/jest.config.isolated.js +10 -0
  40. package/jest.projects.js +301 -0
  41. package/lib/api/certificates.api.js +62 -0
  42. package/lib/api/datasources-core.api.js +3 -3
  43. package/lib/api/dev-mtls-request.js +110 -0
  44. package/lib/api/dev-server-https.js +145 -0
  45. package/lib/api/dev.api.js +133 -144
  46. package/lib/api/index.js +11 -3
  47. package/lib/api/pipeline.api.js +67 -20
  48. package/lib/api/types/certificates.types.js +48 -0
  49. package/lib/api/types/dev.types.js +4 -3
  50. package/lib/api/types/pipeline.types.js +8 -5
  51. package/lib/api/types/validation-run.types.js +56 -0
  52. package/lib/api/validation-run.api.js +111 -0
  53. package/lib/api/validation-runner.js +109 -0
  54. package/lib/app/certification-show-enrich.js +129 -0
  55. package/lib/app/certification-verify-rows.js +60 -0
  56. package/lib/app/config.js +1 -1
  57. package/lib/app/deploy-status-display.js +2 -2
  58. package/lib/app/deploy.js +7 -6
  59. package/lib/app/display.js +2 -1
  60. package/lib/app/dockerfile.js +3 -2
  61. package/lib/app/down.js +2 -1
  62. package/lib/app/helpers.js +6 -5
  63. package/lib/app/index.js +27 -8
  64. package/lib/app/list.js +7 -6
  65. package/lib/app/push.js +4 -3
  66. package/lib/app/register.js +16 -7
  67. package/lib/app/rotate-secret.js +14 -13
  68. package/lib/app/run-container-start.js +184 -0
  69. package/lib/app/run-docker-fallback.js +108 -0
  70. package/lib/app/run-env-compose.js +30 -42
  71. package/lib/app/run-helpers.js +49 -126
  72. package/lib/app/run-infra-requirements.js +30 -0
  73. package/lib/app/run-resolve-image.js +21 -0
  74. package/lib/app/run.js +74 -21
  75. package/lib/app/show-display.js +44 -1
  76. package/lib/app/show.js +93 -9
  77. package/lib/build/index.js +13 -10
  78. package/lib/certification/cli-cert-sync-skip.js +21 -0
  79. package/lib/certification/merge-certification-from-artifact.js +185 -0
  80. package/lib/certification/post-unified-cert-sync.js +33 -0
  81. package/lib/certification/sync-after-external-command.js +52 -0
  82. package/lib/certification/sync-system-certification.js +197 -0
  83. package/lib/cli/index.js +2 -0
  84. package/lib/cli/setup-app.help.js +67 -0
  85. package/lib/cli/setup-app.js +61 -121
  86. package/lib/cli/setup-app.test-commands.js +195 -0
  87. package/lib/cli/setup-auth.js +19 -5
  88. package/lib/cli/setup-credential-deployment.js +22 -8
  89. package/lib/cli/setup-dev-path-commands.js +124 -0
  90. package/lib/cli/setup-dev.js +170 -113
  91. package/lib/cli/setup-environment.js +7 -1
  92. package/lib/cli/setup-external-system.js +84 -23
  93. package/lib/cli/setup-infra.js +126 -47
  94. package/lib/cli/setup-parameters.js +32 -0
  95. package/lib/cli/setup-secrets.js +137 -18
  96. package/lib/cli/setup-service-user.js +1 -1
  97. package/lib/cli/setup-utility.js +54 -22
  98. package/lib/commands/app-down.js +5 -7
  99. package/lib/commands/app-install.js +14 -7
  100. package/lib/commands/app-logs.js +13 -10
  101. package/lib/commands/app-shell.js +4 -1
  102. package/lib/commands/app-test.js +25 -19
  103. package/lib/commands/app.js +32 -11
  104. package/lib/commands/auth-config.js +6 -6
  105. package/lib/commands/auth-status.js +4 -3
  106. package/lib/commands/credential-env.js +4 -3
  107. package/lib/commands/credential-list.js +5 -4
  108. package/lib/commands/credential-push.js +4 -3
  109. package/lib/commands/datasource-unified-test-cli.js +428 -0
  110. package/lib/commands/datasource-unified-test-cli.options.js +191 -0
  111. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
  112. package/lib/commands/datasource-validation-cli.js +143 -0
  113. package/lib/commands/datasource.js +125 -95
  114. package/lib/commands/deployment-list.js +6 -5
  115. package/lib/commands/dev-cli-handlers.js +122 -18
  116. package/lib/commands/dev-down.js +4 -3
  117. package/lib/commands/dev-init.js +231 -116
  118. package/lib/commands/dev-show-display.js +473 -0
  119. package/lib/commands/login-credentials.js +3 -2
  120. package/lib/commands/login-device.js +4 -3
  121. package/lib/commands/login.js +5 -4
  122. package/lib/commands/logout.js +8 -7
  123. package/lib/commands/parameters-validate.js +54 -0
  124. package/lib/commands/repair-datasource.js +314 -68
  125. package/lib/commands/repair-env-template.js +2 -2
  126. package/lib/commands/repair.js +21 -3
  127. package/lib/commands/secrets-list.js +23 -12
  128. package/lib/commands/secrets-remove-all.js +220 -0
  129. package/lib/commands/secrets-remove.js +21 -12
  130. package/lib/commands/secrets-set.js +21 -12
  131. package/lib/commands/secrets-validate.js +4 -4
  132. package/lib/commands/secure.js +10 -9
  133. package/lib/commands/service-user.js +26 -25
  134. package/lib/commands/test-e2e-external.js +27 -1
  135. package/lib/commands/up-common.js +3 -2
  136. package/lib/commands/up-dataplane.js +29 -16
  137. package/lib/commands/up-miso.js +19 -29
  138. package/lib/commands/upload.js +149 -39
  139. package/lib/commands/wizard-core-helpers.js +1 -1
  140. package/lib/commands/wizard-dataplane.js +4 -3
  141. package/lib/commands/wizard-helpers.js +3 -3
  142. package/lib/commands/wizard.js +2 -2
  143. package/lib/core/admin-secrets.js +14 -5
  144. package/lib/core/audit-logger.js +12 -4
  145. package/lib/core/config-attach-extensions.js +46 -0
  146. package/lib/core/config-runtime-paths.js +29 -0
  147. package/lib/core/config.js +55 -56
  148. package/lib/core/diff.js +3 -2
  149. package/lib/core/ensure-encryption-key.js +1 -1
  150. package/lib/core/secrets-ensure-infra.js +77 -0
  151. package/lib/core/secrets-ensure.js +120 -64
  152. package/lib/core/secrets-env-write.js +35 -7
  153. package/lib/core/secrets-infra-placeholder-sync.js +61 -0
  154. package/lib/core/secrets.js +200 -37
  155. package/lib/core/templates-env.js +4 -3
  156. package/lib/datasource/abac-validator.js +1 -10
  157. package/lib/datasource/deploy.js +75 -53
  158. package/lib/datasource/field-reference-validator.js +9 -6
  159. package/lib/datasource/integration-context.js +63 -0
  160. package/lib/datasource/list.js +8 -7
  161. package/lib/datasource/log-viewer.js +189 -67
  162. package/lib/datasource/resolve-app.js +4 -4
  163. package/lib/datasource/test-e2e.js +113 -146
  164. package/lib/datasource/test-integration.js +114 -122
  165. package/lib/datasource/unified-validation-run-body.js +68 -0
  166. package/lib/datasource/unified-validation-run-post.js +23 -0
  167. package/lib/datasource/unified-validation-run-resolve.js +43 -0
  168. package/lib/datasource/unified-validation-run.js +93 -0
  169. package/lib/datasource/validate.js +157 -13
  170. package/lib/deployment/deployer.js +4 -3
  171. package/lib/deployment/environment.js +7 -6
  172. package/lib/deployment/push.js +17 -8
  173. package/lib/external-system/delete.js +4 -3
  174. package/lib/external-system/deploy.js +166 -53
  175. package/lib/external-system/download-helpers.js +1 -1
  176. package/lib/external-system/download.js +7 -6
  177. package/lib/external-system/generator.js +92 -6
  178. package/lib/external-system/integration-test-dispatch.js +26 -0
  179. package/lib/external-system/test-execution.js +5 -1
  180. package/lib/external-system/test-helpers.js +0 -4
  181. package/lib/external-system/test-system-level-helpers.js +110 -0
  182. package/lib/external-system/test-system-level.js +83 -44
  183. package/lib/external-system/test.js +59 -8
  184. package/lib/generator/builders.js +23 -11
  185. package/lib/generator/deploy-manifest-azure-kv.js +81 -0
  186. package/lib/generator/external.js +16 -4
  187. package/lib/generator/helpers.js +58 -3
  188. package/lib/generator/index.js +4 -0
  189. package/lib/generator/split-readme.js +12 -7
  190. package/lib/generator/split-variables.js +2 -1
  191. package/lib/generator/split.js +1 -1
  192. package/lib/generator/wizard-readme.js +3 -3
  193. package/lib/generator/wizard.js +8 -8
  194. package/lib/infrastructure/compose.js +70 -7
  195. package/lib/infrastructure/helpers-docker-check.js +67 -0
  196. package/lib/infrastructure/helpers.js +203 -42
  197. package/lib/infrastructure/index.js +31 -18
  198. package/lib/infrastructure/services.js +21 -67
  199. package/lib/internal/fs-real-sync.js +104 -0
  200. package/lib/internal/node-fs.js +98 -0
  201. package/lib/parameters/database-secret-values.js +173 -0
  202. package/lib/parameters/infra-kv-discovery.js +121 -0
  203. package/lib/parameters/infra-parameter-catalog.js +458 -0
  204. package/lib/parameters/infra-parameter-validate.js +64 -0
  205. package/lib/schema/application-schema.json +37 -17
  206. package/lib/schema/datasource-test-run.schema.json +493 -0
  207. package/lib/schema/deployment-rules.yaml +102 -63
  208. package/lib/schema/external-datasource.schema.json +1200 -442
  209. package/lib/schema/external-system.schema.json +203 -5
  210. package/lib/schema/flag-map-validation-run.json +31 -0
  211. package/lib/schema/infra-parameter.schema.json +106 -0
  212. package/lib/schema/infra.parameter.yaml +421 -0
  213. package/lib/schema/type/credential-auth-templates.json +40 -0
  214. package/lib/schema/type/document-storage.json +226 -0
  215. package/lib/schema/type/message-service.json +123 -0
  216. package/lib/schema/type/vector-store.json +88 -0
  217. package/lib/utils/aifabrix-runtime-config-dir.js +132 -0
  218. package/lib/utils/api-error-handler.js +2 -2
  219. package/lib/utils/api.js +77 -17
  220. package/lib/utils/app-register-api.js +3 -2
  221. package/lib/utils/app-register-auth.js +1 -1
  222. package/lib/utils/app-register-config.js +4 -4
  223. package/lib/utils/app-register-display.js +3 -2
  224. package/lib/utils/app-register-validator.js +3 -2
  225. package/lib/utils/app-run-containers.js +26 -22
  226. package/lib/utils/app-scoped-config.js +31 -0
  227. package/lib/utils/app-service-env-from-builder.js +164 -0
  228. package/lib/utils/build-copy.js +1 -1
  229. package/lib/utils/build-helpers.js +20 -20
  230. package/lib/utils/build-resolve-image.js +165 -0
  231. package/lib/utils/cli-layout-chalk.js +8 -0
  232. package/lib/utils/cli-test-layout-chalk.js +267 -0
  233. package/lib/utils/cli-utils.js +88 -11
  234. package/lib/utils/compose-db-passwords.js +138 -0
  235. package/lib/utils/compose-generate-docker-compose.js +216 -0
  236. package/lib/utils/compose-generator.js +197 -291
  237. package/lib/utils/compose-miso-env.js +18 -0
  238. package/lib/utils/compose-traefik-ingress-base.js +158 -0
  239. package/lib/utils/config-paths.js +166 -7
  240. package/lib/utils/config-scoped-resources-preference.js +41 -0
  241. package/lib/utils/configuration-env-resolver.js +11 -8
  242. package/lib/utils/controller-deployment-outcome.js +68 -0
  243. package/lib/utils/credential-display.js +2 -2
  244. package/lib/utils/credential-secrets-env.js +5 -5
  245. package/lib/utils/dataplane-pipeline-warning.js +4 -3
  246. package/lib/utils/datasource-test-run-capability-scope.js +43 -0
  247. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  248. package/lib/utils/datasource-test-run-debug-display.js +137 -0
  249. package/lib/utils/datasource-test-run-debug-slice.js +93 -0
  250. package/lib/utils/datasource-test-run-display.js +459 -0
  251. package/lib/utils/datasource-test-run-exit.js +83 -0
  252. package/lib/utils/datasource-test-run-legacy-adapter.js +93 -0
  253. package/lib/utils/datasource-test-run-report-version.js +51 -0
  254. package/lib/utils/datasource-test-run-schema-sync.js +59 -0
  255. package/lib/utils/datasource-test-run-tty-log.js +81 -0
  256. package/lib/utils/datasource-validation-watch.js +266 -0
  257. package/lib/utils/declarative-url-ports.js +47 -0
  258. package/lib/utils/derive-env-key-from-client-id.js +41 -0
  259. package/lib/utils/dev-ca-install.js +185 -23
  260. package/lib/utils/dev-cert-helper.js +266 -17
  261. package/lib/utils/dev-hosts-helper.js +307 -0
  262. package/lib/utils/dev-init-cert-hints.js +37 -0
  263. package/lib/utils/dev-init-health-messages.js +52 -0
  264. package/lib/utils/dev-init-resolve.js +86 -0
  265. package/lib/utils/dev-init-ssh-merge.js +65 -0
  266. package/lib/utils/dev-ssh-config-helper.js +196 -0
  267. package/lib/utils/dev-user-groups.js +93 -0
  268. package/lib/utils/docker-build.js +42 -17
  269. package/lib/utils/docker-exec.js +28 -0
  270. package/lib/utils/docker-manifest-public-port.js +116 -0
  271. package/lib/utils/docker-not-running-hint.js +52 -0
  272. package/lib/utils/docker.js +98 -11
  273. package/lib/utils/ensure-dev-certs-for-remote-docker.js +192 -0
  274. package/lib/utils/env-config-loader.js +10 -91
  275. package/lib/utils/env-copy.js +19 -10
  276. package/lib/utils/env-map.js +35 -8
  277. package/lib/utils/env-template.js +2 -2
  278. package/lib/utils/environment-scoped-resources.js +144 -0
  279. package/lib/utils/error-formatter.js +92 -13
  280. package/lib/utils/error-formatters/http-status-errors.js +6 -5
  281. package/lib/utils/error-formatters/network-errors.js +2 -1
  282. package/lib/utils/error-formatters/permission-errors.js +2 -1
  283. package/lib/utils/error-formatters/validation-errors.js +2 -1
  284. package/lib/utils/external-readme.js +8 -1
  285. package/lib/utils/external-system-display.js +242 -136
  286. package/lib/utils/external-system-local-test-tty.js +389 -0
  287. package/lib/utils/external-system-readiness-core.js +377 -0
  288. package/lib/utils/external-system-readiness-deploy-display.js +270 -0
  289. package/lib/utils/external-system-readiness-display-internals.js +150 -0
  290. package/lib/utils/external-system-readiness-display.js +186 -0
  291. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  292. package/lib/utils/external-system-system-test-tty.js +417 -0
  293. package/lib/utils/external-system-test-helpers.js +24 -6
  294. package/lib/utils/external-system-validators.js +30 -12
  295. package/lib/utils/health-check-url.js +119 -0
  296. package/lib/utils/health-check.js +59 -25
  297. package/lib/utils/help-builder.js +11 -8
  298. package/lib/utils/image-version.js +4 -8
  299. package/lib/utils/infra-containers.js +4 -7
  300. package/lib/utils/infra-env-defaults.js +162 -0
  301. package/lib/utils/infra-status-display.js +167 -0
  302. package/lib/utils/infra-status.js +16 -8
  303. package/lib/utils/local-secrets.js +3 -4
  304. package/lib/utils/paths.js +148 -47
  305. package/lib/utils/port-resolver.js +10 -23
  306. package/lib/utils/redis-env-scope.js +62 -0
  307. package/lib/utils/register-aifabrix-shell-env.js +204 -0
  308. package/lib/utils/remote-builder-validation.js +99 -0
  309. package/lib/utils/remote-dev-auth.js +117 -21
  310. package/lib/utils/remote-docker-env.js +67 -15
  311. package/lib/utils/remote-secrets-loader.js +13 -4
  312. package/lib/utils/resolve-docker-image-ref.js +124 -0
  313. package/lib/utils/schema-loader.js +22 -9
  314. package/lib/utils/secrets-bash-kv.js +25 -0
  315. package/lib/utils/secrets-generator.js +169 -49
  316. package/lib/utils/secrets-helpers.js +70 -59
  317. package/lib/utils/secrets-kv-scope.js +60 -0
  318. package/lib/utils/secrets-utils.js +32 -38
  319. package/lib/utils/secrets-validation.js +3 -1
  320. package/lib/utils/secrets-yaml-preserve.js +109 -0
  321. package/lib/utils/ssh-key-helper.js +4 -2
  322. package/lib/utils/template-helpers.js +2 -2
  323. package/lib/utils/test-log-writer.js +3 -3
  324. package/lib/utils/token-manager.js +1 -2
  325. package/lib/utils/url-declarative-public-base.js +188 -0
  326. package/lib/utils/url-declarative-resolve-build.js +493 -0
  327. package/lib/utils/url-declarative-resolve-load-doc.js +51 -0
  328. package/lib/utils/url-declarative-resolve.js +220 -0
  329. package/lib/utils/url-declarative-token-parse.js +74 -0
  330. package/lib/utils/url-declarative-url-flags.js +50 -0
  331. package/lib/utils/url-declarative-vdir-inactive-env.js +99 -0
  332. package/lib/utils/url-public-path-prefix.js +34 -0
  333. package/lib/utils/urls-local-registry.js +220 -0
  334. package/lib/utils/validation-report-tty-kit.js +77 -0
  335. package/lib/utils/validation-run-poll.js +112 -0
  336. package/lib/utils/validation-run-post-retry.js +85 -0
  337. package/lib/utils/validation-run-request.js +116 -0
  338. package/lib/utils/variable-transformer.js +21 -4
  339. package/lib/utils/yaml-preserve.js +33 -14
  340. package/lib/validation/datasource-warnings.js +56 -0
  341. package/lib/validation/env-template-auth.js +1 -1
  342. package/lib/validation/external-manifest-validator.js +27 -7
  343. package/lib/validation/validate-display.js +37 -31
  344. package/lib/validation/validate-external-cert-sync.js +23 -0
  345. package/lib/validation/validate.js +8 -14
  346. package/lib/validation/validator-unresolved-placeholders.js +98 -0
  347. package/lib/validation/validator.js +22 -65
  348. package/lib/validation/wizard-config-validator.js +2 -1
  349. package/package.json +9 -4
  350. package/scripts/check-datasource-test-run-schema-sync.js +34 -0
  351. package/scripts/diagnose-cli.js +150 -0
  352. package/scripts/install-local.js +307 -55
  353. package/scripts/pnpm-global-remove.js +48 -0
  354. package/templates/README.md +15 -2
  355. package/templates/applications/dataplane/application.yaml +52 -2
  356. package/templates/applications/dataplane/env.template +79 -17
  357. package/templates/applications/dataplane/rbac.yaml +8 -0
  358. package/templates/applications/keycloak/application.yaml +9 -1
  359. package/templates/applications/keycloak/env.template +15 -6
  360. package/templates/applications/miso-controller/application.yaml +10 -2
  361. package/templates/applications/miso-controller/env.template +42 -12
  362. package/templates/applications/miso-controller/rbac.yaml +5 -0
  363. package/templates/external-system/README.md.hbs +20 -7
  364. package/templates/external-system/deploy.js.hbs +5 -5
  365. package/templates/external-system/external-datasource.yaml.hbs +197 -118
  366. package/templates/infra/compose.yaml.hbs +33 -16
  367. package/templates/infra/servers.json.hbs +3 -1
  368. package/templates/python/docker-compose.hbs +16 -0
  369. package/templates/typescript/docker-compose.hbs +16 -0
  370. package/lib/api/external-test.api.js +0 -111
  371. package/lib/schema/env-config.yaml +0 -60
@@ -22,60 +22,210 @@ const SSL_UNTRUSTED_CODES = [
22
22
  'CERT_UNTRUSTED',
23
23
  'SELF_SIGNED_CERT_IN_CHAIN',
24
24
  'UNABLE_TO_GET_ISSUER_CERT',
25
- 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
25
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
26
+ 'CERT_HAS_EXPIRED'
26
27
  ];
27
28
 
29
+ const SSL_HOSTNAME_MISMATCH_CODES = ['ERR_TLS_CERT_ALTNAME_INVALID'];
30
+
31
+ /**
32
+ * Walks err and nested err.cause (fetch wraps TLS failures).
33
+ * @param {Error|undefined|null} err - Error chain root
34
+ * @param {function(Error): void} fn - Visitor
35
+ * @param {number} [maxDepth=12]
36
+ */
37
+ function walkErrorCauseChain(err, fn, maxDepth = 12) {
38
+ let e = err;
39
+ let depth = 0;
40
+ while (e && depth < maxDepth) {
41
+ fn(e);
42
+ e = e.cause;
43
+ depth++;
44
+ }
45
+ }
46
+
28
47
  /**
29
48
  * Returns true if the error indicates an untrusted/self-signed server certificate.
30
49
  * @param {Error} err - Thrown error (e.g. from devApi.getHealth)
31
50
  * @returns {boolean}
32
51
  */
33
52
  function isSslUntrustedError(err) {
34
- const code = err?.code || err?.cause?.code;
35
- const msg = (err?.message || '').toUpperCase();
36
- return SSL_UNTRUSTED_CODES.some(c => code === c || msg.includes(c));
53
+ if (!err) return false;
54
+ let untrusted = false;
55
+ walkErrorCauseChain(err, (e) => {
56
+ const code = e && e.code;
57
+ const msg = ((e && e.message) || '').toUpperCase();
58
+ if (SSL_UNTRUSTED_CODES.some(c => code === c || msg.includes(c))) {
59
+ untrusted = true;
60
+ }
61
+ });
62
+ return untrusted;
37
63
  }
38
64
 
39
65
  /**
40
- * Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
41
- * Uses rejectUnauthorized: false only for this endpoint (dev setup).
42
- * @param {string} baseUrl - Builder Server base URL (no trailing slash)
43
- * @returns {Promise<Buffer>} CA certificate PEM
66
+ * True when the server cert is trusted enough to verify but the hostname does not match (wrong SAN).
67
+ * Installing the dev CA does not fix this.
68
+ * @param {Error} err - Thrown error
69
+ * @returns {boolean}
44
70
  */
45
- function fetchInstallCa(baseUrl) {
71
+ function isSslHostnameMismatchError(err) {
72
+ if (!err) return false;
73
+ let mismatch = false;
74
+ walkErrorCauseChain(err, (e) => {
75
+ const code = e && e.code;
76
+ if (SSL_HOSTNAME_MISMATCH_CODES.includes(code)) {
77
+ mismatch = true;
78
+ }
79
+ });
80
+ return mismatch;
81
+ }
82
+
83
+ const MAX_INSTALL_CA_REDIRECTS = 5;
84
+ const PEM_CERT_BLOCK_RE = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/;
85
+ const JSON_CA_KEYS = [
86
+ 'caCertificate', 'ca', 'certificate', 'pem', 'rootCa', 'rootCertificate', 'caPem', 'data'
87
+ ];
88
+
89
+ /**
90
+ * One-line preview of a response body for errors (no PEM secrets expected in typical error HTML).
91
+ * @param {string} body
92
+ * @param {number} maxLen
93
+ * @returns {string}
94
+ */
95
+ function truncateBodyForError(body, maxLen = 180) {
96
+ const oneLine = String(body || '').replace(/\s+/g, ' ').trim();
97
+ if (oneLine.length <= maxLen) return oneLine || '(empty)';
98
+ return `${oneLine.slice(0, maxLen)}…`;
99
+ }
100
+
101
+ /**
102
+ * @param {Object} j - Parsed JSON object
103
+ * @returns {string|null} PEM or null
104
+ */
105
+ function pemFromJsonObject(j) {
106
+ if (!j || typeof j !== 'object' || Array.isArray(j)) return null;
107
+ for (const k of JSON_CA_KEYS) {
108
+ if (typeof j[k] !== 'string') continue;
109
+ const m = j[k].match(PEM_CERT_BLOCK_RE);
110
+ if (m) return m[0].trim();
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Extract first PEM certificate from install-ca response: raw PEM, JSON field, or HTML/text wrapper.
117
+ * @param {string} raw - Response body as UTF-8
118
+ * @returns {string|null} PEM text or null
119
+ */
120
+ function extractCaPemFromBody(raw) {
121
+ if (!raw || typeof raw !== 'string') return null;
122
+ const s = raw.trim();
123
+ try {
124
+ const fromJson = pemFromJsonObject(JSON.parse(s));
125
+ if (fromJson) return fromJson;
126
+ } catch {
127
+ /* not JSON */
128
+ }
129
+ const block = s.match(PEM_CERT_BLOCK_RE);
130
+ return block ? block[0].trim() : null;
131
+ }
132
+
133
+ /**
134
+ * Resolve install-ca response body to PEM buffer or reject with context.
135
+ * @param {import('http').IncomingMessage} res
136
+ * @param {string} absoluteUrl
137
+ * @param {Buffer[]} chunks
138
+ * @param {function(Buffer):void} resolve
139
+ * @param {function(Error):void} reject
140
+ */
141
+ function finalizeInstallCaResponse(res, absoluteUrl, chunks, resolve, reject) {
142
+ const body = Buffer.concat(chunks).toString('utf8');
143
+ const status = res.statusCode || 0;
144
+ if (status < 200 || status >= 300) {
145
+ reject(
146
+ new Error(
147
+ `install-ca returned HTTP ${status}. ${truncateBodyForError(body)} — open ${absoluteUrl} in a browser or ask your admin to serve PEM at /install-ca.`
148
+ )
149
+ );
150
+ return;
151
+ }
152
+ const pem = extractCaPemFromBody(body);
153
+ if (!pem) {
154
+ const hint = body.trim().startsWith('<') ? 'Received HTML (SPA or error page). ' : '';
155
+ reject(
156
+ new Error(
157
+ `${hint}Invalid CA response: expected PEM certificate. ${truncateBodyForError(body)} — try: ${absoluteUrl}`
158
+ )
159
+ );
160
+ return;
161
+ }
162
+ resolve(Buffer.from(`${pem}\n`, 'utf8'));
163
+ }
164
+
165
+ /**
166
+ * GET absolute install-ca URL with insecure TLS; follow redirects (resolve relative Location).
167
+ * @param {string} absoluteUrl - Full https URL
168
+ * @param {number} redirectCount - Redirect depth
169
+ * @returns {Promise<Buffer>} CA PEM
170
+ */
171
+ function fetchInstallCaAbsolute(absoluteUrl, redirectCount) {
46
172
  return new Promise((resolve, reject) => {
47
- const url = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
48
- const urlObj = new URL(url);
173
+ if (redirectCount > MAX_INSTALL_CA_REDIRECTS) {
174
+ reject(new Error(`install-ca: too many redirects (max ${MAX_INSTALL_CA_REDIRECTS})`));
175
+ return;
176
+ }
177
+ const urlObj = new URL(absoluteUrl);
49
178
  if (urlObj.protocol !== 'https:') {
50
179
  reject(new Error('install-ca requires https URL'));
51
180
  return;
52
181
  }
53
182
  const agent = new https.Agent({ rejectUnauthorized: false });
54
183
  const req = https.get(
55
- url,
184
+ absoluteUrl,
56
185
  { agent },
57
186
  (res) => {
58
187
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
59
- req.destroy();
60
- fetchInstallCa(res.headers.location).then(resolve).catch(reject);
188
+ res.destroy();
189
+ let nextUrl;
190
+ try {
191
+ nextUrl = new URL(res.headers.location, absoluteUrl).href;
192
+ } catch {
193
+ reject(new Error(`install-ca: invalid redirect Location: ${res.headers.location}`));
194
+ return;
195
+ }
196
+ fetchInstallCaAbsolute(nextUrl, redirectCount + 1).then(resolve).catch(reject);
61
197
  return;
62
198
  }
199
+
63
200
  const chunks = [];
64
201
  res.on('data', c => chunks.push(c));
65
- res.on('end', () => {
66
- const body = Buffer.concat(chunks).toString('utf8');
67
- if (!body || !body.includes('-----BEGIN CERTIFICATE-----')) {
68
- reject(new Error('Invalid CA response: expected PEM certificate'));
69
- return;
70
- }
71
- resolve(Buffer.from(body, 'utf8'));
72
- });
202
+ res.on('end', () => finalizeInstallCaResponse(res, absoluteUrl, chunks, resolve, reject));
73
203
  }
74
204
  );
75
205
  req.on('error', reject);
76
206
  });
77
207
  }
78
208
 
209
+ /**
210
+ * Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
211
+ * Uses rejectUnauthorized: false only for this endpoint (dev setup).
212
+ * @param {string} baseUrl - Builder Server base URL (no trailing slash)
213
+ * @returns {Promise<Buffer>} CA certificate PEM
214
+ */
215
+ function fetchInstallCa(baseUrl) {
216
+ const url = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
217
+ let urlObj;
218
+ try {
219
+ urlObj = new URL(url);
220
+ } catch {
221
+ return Promise.reject(new Error('install-ca: invalid server URL'));
222
+ }
223
+ if (urlObj.protocol !== 'https:') {
224
+ return Promise.reject(new Error('install-ca requires https URL'));
225
+ }
226
+ return fetchInstallCaAbsolute(urlObj.href, 0);
227
+ }
228
+
79
229
  /**
80
230
  * Install CA PEM into OS trust store (platform-specific).
81
231
  * @param {Buffer|string} caPem - CA certificate PEM
@@ -131,9 +281,21 @@ function promptInstallCa() {
131
281
  });
132
282
  }
133
283
 
284
+ /**
285
+ * True when installCaPlatform failed because Linux needs root to update the system CA store.
286
+ * @param {unknown} err - Caught error
287
+ * @returns {boolean}
288
+ */
289
+ function isLinuxCaSudoRequiredError(err) {
290
+ const msg = err && typeof err.message === 'string' ? err.message : '';
291
+ return /Linux CA install requires sudo/i.test(msg);
292
+ }
293
+
134
294
  module.exports = {
135
295
  isSslUntrustedError,
296
+ isSslHostnameMismatchError,
136
297
  fetchInstallCa,
137
298
  installCaPlatform,
138
- promptInstallCa
299
+ promptInstallCa,
300
+ isLinuxCaSudoRequiredError
139
301
  };
@@ -6,9 +6,163 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- const { execSync } = require('child_process');
9
+ const { execFileSync } = require('child_process');
10
10
  const os = require('os');
11
11
 
12
+ /**
13
+ * @param {string} dir
14
+ * @returns {string|null}
15
+ */
16
+ function tryOpenSSLExe(dir) {
17
+ if (!dir) return null;
18
+ const trimmed = dir.trim();
19
+ if (!trimmed) return null;
20
+ const exe = path.join(trimmed, 'openssl.exe');
21
+ return fs.existsSync(exe) ? exe : null;
22
+ }
23
+
24
+ /**
25
+ * Git for Windows often adds ...\\Git\\cmd to PATH but not ...\\Git\\usr\\bin (so `where openssl` fails).
26
+ * @param {string} pathEnv
27
+ * @returns {string|null}
28
+ */
29
+ function findOpenSSLViaGitCmdOnPath(pathEnv) {
30
+ if (!pathEnv || typeof pathEnv !== 'string') return null;
31
+ const needle = `${path.sep}git${path.sep}cmd`.toLowerCase();
32
+ for (const segment of pathEnv.split(path.delimiter)) {
33
+ const normalized = path.normalize(segment.trim());
34
+ if (!normalized) continue;
35
+ const lower = normalized.toLowerCase();
36
+ const cmdIdx = lower.lastIndexOf(needle);
37
+ if (cmdIdx === -1) continue;
38
+ const gitRoot = normalized.slice(0, cmdIdx + `${path.sep}git`.length);
39
+ const fromUsrBin = tryOpenSSLExe(path.join(gitRoot, 'usr', 'bin'));
40
+ if (fromUsrBin) return fromUsrBin;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * First openssl.exe found in PATH directory entries.
47
+ * @param {string} pathEnv
48
+ * @returns {string|null}
49
+ */
50
+ function findOpenSSLInPathDirs(pathEnv) {
51
+ if (!pathEnv || typeof pathEnv !== 'string') return null;
52
+ for (const segment of pathEnv.split(path.delimiter)) {
53
+ const found = tryOpenSSLExe(segment);
54
+ if (found) return found;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Windows: Git\\cmd on PATH without usr\\bin, per-user Git, standard install dirs.
61
+ * @returns {string}
62
+ */
63
+ function resolveOpenSSLExecutableWin32() {
64
+ const pf = process.env.ProgramFiles || 'C:\\Program Files';
65
+ const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
66
+ const pf64 = process.env.ProgramW6432 || pf;
67
+ const localAppData = process.env.LOCALAPPDATA || '';
68
+ const pathEnv = process.env.Path || process.env.PATH || '';
69
+
70
+ const fromGitPath = findOpenSSLViaGitCmdOnPath(pathEnv);
71
+ if (fromGitPath) return fromGitPath;
72
+
73
+ const fromPathDirs = findOpenSSLInPathDirs(pathEnv);
74
+ if (fromPathDirs) return fromPathDirs;
75
+
76
+ const candidates = [
77
+ path.join(pf64, 'Git', 'usr', 'bin', 'openssl.exe'),
78
+ path.join(pf, 'Git', 'usr', 'bin', 'openssl.exe'),
79
+ path.join(pf86, 'Git', 'usr', 'bin', 'openssl.exe'),
80
+ localAppData ? path.join(localAppData, 'Programs', 'Git', 'usr', 'bin', 'openssl.exe') : '',
81
+ path.join(pf64, 'OpenSSL-Win64', 'bin', 'openssl.exe'),
82
+ path.join(pf, 'OpenSSL-Win64', 'bin', 'openssl.exe'),
83
+ path.join(pf64, 'OpenSSL', 'bin', 'openssl.exe'),
84
+ path.join(pf, 'OpenSSL', 'bin', 'openssl.exe')
85
+ ];
86
+ for (const c of candidates) {
87
+ if (c && fs.existsSync(c)) return c;
88
+ }
89
+ return 'openssl.exe';
90
+ }
91
+
92
+ /**
93
+ * Executable used for OpenSSL (PATH name or absolute path). Admin Windows shells often
94
+ * omit Git usr\\bin; probe standard install locations when plain "openssl" is unavailable.
95
+ * @returns {string}
96
+ */
97
+ function resolveOpenSSLExecutable() {
98
+ const fromEnv = process.env.AIFABRIX_OPENSSL;
99
+ if (fromEnv && typeof fromEnv === 'string' && fs.existsSync(fromEnv.trim())) {
100
+ return path.normalize(fromEnv.trim());
101
+ }
102
+ if (process.platform === 'win32') {
103
+ return resolveOpenSSLExecutableWin32();
104
+ }
105
+ return 'openssl';
106
+ }
107
+
108
+ /**
109
+ * Run OpenSSL with argv (no shell); works when openssl is only on disk, not on PATH.
110
+ * @param {string[]} args
111
+ * @param {Object} [opts] - Extra options for execFileSync (encoding utf8 applied by default)
112
+ * @returns {string|Buffer}
113
+ */
114
+ function runOpenSSL(args, opts) {
115
+ const exe = resolveOpenSSLExecutable();
116
+ return execFileSync(exe, args, {
117
+ stdio: ['pipe', 'pipe', 'pipe'],
118
+ encoding: 'utf8',
119
+ ...opts
120
+ });
121
+ }
122
+
123
+ /**
124
+ * OpenSSL argv for CSR generation (CN is typically dev-01, dev-02, …).
125
+ * @param {string} cn - Distinguished name CN value
126
+ * @param {string} keyPath - Output key path
127
+ * @param {string} csrPath - Output CSR path
128
+ * @returns {string[]}
129
+ */
130
+ function opensslCsrArgs(cn, keyPath, csrPath) {
131
+ return [
132
+ 'req',
133
+ '-new',
134
+ '-newkey',
135
+ 'rsa:2048',
136
+ '-keyout',
137
+ keyPath,
138
+ '-nodes',
139
+ '-subj',
140
+ `/CN=${cn}`,
141
+ '-out',
142
+ csrPath
143
+ ];
144
+ }
145
+
146
+ /**
147
+ * Map OpenSSL spawn/read failures to a clear Error for CSR generation.
148
+ * @param {Error & { code?: string }} err
149
+ * @returns {Error}
150
+ */
151
+ function csrGenerationError(err) {
152
+ const code = err && err.code;
153
+ const msg = err && err.message ? String(err.message) : '';
154
+ if (code === 'ENOENT') {
155
+ return new Error(
156
+ 'OpenSSL is required for certificate generation. Install OpenSSL or Git for Windows, add Git usr\\bin or openssl to PATH, or set AIFABRIX_OPENSSL to the full path of openssl.exe.'
157
+ );
158
+ }
159
+ const stderr = err && err.stderr ? String(err.stderr).trim() : '';
160
+ if (typeof err.status === 'number' && err.status !== 0 && stderr) {
161
+ return new Error(`OpenSSL failed: ${stderr}`);
162
+ }
163
+ return new Error(`CSR generation failed: ${msg}`);
164
+ }
165
+
12
166
  /**
13
167
  * Generate a key pair and CSR for developer certificate. Uses OpenSSL when available.
14
168
  * CN in CSR is set to dev-<developerId> per Builder Server convention.
@@ -26,20 +180,14 @@ function generateCSR(developerId) {
26
180
  const keyPath = path.join(tmpDir, 'key.pem');
27
181
  const csrPath = path.join(tmpDir, 'csr.pem');
28
182
  try {
29
- execSync(
30
- `openssl req -new -newkey rsa:2048 -keyout "${keyPath}" -nodes -subj "/CN=${cn}" -out "${csrPath}"`,
31
- { stdio: 'pipe', encoding: 'utf8' }
32
- );
183
+ try {
184
+ runOpenSSL(opensslCsrArgs(cn, keyPath, csrPath));
185
+ } catch (sslErr) {
186
+ throw csrGenerationError(sslErr);
187
+ }
33
188
  const keyPem = fs.readFileSync(keyPath, 'utf8');
34
189
  const csrPem = fs.readFileSync(csrPath, 'utf8');
35
190
  return { csrPem, keyPem };
36
- } catch (err) {
37
- if (err.message && (err.message.includes('openssl') || err.message.includes('ENOENT'))) {
38
- throw new Error(
39
- 'OpenSSL is required for certificate generation. Install OpenSSL and ensure it is on PATH, or use a system that provides it (e.g. Git for Windows).'
40
- );
41
- }
42
- throw new Error(`CSR generation failed: ${err.message}`);
43
191
  } finally {
44
192
  try {
45
193
  fs.unlinkSync(keyPath);
@@ -91,6 +239,22 @@ function readClientKeyPem(certDir) {
91
239
  }
92
240
  }
93
241
 
242
+ /**
243
+ * Read Builder Server root CA PEM (ca.pem) for TLS verification in Node.
244
+ * Node does not use the OS trust store the same way as browsers; this file is used with tls.rootCertificates.
245
+ * @param {string} certDir - Directory containing ca.pem
246
+ * @returns {string|null} PEM content or null if not found
247
+ */
248
+ function readServerCaPem(certDir) {
249
+ const caPath = path.join(certDir, 'ca.pem');
250
+ try {
251
+ return fs.readFileSync(caPath, 'utf8');
252
+ } catch (e) {
253
+ if (e.code === 'ENOENT') return null;
254
+ throw e;
255
+ }
256
+ }
257
+
94
258
  /**
95
259
  * Get certificate validity end (notAfter) from cert.pem in certDir using OpenSSL.
96
260
  * @param {string} certDir - Directory containing cert.pem
@@ -100,10 +264,7 @@ function getCertValidNotAfter(certDir) {
100
264
  const certPath = path.join(certDir, 'cert.pem');
101
265
  try {
102
266
  if (!fs.existsSync(certPath)) return null;
103
- const out = execSync(
104
- `openssl x509 -enddate -noout -in "${certPath}"`,
105
- { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
106
- );
267
+ const out = runOpenSSL(['x509', '-enddate', '-noout', '-in', certPath], { encoding: 'utf8' });
107
268
  const match = out.match(/notAfter=(.+)/);
108
269
  if (!match) return null;
109
270
  const date = new Date(match[1].trim());
@@ -113,10 +274,98 @@ function getCertValidNotAfter(certDir) {
113
274
  }
114
275
  }
115
276
 
277
+ /**
278
+ * Parse developer-id digits from OpenSSL `x509 -subject -noout` output (subject=RDN… line).
279
+ * Expects CN in the form dev-<digits> per Builder client-cert convention.
280
+ *
281
+ * @param {string} subjectOutput - stdout from openssl x509 -subject
282
+ * @returns {string|null} Developer id as in the CN (e.g. "01"), or null if missing/invalid
283
+ */
284
+ function parseDeveloperIdFromX509SubjectOutput(subjectOutput) {
285
+ if (!subjectOutput || typeof subjectOutput !== 'string') return null;
286
+ const upper = subjectOutput.replace(/\r\n/g, '\n');
287
+ const cnMatch = upper.match(/CN\s*=\s*dev-([0-9]+)/i);
288
+ if (!cnMatch) return null;
289
+ return cnMatch[1];
290
+ }
291
+
292
+ /**
293
+ * Whether two developer-id strings refer to the same non-negative integer (e.g. "2" vs "02").
294
+ * @param {string} configId - Id from config
295
+ * @param {string} certIdDigits - Id digits from certificate CN
296
+ * @returns {boolean}
297
+ */
298
+ function developerIdsMatchNumeric(configId, certIdDigits) {
299
+ if (
300
+ configId === null ||
301
+ configId === undefined ||
302
+ certIdDigits === null ||
303
+ certIdDigits === undefined
304
+ ) {
305
+ return false;
306
+ }
307
+ const a = String(configId).trim();
308
+ const b = String(certIdDigits).trim();
309
+ if (!/^[0-9]+$/.test(a) || !/^[0-9]+$/.test(b)) return false;
310
+ return parseInt(a, 10) === parseInt(b, 10);
311
+ }
312
+
313
+ /**
314
+ * Read client certificate subject and return developer-id digits from CN=dev-<id>.
315
+ * @param {string} certDir - Directory containing cert.pem
316
+ * @returns {string|null} Id string from CN or null if cert missing/unreadable or CN invalid
317
+ */
318
+ function getCertSubjectDeveloperId(certDir) {
319
+ const certPath = path.join(certDir, 'cert.pem');
320
+ try {
321
+ if (!certDir || typeof certDir !== 'string') return null;
322
+ if (!fs.existsSync(certPath)) return null;
323
+ const out = runOpenSSL(['x509', '-subject', '-noout', '-in', certPath], { encoding: 'utf8' });
324
+ return parseDeveloperIdFromX509SubjectOutput(out);
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Normalize PEM: JSON-escaped \\n to real newlines (Docker/OpenSSL).
332
+ * @param {string} pem
333
+ * @returns {string}
334
+ */
335
+ function normalizePemNewlines(pem) {
336
+ if (typeof pem !== 'string') return pem;
337
+ return pem.replace(/\\n/g, '\n');
338
+ }
339
+
340
+ /**
341
+ * Distinct PEM blocks joined for ca.pem (HTTPS root + issue-cert CA, deduped).
342
+ * @param {...(string|null|undefined)} pems
343
+ * @returns {string|null}
344
+ */
345
+ function mergeCaPemBlocks(...pems) {
346
+ const blocks = [];
347
+ const seen = new Set();
348
+ for (const p of pems) {
349
+ if (!p || typeof p !== 'string') continue;
350
+ const normalized = normalizePemNewlines(p.trim());
351
+ if (!normalized) continue;
352
+ if (seen.has(normalized)) continue;
353
+ seen.add(normalized);
354
+ blocks.push(normalized);
355
+ }
356
+ return blocks.length ? blocks.join('\n\n') : null;
357
+ }
358
+
116
359
  module.exports = {
117
360
  generateCSR,
118
361
  getCertDir,
119
362
  readClientCertPem,
120
363
  readClientKeyPem,
121
- getCertValidNotAfter
364
+ readServerCaPem,
365
+ getCertValidNotAfter,
366
+ getCertSubjectDeveloperId,
367
+ parseDeveloperIdFromX509SubjectOutput,
368
+ developerIdsMatchNumeric,
369
+ normalizePemNewlines,
370
+ mergeCaPemBlocks
122
371
  };