@aifabrix/builder 2.42.1 → 2.44.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 (392) hide show
  1. package/.cursor/rules/anchor-docs.mdc +15 -0
  2. package/README.md +2 -2
  3. package/anchor-docs/README.md +10 -0
  4. package/anchor-docs/_TEMPLATE +24 -0
  5. package/bin/aifabrix.js +13 -4
  6. package/integration/hubspot-test/README.md +157 -0
  7. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  8. package/integration/{hubspot → hubspot-test}/create-hubspot.js +10 -10
  9. package/integration/hubspot-test/env.template +4 -0
  10. package/integration/hubspot-test/hubspot-test-datasource-company.json +138 -0
  11. package/integration/hubspot-test/hubspot-test-datasource-contact.json +146 -0
  12. package/integration/hubspot-test/hubspot-test-datasource-deal.json +146 -0
  13. package/integration/hubspot-test/hubspot-test-datasource-users.json +76 -0
  14. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +201 -24
  15. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  16. package/integration/hubspot-test/rbac.json +166 -0
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  20. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  23. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  26. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  27. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  29. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  30. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  31. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  32. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  33. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  34. package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +1 -7
  35. package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +3 -3
  36. package/integration/{hubspot → hubspot-test}/test.js +137 -102
  37. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  38. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  39. package/integration/hubspot-test/wizard-hubspot-test-headless.yaml +23 -0
  40. package/integration/roundtrip-test-local/README.md +144 -0
  41. package/integration/roundtrip-test-local/application.yaml +13 -0
  42. package/integration/roundtrip-test-local/env.template +15 -0
  43. package/integration/roundtrip-test-local/roundtrip-test-local-datasource-roundtrip-test-company.yaml +14 -0
  44. package/integration/roundtrip-test-local/roundtrip-test-local-deploy.json +61 -0
  45. package/integration/roundtrip-test-local/roundtrip-test-local-system.yaml +25 -0
  46. package/integration/roundtrip-test-local2/README.md +144 -0
  47. package/integration/roundtrip-test-local2/application.yaml +13 -0
  48. package/integration/roundtrip-test-local2/env.template +15 -0
  49. package/integration/roundtrip-test-local2/roundtrip-test-local2-datasource-company.yaml +31 -0
  50. package/integration/roundtrip-test-local2/roundtrip-test-local2-deploy.json +86 -0
  51. package/integration/roundtrip-test-local2/roundtrip-test-local2-system.yaml +25 -0
  52. package/integration/test/wizard.yaml +8 -0
  53. package/jest.config.default.js +10 -0
  54. package/jest.config.integration.fixtures.js +22 -0
  55. package/jest.config.integration.js +21 -18
  56. package/jest.config.isolated.js +10 -0
  57. package/jest.projects.js +288 -0
  58. package/lib/api/datasources-core.api.js +3 -3
  59. package/lib/api/dev-mtls-request.js +110 -0
  60. package/lib/api/dev-server-https.js +145 -0
  61. package/lib/api/dev.api.js +133 -144
  62. package/lib/api/index.js +0 -1
  63. package/lib/api/pipeline.api.js +67 -20
  64. package/lib/api/service-users.api.js +111 -2
  65. package/lib/api/types/dev.types.js +4 -3
  66. package/lib/api/types/pipeline.types.js +8 -5
  67. package/lib/api/types/service-users.types.js +41 -0
  68. package/lib/api/types/validation-run.types.js +56 -0
  69. package/lib/api/validation-run.api.js +99 -0
  70. package/lib/api/validation-runner.js +99 -0
  71. package/lib/app/config.js +1 -1
  72. package/lib/app/deploy-status-display.js +2 -2
  73. package/lib/app/deploy.js +7 -6
  74. package/lib/app/display.js +2 -1
  75. package/lib/app/dockerfile.js +3 -2
  76. package/lib/app/down.js +2 -1
  77. package/lib/app/helpers.js +6 -5
  78. package/lib/app/index.js +27 -8
  79. package/lib/app/list.js +7 -6
  80. package/lib/app/push.js +4 -3
  81. package/lib/app/register.js +19 -8
  82. package/lib/app/rotate-secret.js +17 -13
  83. package/lib/app/run-container-start.js +184 -0
  84. package/lib/app/run-docker-fallback.js +108 -0
  85. package/lib/app/run-env-compose.js +30 -42
  86. package/lib/app/run-helpers.js +49 -126
  87. package/lib/app/run-infra-requirements.js +30 -0
  88. package/lib/app/run-resolve-image.js +21 -0
  89. package/lib/app/run.js +74 -21
  90. package/lib/app/show-display.js +1 -1
  91. package/lib/app/show.js +1 -1
  92. package/lib/build/index.js +13 -10
  93. package/lib/cli/index.js +2 -0
  94. package/lib/cli/setup-app.help.js +67 -0
  95. package/lib/cli/setup-app.js +59 -123
  96. package/lib/cli/setup-app.test-commands.js +179 -0
  97. package/lib/cli/setup-auth.js +36 -14
  98. package/lib/cli/setup-credential-deployment.js +22 -8
  99. package/lib/cli/setup-dev-path-commands.js +124 -0
  100. package/lib/cli/setup-dev.js +190 -103
  101. package/lib/cli/setup-environment.js +11 -20
  102. package/lib/cli/setup-external-system.js +62 -22
  103. package/lib/cli/setup-infra.js +139 -47
  104. package/lib/cli/setup-parameters.js +32 -0
  105. package/lib/cli/setup-secrets.js +147 -10
  106. package/lib/cli/setup-service-user.js +146 -20
  107. package/lib/cli/setup-utility.js +47 -19
  108. package/lib/commands/app-down.js +5 -7
  109. package/lib/commands/app-install.js +14 -7
  110. package/lib/commands/app-logs.js +13 -10
  111. package/lib/commands/app-shell.js +4 -1
  112. package/lib/commands/app-test.js +25 -19
  113. package/lib/commands/app.js +22 -10
  114. package/lib/commands/auth-config.js +10 -14
  115. package/lib/commands/auth-status.js +4 -3
  116. package/lib/commands/credential-env.js +4 -3
  117. package/lib/commands/credential-list.js +5 -4
  118. package/lib/commands/credential-push.js +4 -3
  119. package/lib/commands/datasource-unified-test-cli.js +495 -0
  120. package/lib/commands/datasource-unified-test-cli.options.js +149 -0
  121. package/lib/commands/datasource-validation-cli.js +129 -0
  122. package/lib/commands/datasource.js +123 -71
  123. package/lib/commands/deployment-list.js +6 -5
  124. package/lib/commands/dev-cli-handlers.js +122 -18
  125. package/lib/commands/dev-down.js +4 -3
  126. package/lib/commands/dev-init.js +231 -116
  127. package/lib/commands/dev-show-display.js +473 -0
  128. package/lib/commands/login-credentials.js +3 -2
  129. package/lib/commands/login-device.js +4 -3
  130. package/lib/commands/login.js +5 -4
  131. package/lib/commands/logout.js +8 -7
  132. package/lib/commands/parameters-validate.js +54 -0
  133. package/lib/commands/repair-datasource.js +314 -68
  134. package/lib/commands/repair-env-template.js +16 -10
  135. package/lib/commands/repair-rbac.js +25 -19
  136. package/lib/commands/repair.js +116 -32
  137. package/lib/commands/secrets-list.js +23 -12
  138. package/lib/commands/secrets-remove-all.js +220 -0
  139. package/lib/commands/secrets-remove.js +22 -13
  140. package/lib/commands/secrets-set.js +21 -12
  141. package/lib/commands/secrets-validate.js +20 -7
  142. package/lib/commands/secure.js +10 -9
  143. package/lib/commands/service-user.js +243 -13
  144. package/lib/commands/test-e2e-external.js +27 -1
  145. package/lib/commands/up-common.js +28 -2
  146. package/lib/commands/up-dataplane.js +31 -18
  147. package/lib/commands/up-miso.js +19 -29
  148. package/lib/commands/upload.js +138 -39
  149. package/lib/commands/wizard-core-helpers.js +1 -1
  150. package/lib/commands/wizard-dataplane.js +4 -3
  151. package/lib/commands/wizard-helpers.js +3 -3
  152. package/lib/commands/wizard.js +2 -2
  153. package/lib/core/admin-secrets.js +16 -5
  154. package/lib/core/audit-logger.js +12 -4
  155. package/lib/core/config-attach-extensions.js +46 -0
  156. package/lib/core/config-runtime-paths.js +29 -0
  157. package/lib/core/config.js +59 -58
  158. package/lib/core/diff.js +3 -2
  159. package/lib/core/ensure-encryption-key.js +2 -4
  160. package/lib/core/secrets-ensure-infra.js +77 -0
  161. package/lib/core/secrets-ensure.js +120 -64
  162. package/lib/core/secrets-env-write.js +35 -7
  163. package/lib/core/secrets-infra-placeholder-sync.js +61 -0
  164. package/lib/core/secrets.js +228 -42
  165. package/lib/core/templates-env.js +4 -3
  166. package/lib/core/templates.js +1 -1
  167. package/lib/datasource/abac-validator.js +148 -0
  168. package/lib/datasource/deploy.js +75 -53
  169. package/lib/datasource/field-reference-validator.js +77 -36
  170. package/lib/datasource/integration-context.js +63 -0
  171. package/lib/datasource/list.js +8 -7
  172. package/lib/datasource/log-viewer.js +252 -0
  173. package/lib/datasource/resolve-app.js +109 -0
  174. package/lib/datasource/test-e2e.js +95 -155
  175. package/lib/datasource/test-integration.js +121 -109
  176. package/lib/datasource/unified-validation-run-body.js +65 -0
  177. package/lib/datasource/unified-validation-run-post.js +23 -0
  178. package/lib/datasource/unified-validation-run-resolve.js +43 -0
  179. package/lib/datasource/unified-validation-run.js +92 -0
  180. package/lib/datasource/validate.js +162 -15
  181. package/lib/deployment/deployer.js +4 -3
  182. package/lib/deployment/environment.js +7 -6
  183. package/lib/deployment/push.js +17 -8
  184. package/lib/external-system/delete.js +4 -3
  185. package/lib/external-system/deploy.js +131 -53
  186. package/lib/external-system/download-helpers.js +1 -1
  187. package/lib/external-system/download.js +7 -6
  188. package/lib/external-system/generator.js +104 -14
  189. package/lib/external-system/integration-test-dispatch.js +26 -0
  190. package/lib/external-system/test-execution.js +5 -1
  191. package/lib/external-system/test-helpers.js +0 -4
  192. package/lib/external-system/test-system-level-helpers.js +110 -0
  193. package/lib/external-system/test-system-level.js +83 -44
  194. package/lib/external-system/test.js +59 -8
  195. package/lib/generator/builders.js +23 -11
  196. package/lib/generator/deploy-manifest-azure-kv.js +81 -0
  197. package/lib/generator/external-controller-manifest.js +3 -3
  198. package/lib/generator/external.js +23 -11
  199. package/lib/generator/helpers.js +71 -12
  200. package/lib/generator/index.js +8 -4
  201. package/lib/generator/split-readme.js +12 -7
  202. package/lib/generator/split-variables.js +2 -1
  203. package/lib/generator/split.js +46 -11
  204. package/lib/generator/wizard-readme.js +3 -3
  205. package/lib/generator/wizard.js +16 -13
  206. package/lib/infrastructure/compose.js +60 -6
  207. package/lib/infrastructure/helpers.js +238 -51
  208. package/lib/infrastructure/index.js +64 -37
  209. package/lib/infrastructure/services.js +21 -15
  210. package/lib/internal/fs-real-sync.js +104 -0
  211. package/lib/internal/node-fs.js +98 -0
  212. package/lib/parameters/database-secret-values.js +173 -0
  213. package/lib/parameters/infra-kv-discovery.js +121 -0
  214. package/lib/parameters/infra-parameter-catalog.js +458 -0
  215. package/lib/parameters/infra-parameter-validate.js +64 -0
  216. package/lib/schema/application-schema.json +37 -17
  217. package/lib/schema/datasource-test-run.schema.json +493 -0
  218. package/lib/schema/deployment-rules.yaml +102 -63
  219. package/lib/schema/external-datasource.schema.json +1201 -433
  220. package/lib/schema/external-system.schema.json +181 -5
  221. package/lib/schema/flag-map-validation-run.json +31 -0
  222. package/lib/schema/infra-parameter.schema.json +106 -0
  223. package/lib/schema/infra.parameter.yaml +421 -0
  224. package/lib/schema/type/credential-auth-templates.json +40 -0
  225. package/lib/schema/type/document-storage.json +213 -0
  226. package/lib/schema/type/message-service.json +123 -0
  227. package/lib/schema/type/vector-store.json +88 -0
  228. package/lib/utils/aifabrix-runtime-config-dir.js +132 -0
  229. package/lib/utils/api-error-handler.js +2 -2
  230. package/lib/utils/api.js +49 -14
  231. package/lib/utils/app-config-resolver.js +23 -1
  232. package/lib/utils/app-register-api.js +3 -2
  233. package/lib/utils/app-register-auth.js +1 -1
  234. package/lib/utils/app-register-config.js +4 -4
  235. package/lib/utils/app-register-display.js +3 -2
  236. package/lib/utils/app-register-validator.js +3 -2
  237. package/lib/utils/app-run-containers.js +26 -22
  238. package/lib/utils/app-scoped-config.js +31 -0
  239. package/lib/utils/app-service-env-from-builder.js +164 -0
  240. package/lib/utils/build-copy.js +1 -1
  241. package/lib/utils/build-helpers.js +20 -20
  242. package/lib/utils/build-resolve-image.js +165 -0
  243. package/lib/utils/cli-layout-chalk.js +8 -0
  244. package/lib/utils/cli-test-layout-chalk.js +267 -0
  245. package/lib/utils/cli-utils.js +88 -11
  246. package/lib/utils/compose-db-passwords.js +138 -0
  247. package/lib/utils/compose-generate-docker-compose.js +216 -0
  248. package/lib/utils/compose-generator.js +197 -291
  249. package/lib/utils/compose-miso-env.js +18 -0
  250. package/lib/utils/compose-traefik-ingress-base.js +158 -0
  251. package/lib/utils/config-paths.js +209 -6
  252. package/lib/utils/config-scoped-resources-preference.js +41 -0
  253. package/lib/utils/controller-deployment-outcome.js +68 -0
  254. package/lib/utils/credential-display.js +2 -2
  255. package/lib/utils/credential-secrets-env.js +16 -1
  256. package/lib/utils/dataplane-pipeline-warning.js +4 -3
  257. package/lib/utils/datasource-test-run-capability-scope.js +43 -0
  258. package/lib/utils/datasource-test-run-debug-display.js +137 -0
  259. package/lib/utils/datasource-test-run-debug-slice.js +93 -0
  260. package/lib/utils/datasource-test-run-display.js +442 -0
  261. package/lib/utils/datasource-test-run-exit.js +58 -0
  262. package/lib/utils/datasource-test-run-legacy-adapter.js +93 -0
  263. package/lib/utils/datasource-test-run-report-version.js +51 -0
  264. package/lib/utils/datasource-test-run-schema-sync.js +59 -0
  265. package/lib/utils/datasource-test-run-tty-log.js +81 -0
  266. package/lib/utils/datasource-validation-watch.js +266 -0
  267. package/lib/utils/declarative-url-ports.js +47 -0
  268. package/lib/utils/derive-env-key-from-client-id.js +41 -0
  269. package/lib/utils/dev-ca-install.js +185 -23
  270. package/lib/utils/dev-cert-helper.js +266 -17
  271. package/lib/utils/dev-hosts-helper.js +307 -0
  272. package/lib/utils/dev-init-cert-hints.js +37 -0
  273. package/lib/utils/dev-init-health-messages.js +52 -0
  274. package/lib/utils/dev-init-resolve.js +86 -0
  275. package/lib/utils/dev-init-ssh-merge.js +65 -0
  276. package/lib/utils/dev-ssh-config-helper.js +196 -0
  277. package/lib/utils/dev-user-groups.js +93 -0
  278. package/lib/utils/docker-build.js +42 -17
  279. package/lib/utils/docker-exec.js +28 -0
  280. package/lib/utils/docker-manifest-public-port.js +116 -0
  281. package/lib/utils/docker-not-running-hint.js +52 -0
  282. package/lib/utils/docker.js +98 -11
  283. package/lib/utils/ensure-dev-certs-for-remote-docker.js +192 -0
  284. package/lib/utils/env-config-loader.js +10 -91
  285. package/lib/utils/env-copy.js +19 -10
  286. package/lib/utils/env-map.js +42 -11
  287. package/lib/utils/env-template.js +2 -2
  288. package/lib/utils/environment-scoped-resources.js +144 -0
  289. package/lib/utils/error-formatter.js +125 -9
  290. package/lib/utils/error-formatters/http-status-errors.js +6 -5
  291. package/lib/utils/error-formatters/network-errors.js +2 -1
  292. package/lib/utils/error-formatters/permission-errors.js +2 -1
  293. package/lib/utils/error-formatters/validation-errors.js +2 -1
  294. package/lib/utils/external-env-template.js +180 -0
  295. package/lib/utils/external-readme.js +8 -1
  296. package/lib/utils/external-system-display.js +277 -136
  297. package/lib/utils/external-system-local-test-tty.js +389 -0
  298. package/lib/utils/external-system-readiness-core.js +377 -0
  299. package/lib/utils/external-system-readiness-deploy-display.js +270 -0
  300. package/lib/utils/external-system-readiness-display-internals.js +150 -0
  301. package/lib/utils/external-system-readiness-display.js +186 -0
  302. package/lib/utils/external-system-test-helpers.js +24 -6
  303. package/lib/utils/external-system-validators.js +32 -14
  304. package/lib/utils/health-check-url.js +119 -0
  305. package/lib/utils/health-check.js +59 -25
  306. package/lib/utils/help-builder.js +14 -13
  307. package/lib/utils/image-version.js +4 -8
  308. package/lib/utils/infra-containers.js +4 -7
  309. package/lib/utils/infra-env-defaults.js +162 -0
  310. package/lib/utils/infra-status-display.js +167 -0
  311. package/lib/utils/infra-status.js +16 -8
  312. package/lib/utils/local-secrets.js +29 -7
  313. package/lib/utils/paths.js +136 -48
  314. package/lib/utils/port-resolver.js +10 -23
  315. package/lib/utils/redis-env-scope.js +62 -0
  316. package/lib/utils/register-aifabrix-shell-env.js +204 -0
  317. package/lib/utils/remote-builder-validation.js +99 -0
  318. package/lib/utils/remote-dev-auth.js +117 -21
  319. package/lib/utils/remote-docker-env.js +67 -15
  320. package/lib/utils/remote-secrets-loader.js +13 -4
  321. package/lib/utils/resolve-docker-image-ref.js +124 -0
  322. package/lib/utils/schema-loader.js +22 -9
  323. package/lib/utils/secrets-bash-kv.js +25 -0
  324. package/lib/utils/secrets-generator.js +171 -51
  325. package/lib/utils/secrets-helpers.js +70 -59
  326. package/lib/utils/secrets-kv-scope.js +60 -0
  327. package/lib/utils/secrets-utils.js +35 -37
  328. package/lib/utils/secrets-validation.js +3 -1
  329. package/lib/utils/secrets-yaml-preserve.js +109 -0
  330. package/lib/utils/secure-file-permissions.js +91 -0
  331. package/lib/utils/ssh-key-helper.js +4 -2
  332. package/lib/utils/template-helpers.js +2 -2
  333. package/lib/utils/test-log-writer.js +3 -3
  334. package/lib/utils/token-manager.js +37 -5
  335. package/lib/utils/url-declarative-public-base.js +188 -0
  336. package/lib/utils/url-declarative-resolve-build.js +493 -0
  337. package/lib/utils/url-declarative-resolve-load-doc.js +51 -0
  338. package/lib/utils/url-declarative-resolve.js +220 -0
  339. package/lib/utils/url-declarative-token-parse.js +74 -0
  340. package/lib/utils/url-declarative-url-flags.js +50 -0
  341. package/lib/utils/url-declarative-vdir-inactive-env.js +99 -0
  342. package/lib/utils/url-public-path-prefix.js +34 -0
  343. package/lib/utils/urls-local-registry.js +220 -0
  344. package/lib/utils/validation-report-tty-kit.js +77 -0
  345. package/lib/utils/validation-run-poll.js +89 -0
  346. package/lib/utils/validation-run-post-retry.js +73 -0
  347. package/lib/utils/validation-run-request.js +98 -0
  348. package/lib/utils/variable-transformer.js +21 -4
  349. package/lib/utils/yaml-preserve.js +78 -1
  350. package/lib/validation/datasource-warnings.js +56 -0
  351. package/lib/validation/env-template-auth.js +50 -2
  352. package/lib/validation/external-manifest-validator.js +35 -7
  353. package/lib/validation/validate-display.js +37 -31
  354. package/lib/validation/validate.js +9 -10
  355. package/lib/validation/validator-unresolved-placeholders.js +98 -0
  356. package/lib/validation/validator.js +32 -78
  357. package/lib/validation/wizard-config-validator.js +2 -1
  358. package/package.json +11 -3
  359. package/scripts/check-datasource-test-run-schema-sync.js +34 -0
  360. package/scripts/diagnose-cli.js +150 -0
  361. package/scripts/install-local.js +304 -55
  362. package/templates/README.md +15 -2
  363. package/templates/applications/dataplane/application.yaml +52 -2
  364. package/templates/applications/dataplane/env.template +80 -18
  365. package/templates/applications/dataplane/rbac.yaml +8 -0
  366. package/templates/applications/keycloak/application.yaml +9 -1
  367. package/templates/applications/keycloak/env.template +15 -6
  368. package/templates/applications/miso-controller/application.yaml +10 -2
  369. package/templates/applications/miso-controller/env.template +55 -14
  370. package/templates/applications/miso-controller/rbac.yaml +5 -0
  371. package/templates/external-system/README.md.hbs +20 -7
  372. package/templates/external-system/deploy.js.hbs +5 -5
  373. package/templates/external-system/env.template.hbs +22 -0
  374. package/templates/external-system/external-datasource.yaml.hbs +197 -118
  375. package/templates/infra/compose.yaml.hbs +20 -4
  376. package/templates/python/docker-compose.hbs +16 -0
  377. package/templates/typescript/docker-compose.hbs +16 -0
  378. package/integration/hubspot/README.md +0 -102
  379. package/integration/hubspot/env.template +0 -4
  380. package/integration/hubspot/hubspot-datasource-company.json +0 -541
  381. package/integration/hubspot/hubspot-datasource-contact.json +0 -639
  382. package/integration/hubspot/hubspot-datasource-deal.json +0 -588
  383. package/integration/hubspot/hubspot-datasource-users.json +0 -116
  384. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  385. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  386. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  387. package/lib/api/external-test.api.js +0 -111
  388. package/lib/schema/env-config.yaml +0 -43
  389. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  390. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  391. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  392. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
@@ -14,29 +14,23 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const logger = require('./logger');
16
16
  const pathsUtil = require('./paths');
17
+ const { ensureSecureFilePermissions } = require('./secure-file-permissions');
17
18
  const { getContainerPort } = require('./port-resolver');
18
19
  const { loadYamlTolerantOfDuplicateKeys } = require('./secrets-generator');
19
20
 
20
21
  /**
21
- * Parses secrets YAML content with fallback for duplicate keys.
22
+ * Parses secrets YAML content with fallback for duplicate keys and broken concat files
23
+ * (e.g. empty map `{}` then appended key lines — invalid without `---`).
22
24
  * @param {string} content - Raw file content
23
25
  * @returns {Object} Parsed secrets object
24
26
  */
25
27
  function parseSecretsContent(content) {
26
- try {
27
- return yaml.load(content);
28
- } catch (yamlErr) {
29
- const msg = yamlErr.message || '';
30
- if (msg.includes('duplicate') || msg.includes('duplicated mapping')) {
31
- return loadYamlTolerantOfDuplicateKeys(content);
32
- }
33
- throw yamlErr;
34
- }
28
+ return loadYamlTolerantOfDuplicateKeys(content);
35
29
  }
36
30
 
37
31
  /**
38
32
  * Loads secrets from file with cascading lookup support
39
- * First checks ~/.aifabrix/secrets.local.yaml, then aifabrix-secrets from config.yaml
33
+ * First checks the primary user secrets file (see getPrimaryUserSecretsLocalPath in paths.js), then aifabrix-secrets from config.yaml
40
34
  *
41
35
  * @async
42
36
  * @function loadSecretsFromFile
@@ -50,7 +44,7 @@ async function loadSecretsFromFile(filePath) {
50
44
 
51
45
  try {
52
46
  const content = fs.readFileSync(filePath, 'utf8');
53
- const secrets = yaml.load(content);
47
+ const secrets = parseSecretsContent(content);
54
48
 
55
49
  if (!secrets || typeof secrets !== 'object') {
56
50
  return {};
@@ -64,20 +58,19 @@ async function loadSecretsFromFile(filePath) {
64
58
  }
65
59
 
66
60
  /**
67
- * Loads user secrets from the primary config directory (AIFABRIX_HOME or ~/.aifabrix).
61
+ * Loads user secrets from getPrimaryUserSecretsLocalPath() (same dir as config.yaml resolution).
68
62
  * Used as the master source when merging with project/public secrets: user values win,
69
63
  * missing keys are filled from the public (aifabrix-secrets) file.
70
- * Does not use config.yaml aifabrix-home so the merge always sees the actual user file.
71
64
  *
72
65
  * @function loadPrimaryUserSecrets
73
66
  * @returns {Object} Loaded secrets object or empty object
74
67
  */
75
68
  function loadPrimaryUserSecrets() {
76
- const primaryDir = pathsUtil.getConfigDirForPaths();
77
- const userSecretsPath = path.join(primaryDir, 'secrets.local.yaml');
69
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
78
70
  if (!fs.existsSync(userSecretsPath)) {
79
71
  return {};
80
72
  }
73
+ ensureSecureFilePermissions(userSecretsPath);
81
74
 
82
75
  try {
83
76
  const content = fs.readFileSync(userSecretsPath, 'utf8');
@@ -96,31 +89,12 @@ function loadPrimaryUserSecrets() {
96
89
  }
97
90
 
98
91
  /**
99
- * Loads user secrets from ~/.aifabrix/secrets.local.yaml
100
- * Uses paths.getAifabrixHome() to respect config.yaml aifabrix-home override
92
+ * Same as {@link loadPrimaryUserSecrets} (legacy name; kept for callers).
101
93
  * @function loadUserSecrets
102
94
  * @returns {Object} Loaded secrets object or empty object
103
95
  */
104
96
  function loadUserSecrets() {
105
- const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
106
- if (!fs.existsSync(userSecretsPath)) {
107
- return {};
108
- }
109
-
110
- try {
111
- const content = fs.readFileSync(userSecretsPath, 'utf8');
112
- const secrets = parseSecretsContent(content);
113
- if (!secrets || typeof secrets !== 'object') {
114
- throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
115
- }
116
- return secrets;
117
- } catch (error) {
118
- if (error.message.includes('Invalid secrets file format')) {
119
- throw error;
120
- }
121
- logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
122
- return {};
123
- }
97
+ return loadPrimaryUserSecrets();
124
98
  }
125
99
 
126
100
  /**
@@ -134,6 +108,7 @@ function loadDefaultSecrets() {
134
108
  if (!fs.existsSync(defaultPath)) {
135
109
  return {};
136
110
  }
111
+ ensureSecureFilePermissions(defaultPath);
137
112
 
138
113
  try {
139
114
  const content = fs.readFileSync(defaultPath, 'utf8');
@@ -151,6 +126,28 @@ function loadDefaultSecrets() {
151
126
  }
152
127
  }
153
128
 
129
+ /**
130
+ * Creates the primary user secrets file if missing (empty map) for first-run installs.
131
+ * Uses the same directory as {@link loadPrimaryUserSecrets} (config dir / ~/.aifabrix).
132
+ *
133
+ * @function ensurePrimaryUserSecretsFileExists
134
+ */
135
+ function ensurePrimaryUserSecretsFileExists() {
136
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
137
+ const primaryDir = path.dirname(userSecretsPath);
138
+ if (fs.existsSync(userSecretsPath)) {
139
+ return;
140
+ }
141
+ if (!fs.existsSync(primaryDir)) {
142
+ fs.mkdirSync(primaryDir, { recursive: true, mode: 0o700 });
143
+ }
144
+ const header =
145
+ '# Local secrets for AI Fabrix CLI (kv:// references in env.template resolve here)\n' +
146
+ '# Keys are appended by `aifabrix resolve` / ensure — do not prefix with JSON `{}`.\n';
147
+ fs.writeFileSync(userSecretsPath, header, { mode: 0o600 });
148
+ ensureSecureFilePermissions(userSecretsPath);
149
+ }
150
+
154
151
  /**
155
152
  * Builds a map of hostname to service name from environment config
156
153
  * @function buildHostnameToServiceMap
@@ -210,6 +207,7 @@ module.exports = {
210
207
  loadPrimaryUserSecrets,
211
208
  loadUserSecrets,
212
209
  loadDefaultSecrets,
210
+ ensurePrimaryUserSecretsFileExists,
213
211
  buildHostnameToServiceMap,
214
212
  resolveUrlPort
215
213
  };
@@ -63,7 +63,9 @@ function validateSecretsFile(filePath, options = {}) {
63
63
  if (!filePath || typeof filePath !== 'string') {
64
64
  return { valid: false, errors: ['Path is required'], path: '' };
65
65
  }
66
- const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
66
+ // Always normalize to an OS-resolved absolute path. This makes behavior consistent
67
+ // on Windows when callers pass POSIX-style absolute paths like "/nonexistent/foo".
68
+ const resolvedPath = path.resolve(filePath);
67
69
  if (!fs.existsSync(resolvedPath)) {
68
70
  return { valid: false, errors: [`File not found: ${resolvedPath}`], path: resolvedPath };
69
71
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * YAML merge utilities that preserve comments/blank lines for flat secrets files.
3
+ *
4
+ * Intended for `secrets.local.yaml` which is typically a simple `key: value` mapping
5
+ * with user-owned comments. When the file isn't flat (complex YAML), callers should
6
+ * fall back to a full YAML rewrite.
7
+ *
8
+ * @fileoverview Comment-preserving YAML merge helpers
9
+ */
10
+
11
+ const keyLineRe = /^(\s*)([^#:\n]+):\s*(.*)$/;
12
+
13
+ function isCommentOrBlank(line) {
14
+ const t = line.trim();
15
+ return !t || t.startsWith('#');
16
+ }
17
+
18
+ function parseFlatKeyLine(line) {
19
+ const m = line.match(keyLineRe);
20
+ if (!m) return null;
21
+ const indent = m[1] || '';
22
+ const key = (m[2] || '').trim();
23
+ const rest = m[3] || '';
24
+ if (!key) return null;
25
+ return { indent, key, rest };
26
+ }
27
+
28
+ function splitInlineComment(rest) {
29
+ const idx = rest.indexOf(' #');
30
+ return { inlineComment: idx >= 0 ? rest.slice(idx) : '' };
31
+ }
32
+
33
+ function formatYamlScalarForFlatLine(yaml, dumpOpts, value) {
34
+ const dumped = yaml.dump({ _k: value }, dumpOpts);
35
+ const lines = dumped.split(/\r?\n/).filter(Boolean);
36
+ if (lines.length !== 1) return null;
37
+ const idx = lines[0].indexOf(':');
38
+ if (idx < 0) return null;
39
+ return lines[0].slice(idx + 1).trimStart();
40
+ }
41
+
42
+ function appendMissingKeys(out, seen, desiredSecrets, yaml, dumpOpts) {
43
+ for (const key of Object.keys(desiredSecrets)) {
44
+ if (seen.has(key)) continue;
45
+ const scalar = formatYamlScalarForFlatLine(yaml, dumpOpts, desiredSecrets[key]);
46
+ if (scalar === null) return null;
47
+ out.push(`${key}: ${scalar}`.trimEnd());
48
+ seen.add(key);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function mergeExistingLinesPreservingComments(lines, desiredSecrets, yaml, dumpOpts) {
54
+ const out = [];
55
+ const seen = new Set();
56
+ const encountered = new Set();
57
+
58
+ for (const line of lines) {
59
+ if (isCommentOrBlank(line)) {
60
+ out.push(line);
61
+ continue;
62
+ }
63
+ const parsed = parseFlatKeyLine(line);
64
+ if (!parsed) return null;
65
+ const { indent, key, rest } = parsed;
66
+
67
+ if (encountered.has(key)) continue;
68
+ encountered.add(key);
69
+
70
+ if (!Object.prototype.hasOwnProperty.call(desiredSecrets, key)) continue;
71
+
72
+ const { inlineComment } = splitInlineComment(rest);
73
+ const scalar = formatYamlScalarForFlatLine(yaml, dumpOpts, desiredSecrets[key]);
74
+ if (scalar === null) return null;
75
+ seen.add(key);
76
+ out.push(`${indent}${key}: ${scalar}${inlineComment}`.trimEnd());
77
+ }
78
+
79
+ return { out, seen };
80
+ }
81
+
82
+ /**
83
+ * Merge a desired flat secrets object into existing file content while preserving comments/blank lines.
84
+ * Supports deletes: keys present in existing content but absent in desired are removed.
85
+ *
86
+ * Returns null when content cannot be treated as a flat key-value file.
87
+ *
88
+ * @param {string} existingContent
89
+ * @param {Record<string, any>} desiredSecrets
90
+ * @param {{ yaml: any, dumpOpts: any }} yamlCtx
91
+ * @returns {string|null}
92
+ */
93
+ function mergeFlatSecretsYamlPreservingComments(existingContent, desiredSecrets, yamlCtx) {
94
+ if (typeof existingContent !== 'string') return null;
95
+ if (!desiredSecrets || typeof desiredSecrets !== 'object') return null;
96
+ if (!yamlCtx || !yamlCtx.yaml) return null;
97
+
98
+ const { yaml, dumpOpts } = yamlCtx;
99
+ const lines = existingContent.split(/\r?\n/);
100
+ const merged = mergeExistingLinesPreservingComments(lines, desiredSecrets, yaml, dumpOpts);
101
+ if (!merged) return null;
102
+
103
+ const appended = appendMissingKeys(merged.out, merged.seen, desiredSecrets, yaml, dumpOpts);
104
+ if (appended === null) return null;
105
+ return appended.join('\n');
106
+ }
107
+
108
+ module.exports = { mergeFlatSecretsYamlPreservingComments };
109
+
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Secure file permissions for secrets and config (ISO 27001).
3
+ * Ensures sensitive files are restricted to owner-only (0o600) when read or written.
4
+ *
5
+ * @fileoverview Enforce restrictive permissions on secrets and config files
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /** Mode for secrets and admin files: owner read/write only (no group/other). */
16
+ const SECRET_FILE_MODE = 0o600;
17
+
18
+ /** Mode for config file (may contain tokens): owner read/write only. */
19
+ const CONFIG_FILE_MODE = 0o600;
20
+
21
+ /**
22
+ * Ensures a file has restrictive permissions (0o600) when it exists.
23
+ * If the file has group or other read/write/execute bits set, chmods to owner-only.
24
+ * Safe to call on every read path; no-op when file is missing or already 0o600.
25
+ * On Windows, chmod restricts write access; mode bits are not fully supported.
26
+ *
27
+ * @param {string} filePath - Absolute or relative path to the file
28
+ * @param {number} [mode=0o600] - Desired mode (default SECRET_FILE_MODE)
29
+ * @returns {boolean} True if file existed and permissions were (or are now) secure
30
+ */
31
+ function ensureSecureFilePermissions(filePath, mode = SECRET_FILE_MODE) {
32
+ if (!filePath || typeof filePath !== 'string') {
33
+ return false;
34
+ }
35
+ const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
36
+ try {
37
+ if (!fs.existsSync(resolved)) {
38
+ return false;
39
+ }
40
+ const stat = fs.statSync(resolved);
41
+ if (!stat.isFile()) {
42
+ return false;
43
+ }
44
+ const currentMode = stat.mode & 0o777;
45
+ if ((currentMode & 0o77) === 0) {
46
+ return true;
47
+ }
48
+ fs.chmodSync(resolved, mode);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Ensures a directory has restrictive permissions (0o700) when it exists.
57
+ * No-op when directory is missing or already 0o700 (no group/other).
58
+ *
59
+ * @param {string} dirPath - Absolute or relative path to the directory
60
+ * @returns {boolean} True if directory existed and permissions were (or are now) secure
61
+ */
62
+ function ensureSecureDirPermissions(dirPath) {
63
+ if (!dirPath || typeof dirPath !== 'string') {
64
+ return false;
65
+ }
66
+ const resolved = path.isAbsolute(dirPath) ? dirPath : path.resolve(dirPath);
67
+ try {
68
+ if (!fs.existsSync(resolved)) {
69
+ return false;
70
+ }
71
+ const stat = fs.statSync(resolved);
72
+ if (!stat.isDirectory()) {
73
+ return false;
74
+ }
75
+ const currentMode = stat.mode & 0o777;
76
+ if ((currentMode & 0o77) === 0) {
77
+ return true;
78
+ }
79
+ fs.chmodSync(resolved, 0o700);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ module.exports = {
87
+ ensureSecureFilePermissions,
88
+ ensureSecureDirPermissions,
89
+ SECRET_FILE_MODE,
90
+ CONFIG_FILE_MODE
91
+ };
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const fs = require('fs');
8
+ const { nodeFs } = require('../internal/node-fs');
8
9
  const path = require('path');
9
10
  const os = require('os');
10
11
  const { execSync } = require('child_process');
@@ -40,9 +41,10 @@ function getDefaultEd25519PrivateKeyPath() {
40
41
  * @returns {string} Resolved SSH dir path
41
42
  */
42
43
  function ensureSshDir(sshDir) {
44
+ const syncFs = nodeFs();
43
45
  const dir = sshDir || getDefaultSshDir();
44
- if (!fs.existsSync(dir)) {
45
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
+ if (!syncFs.existsSync(dir)) {
47
+ syncFs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
48
  }
47
49
  return dir;
48
50
  }
@@ -35,7 +35,7 @@ async function loadTemplateVariables(templateName) {
35
35
  } catch (error) {
36
36
  // Template application.yaml not found or invalid, continue without it
37
37
  if (error.code !== 'ENOENT') {
38
- logger.warn(chalk.yellow(`⚠️ Warning: Could not load template application.yaml: ${error.message}`));
38
+ logger.warn(chalk.yellow(`⚠ Warning: Could not load template application.yaml: ${error.message}`));
39
39
  }
40
40
  return null;
41
41
  }
@@ -125,7 +125,7 @@ async function updateTemplateVariables(appPath, appName, options, config) {
125
125
  writeConfigFile(variablesPath, variables);
126
126
  } catch (error) {
127
127
  if (error.message && !error.message.includes('not found')) {
128
- logger.warn(chalk.yellow(`⚠️ Warning: Could not update application config: ${error.message}`));
128
+ logger.warn(chalk.yellow(`⚠ Warning: Could not update application config: ${error.message}`));
129
129
  }
130
130
  }
131
131
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Test log writer - writes debug logs to integration/<appKey>/logs/
2
+ * Test log writer - writes debug logs to integration/<systemKey>/logs/
3
3
  * Sanitization (tokens, secrets) is done by dataplane before responses are returned.
4
4
  *
5
5
  * @fileoverview Write test request/response logs for debugging
@@ -29,9 +29,9 @@ function sanitizeForLog(obj, seen = new Set()) {
29
29
  }
30
30
 
31
31
  /**
32
- * Write test log to integration/<appKey>/logs/<logType>-<timestamp>.json
32
+ * Write test log to integration/<systemKey>/logs/<logType>-<timestamp>.json
33
33
  * @async
34
- * @param {string} appKey - Application key (used for path)
34
+ * @param {string} appKey - Integration folder name under integration/ (used for path)
35
35
  * @param {Object} data - Log data (request, response) - will be sanitized
36
36
  * @param {string} [logType] - Log type prefix (default: test-integration)
37
37
  * @param {string} [integrationBaseDir] - Base dir for integration (default: cwd/integration)
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  const fs = require('fs');
13
- const path = require('path');
14
13
  const yaml = require('js-yaml');
15
14
  const config = require('../core/config');
16
15
  const logger = require('./logger');
@@ -21,12 +20,43 @@ const {
21
20
  } = require('./token-manager-refresh');
22
21
  const { warnRefreshFailureOnce, warnRefreshTokenExpiredOnce } = require('./token-manager-messages');
23
22
 
23
+ /** App key used for dataplane client credentials in secrets.local.yaml */
24
+ const DATAPLANE_APP_KEY = 'dataplane';
25
+
24
26
  function getSecretsFilePath() {
25
- return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
27
+ return pathsUtil.getPrimaryUserSecretsLocalPath();
28
+ }
29
+
30
+ /**
31
+ * Validate that secrets.local.yaml contains dataplane client credentials.
32
+ * If missing, the developer should run: aifabrix app rotate-secret dataplane
33
+ * @param {string} [secretsFilePath] - Path to secrets file; defaults to ~/.aifabrix/secrets.local.yaml
34
+ * @returns {{ valid: boolean, hint?: string }}
35
+ */
36
+ function validateDataplaneSecrets(secretsFilePath) {
37
+ const filePath = secretsFilePath || getSecretsFilePath();
38
+ const hint = 'Dataplane credentials are missing. Run: aifabrix app rotate-secret dataplane';
39
+ try {
40
+ if (!fs.existsSync(filePath)) {
41
+ return { valid: false, hint };
42
+ }
43
+ const content = fs.readFileSync(filePath, 'utf8');
44
+ const secrets = yaml.load(content) || {};
45
+ const clientIdKey = `${DATAPLANE_APP_KEY}-client-idKeyVault`;
46
+ const clientSecretKey = `${DATAPLANE_APP_KEY}-client-secretKeyVault`;
47
+ const hasId = secrets[clientIdKey] !== null && secrets[clientIdKey] !== undefined && String(secrets[clientIdKey]).trim() !== '';
48
+ const hasSecret = secrets[clientSecretKey] !== null && secrets[clientSecretKey] !== undefined && String(secrets[clientSecretKey]).trim() !== '';
49
+ if (hasId && hasSecret) {
50
+ return { valid: true };
51
+ }
52
+ return { valid: false, hint };
53
+ } catch {
54
+ return { valid: false, hint };
55
+ }
26
56
  }
27
57
 
28
58
  /**
29
- * Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot/.env).
59
+ * Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot-test/.env).
30
60
  * Reads secrets file using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault.
31
61
  * If not found, checks process.env.CLIENTID and process.env.CLIENTSECRET (set when .env is loaded).
32
62
  * @param {string} appName - Application name
@@ -60,7 +90,7 @@ async function loadClientCredentials(appName) {
60
90
  logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
61
91
  }
62
92
 
63
- // Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot/.env)
93
+ // Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot-test/.env)
64
94
  const envClientId = process.env.CLIENTID || process.env.CLIENT_ID;
65
95
  const envClientSecret = process.env.CLIENTSECRET || process.env.CLIENT_SECRET;
66
96
  if (envClientId && envClientSecret) {
@@ -439,6 +469,7 @@ function requireBearerForDataplanePipeline(authConfig) {
439
469
  }
440
470
 
441
471
  module.exports = {
472
+ DATAPLANE_APP_KEY,
442
473
  getDeviceToken,
443
474
  getClientToken,
444
475
  isTokenExpired,
@@ -452,5 +483,6 @@ module.exports = {
452
483
  getDeploymentAuth,
453
484
  getDeviceOnlyAuth,
454
485
  extractClientCredentials,
455
- requireBearerForDataplanePipeline
486
+ requireBearerForDataplanePipeline,
487
+ validateDataplaneSecrets
456
488
  };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Public URL base for url:// expansion when Traefik + frontDoorRouting.host (plan 122 phase 2).
3
+ *
4
+ * @fileoverview Traefik host template vs remote-server vs localhost+port (no app-specific rules)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
12
+ const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
13
+
14
+ /**
15
+ * Expand frontDoorRouting.host placeholders for url:// (same rules as Traefik labels in compose-generator).
16
+ *
17
+ * @param {string} template
18
+ * @param {Object} opts
19
+ * @param {string|number|null|undefined} opts.developerIdRaw
20
+ * @param {string|null|undefined} opts.remoteServer
21
+ * @returns {string}
22
+ */
23
+ function expandFrontDoorHostTemplateForUrls(template, opts) {
24
+ const { developerIdRaw, remoteServer } = opts || {};
25
+ return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer);
26
+ }
27
+
28
+ function hostPortForProfile(profile, listenPort, developerIdNum) {
29
+ return profile === 'docker'
30
+ ? publishedHostPort(listenPort, developerIdNum)
31
+ : localHostPort(listenPort, developerIdNum);
32
+ }
33
+
34
+ /**
35
+ * Local profile: workstation `+10` applies only to the app being resolved (`currentAppKey`).
36
+ * Cross-app tokens (e.g. `url://keycloak-public` from miso-controller) use `publishedHostPort`
37
+ * (manifest + dev*100, no +10) so sibling services keep compose-published offsets only.
38
+ *
39
+ * @param {Object} opts
40
+ * @param {'docker'|'local'} opts.profile
41
+ * @param {number} opts.listenPort - published/manifest basis for public URLs
42
+ * @param {number} opts.developerIdNum
43
+ * @param {string|undefined} opts.declarativeTargetAppKey - target of the url:// token
44
+ * @param {string|undefined} opts.declarativeCurrentAppKey - app whose env is being generated
45
+ * @returns {number}
46
+ */
47
+ function resolveHostPortForDeclarativePublic(opts) {
48
+ const {
49
+ profile,
50
+ listenPort,
51
+ developerIdNum,
52
+ declarativeTargetAppKey,
53
+ declarativeCurrentAppKey
54
+ } = opts;
55
+ if (profile !== 'local') {
56
+ return hostPortForProfile(profile, listenPort, developerIdNum);
57
+ }
58
+ const cur = String(declarativeCurrentAppKey || '').trim();
59
+ const tgt = String(declarativeTargetAppKey || '').trim();
60
+ const isCurrentApp = !cur || !tgt || tgt === cur;
61
+ return isCurrentApp ? localHostPort(listenPort, developerIdNum) : publishedHostPort(listenPort, developerIdNum);
62
+ }
63
+
64
+ /**
65
+ * Without Traefik, `remote-server` is the dev machine host. Apps bind published host ports, not 443 + path.
66
+ * If the URL already has an explicit port, keep host:port but **scheme follows `infraTlsEnabled`**, not the
67
+ * literal `https://` in `remote-server` when TLS is off (`up-infra` without `--tls`).
68
+ * If `remote-server` omits a port, append the profile-specific published/listen-derived port.
69
+ *
70
+ * @param {string} rawRemote
71
+ * @param {'docker'|'local'} profile
72
+ * @param {number} listenPort
73
+ * @param {number} developerIdNum
74
+ * @param {boolean} infraTlsEnabled
75
+ * @returns {string}
76
+ */
77
+ function remotePublicBaseWithoutTraefik(
78
+ rawRemote,
79
+ profile,
80
+ listenPort,
81
+ developerIdNum,
82
+ infraTlsEnabled,
83
+ declarativePortOpts
84
+ ) {
85
+ const raw = String(rawRemote || '').trim().replace(/\/+$/, '');
86
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : (infraTlsEnabled ? `https://${raw}` : `http://${raw}`);
87
+ const u = new URL(withScheme);
88
+ const scheme = infraTlsEnabled ? 'https' : 'http';
89
+
90
+ if (u.port !== '') {
91
+ return `${scheme}://${u.host}`;
92
+ }
93
+
94
+ const hostPort = resolveHostPortForDeclarativePublic({
95
+ profile,
96
+ listenPort,
97
+ developerIdNum,
98
+ declarativeTargetAppKey: declarativePortOpts && declarativePortOpts.declarativeTargetAppKey,
99
+ declarativeCurrentAppKey: declarativePortOpts && declarativePortOpts.declarativeCurrentAppKey
100
+ });
101
+ const defaultPort = scheme === 'https' ? 443 : 80;
102
+ if (Number(hostPort) === defaultPort) {
103
+ return `${scheme}://${u.hostname}`;
104
+ }
105
+ return `${scheme}://${u.hostname}:${hostPort}`;
106
+ }
107
+
108
+ /**
109
+ * @param {Object} opts - same shape as computePublicUrlBaseString
110
+ * @returns {string|null}
111
+ */
112
+ function buildTraefikPublicBaseIfApplicable(opts) {
113
+ const { traefik, pathActive, hostTemplate, tls, developerIdRaw, remoteServer, infraTlsEnabled } =
114
+ opts;
115
+ // Plan 124: Traefik host authority only when pathActive (traefik ∧ frontDoorRouting.enabled)
116
+ if (!traefik || !pathActive || !hostTemplate || !String(hostTemplate).trim()) {
117
+ return null;
118
+ }
119
+ const expanded = expandFrontDoorHostTemplateForUrls(hostTemplate, {
120
+ developerIdRaw,
121
+ remoteServer
122
+ });
123
+ if (!expanded) {
124
+ return null;
125
+ }
126
+ const useHttps = Boolean(infraTlsEnabled) || tls !== false;
127
+ const scheme = useHttps ? 'https' : 'http';
128
+ return `${scheme}://${expanded}`.replace(/\/+$/, '');
129
+ }
130
+
131
+ /**
132
+ * Scheme + authority (no path) for public url://* when Traefik, remote, or localhost.
133
+ *
134
+ * @param {Object} opts
135
+ * @param {boolean} [opts.traefik]
136
+ * @param {string|null|undefined} opts.hostTemplate
137
+ * @param {boolean} [opts.tls]
138
+ * @param {string|number|null|undefined} opts.developerIdRaw
139
+ * @param {string|null|undefined} opts.remoteServer
140
+ * @param {'docker'|'local'} opts.profile
141
+ * @param {number} opts.listenPort
142
+ * @param {number} opts.developerIdNum
143
+ * @param {boolean} [opts.infraTlsEnabled] - `tlsEnabled` from ~/.aifabrix/config.yaml (`up-infra --tls`); when true, Traefik front-door public URLs use https even if application.yaml has `frontDoorRouting.tls: false`
144
+ * @param {boolean} [opts.pathActive] - traefik ∧ frontDoorRouting.enabled; required for Traefik host branch (plan 124)
145
+ * @returns {string}
146
+ */
147
+ function computePublicUrlBaseString(opts) {
148
+ const { remoteServer, profile, listenPort, developerIdNum, infraTlsEnabled } = opts;
149
+ const declarativePortOpts = {
150
+ declarativeTargetAppKey: opts.declarativeTargetAppKey,
151
+ declarativeCurrentAppKey: opts.declarativeCurrentAppKey
152
+ };
153
+
154
+ const traefikBase = buildTraefikPublicBaseIfApplicable({
155
+ ...opts,
156
+ pathActive: Boolean(opts.pathActive)
157
+ });
158
+ if (traefikBase) {
159
+ return traefikBase;
160
+ }
161
+
162
+ if (remoteServer && String(remoteServer).trim()) {
163
+ return remotePublicBaseWithoutTraefik(
164
+ remoteServer,
165
+ profile,
166
+ listenPort,
167
+ developerIdNum,
168
+ Boolean(infraTlsEnabled),
169
+ declarativePortOpts
170
+ );
171
+ }
172
+
173
+ const hostPort = resolveHostPortForDeclarativePublic({
174
+ profile,
175
+ listenPort,
176
+ developerIdNum,
177
+ declarativeTargetAppKey: declarativePortOpts.declarativeTargetAppKey,
178
+ declarativeCurrentAppKey: declarativePortOpts.declarativeCurrentAppKey
179
+ });
180
+ const scheme = infraTlsEnabled ? 'https' : 'http';
181
+ return `${scheme}://localhost:${hostPort}`;
182
+ }
183
+
184
+ module.exports = {
185
+ expandFrontDoorHostTemplateForUrls,
186
+ computePublicUrlBaseString,
187
+ resolveHostPortForDeclarativePublic
188
+ };