threadforge 0.1.1 → 0.2.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 (358) hide show
  1. package/README.md +52 -20
  2. package/bin/forge.js +2 -1058
  3. package/bin/host-commands.d.ts +2 -0
  4. package/bin/host-commands.d.ts.map +1 -0
  5. package/bin/host-commands.js +7 -8
  6. package/bin/platform-commands.d.ts +2 -0
  7. package/bin/platform-commands.d.ts.map +1 -0
  8. package/bin/platform-commands.js +118 -36
  9. package/dist/cli/base-command.d.ts +12 -0
  10. package/dist/cli/base-command.d.ts.map +1 -0
  11. package/dist/cli/base-command.js +25 -0
  12. package/dist/cli/base-command.js.map +1 -0
  13. package/dist/cli/commands/build.d.ts +10 -0
  14. package/dist/cli/commands/build.d.ts.map +1 -0
  15. package/dist/cli/commands/build.js +110 -0
  16. package/dist/cli/commands/build.js.map +1 -0
  17. package/dist/cli/commands/deploy.d.ts +12 -0
  18. package/dist/cli/commands/deploy.d.ts.map +1 -0
  19. package/dist/cli/commands/deploy.js +143 -0
  20. package/dist/cli/commands/deploy.js.map +1 -0
  21. package/dist/cli/commands/dev.d.ts +10 -0
  22. package/dist/cli/commands/dev.d.ts.map +1 -0
  23. package/dist/cli/commands/dev.js +138 -0
  24. package/dist/cli/commands/dev.js.map +1 -0
  25. package/dist/cli/commands/generate.d.ts +10 -0
  26. package/dist/cli/commands/generate.d.ts.map +1 -0
  27. package/dist/cli/commands/generate.js +76 -0
  28. package/dist/cli/commands/generate.js.map +1 -0
  29. package/dist/cli/commands/host.d.ts +8 -0
  30. package/dist/cli/commands/host.d.ts.map +1 -0
  31. package/dist/cli/commands/host.js +20 -0
  32. package/dist/cli/commands/host.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +16 -0
  34. package/dist/cli/commands/init.d.ts.map +1 -0
  35. package/dist/cli/commands/init.js +246 -0
  36. package/dist/cli/commands/init.js.map +1 -0
  37. package/dist/cli/commands/platform.d.ts +8 -0
  38. package/dist/cli/commands/platform.d.ts.map +1 -0
  39. package/dist/cli/commands/platform.js +20 -0
  40. package/dist/cli/commands/platform.js.map +1 -0
  41. package/dist/cli/commands/restart.d.ts +8 -0
  42. package/dist/cli/commands/restart.d.ts.map +1 -0
  43. package/dist/cli/commands/restart.js +13 -0
  44. package/dist/cli/commands/restart.js.map +1 -0
  45. package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
  46. package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
  47. package/dist/cli/commands/scaffold/frontend.js +130 -0
  48. package/dist/cli/commands/scaffold/frontend.js.map +1 -0
  49. package/dist/cli/commands/scaffold/react.d.ts +7 -0
  50. package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
  51. package/dist/cli/commands/scaffold/react.js +12 -0
  52. package/dist/cli/commands/scaffold/react.js.map +1 -0
  53. package/dist/cli/commands/scale.d.ts +8 -0
  54. package/dist/cli/commands/scale.d.ts.map +1 -0
  55. package/dist/cli/commands/scale.js +13 -0
  56. package/dist/cli/commands/scale.js.map +1 -0
  57. package/dist/cli/commands/start.d.ts +10 -0
  58. package/dist/cli/commands/start.d.ts.map +1 -0
  59. package/dist/cli/commands/start.js +71 -0
  60. package/dist/cli/commands/start.js.map +1 -0
  61. package/dist/cli/commands/status.d.ts +11 -0
  62. package/dist/cli/commands/status.d.ts.map +1 -0
  63. package/dist/cli/commands/status.js +60 -0
  64. package/dist/cli/commands/status.js.map +1 -0
  65. package/dist/cli/commands/stop.d.ts +10 -0
  66. package/dist/cli/commands/stop.d.ts.map +1 -0
  67. package/dist/cli/commands/stop.js +89 -0
  68. package/dist/cli/commands/stop.js.map +1 -0
  69. package/dist/cli/util/config-discovery.d.ts +8 -0
  70. package/dist/cli/util/config-discovery.d.ts.map +1 -0
  71. package/dist/cli/util/config-discovery.js +70 -0
  72. package/dist/cli/util/config-discovery.js.map +1 -0
  73. package/dist/cli/util/config-patcher.d.ts +17 -0
  74. package/dist/cli/util/config-patcher.d.ts.map +1 -0
  75. package/dist/cli/util/config-patcher.js +439 -0
  76. package/dist/cli/util/config-patcher.js.map +1 -0
  77. package/dist/cli/util/frontend-dev.d.ts +8 -0
  78. package/dist/cli/util/frontend-dev.d.ts.map +1 -0
  79. package/dist/cli/util/frontend-dev.js +117 -0
  80. package/dist/cli/util/frontend-dev.js.map +1 -0
  81. package/dist/cli/util/process.d.ts +5 -0
  82. package/dist/cli/util/process.d.ts.map +1 -0
  83. package/dist/cli/util/process.js +17 -0
  84. package/dist/cli/util/process.js.map +1 -0
  85. package/dist/cli/util/templates.d.ts +10 -0
  86. package/dist/cli/util/templates.d.ts.map +1 -0
  87. package/dist/cli/util/templates.js +157 -0
  88. package/dist/cli/util/templates.js.map +1 -0
  89. package/dist/core/AlertSink.d.ts +83 -0
  90. package/dist/core/AlertSink.d.ts.map +1 -0
  91. package/dist/core/AlertSink.js +126 -0
  92. package/dist/core/AlertSink.js.map +1 -0
  93. package/dist/core/DirectMessageBus.d.ts +88 -0
  94. package/dist/core/DirectMessageBus.d.ts.map +1 -0
  95. package/dist/core/DirectMessageBus.js +352 -0
  96. package/dist/core/DirectMessageBus.js.map +1 -0
  97. package/dist/core/EndpointResolver.d.ts +111 -0
  98. package/dist/core/EndpointResolver.d.ts.map +1 -0
  99. package/dist/core/EndpointResolver.js +336 -0
  100. package/dist/core/EndpointResolver.js.map +1 -0
  101. package/dist/core/ForgeContext.d.ts +221 -0
  102. package/dist/core/ForgeContext.d.ts.map +1 -0
  103. package/dist/core/ForgeContext.js +1169 -0
  104. package/dist/core/ForgeContext.js.map +1 -0
  105. package/dist/core/ForgeEndpoints.d.ts +71 -0
  106. package/dist/core/ForgeEndpoints.d.ts.map +1 -0
  107. package/dist/core/ForgeEndpoints.js +442 -0
  108. package/dist/core/ForgeEndpoints.js.map +1 -0
  109. package/dist/core/ForgeHost.d.ts +82 -0
  110. package/dist/core/ForgeHost.d.ts.map +1 -0
  111. package/dist/core/ForgeHost.js +107 -0
  112. package/dist/core/ForgeHost.js.map +1 -0
  113. package/dist/core/ForgePlatform.d.ts +96 -0
  114. package/dist/core/ForgePlatform.d.ts.map +1 -0
  115. package/dist/core/ForgePlatform.js +136 -0
  116. package/dist/core/ForgePlatform.js.map +1 -0
  117. package/dist/core/ForgeWebSocket.d.ts +56 -0
  118. package/dist/core/ForgeWebSocket.d.ts.map +1 -0
  119. package/dist/core/ForgeWebSocket.js +415 -0
  120. package/dist/core/ForgeWebSocket.js.map +1 -0
  121. package/dist/core/Ingress.d.ts +329 -0
  122. package/dist/core/Ingress.d.ts.map +1 -0
  123. package/dist/core/Ingress.js +694 -0
  124. package/dist/core/Ingress.js.map +1 -0
  125. package/dist/core/Interceptors.d.ts +134 -0
  126. package/dist/core/Interceptors.d.ts.map +1 -0
  127. package/dist/core/Interceptors.js +416 -0
  128. package/dist/core/Interceptors.js.map +1 -0
  129. package/dist/core/Logger.d.ts +20 -0
  130. package/dist/core/Logger.d.ts.map +1 -0
  131. package/dist/core/Logger.js +77 -0
  132. package/dist/core/Logger.js.map +1 -0
  133. package/dist/core/MessageBus.d.ts +15 -0
  134. package/dist/core/MessageBus.d.ts.map +1 -0
  135. package/dist/core/MessageBus.js +18 -0
  136. package/dist/core/MessageBus.js.map +1 -0
  137. package/dist/core/Prometheus.d.ts +80 -0
  138. package/dist/core/Prometheus.d.ts.map +1 -0
  139. package/dist/core/Prometheus.js +332 -0
  140. package/dist/core/Prometheus.js.map +1 -0
  141. package/dist/core/RequestContext.d.ts +214 -0
  142. package/dist/core/RequestContext.d.ts.map +1 -0
  143. package/dist/core/RequestContext.js +556 -0
  144. package/dist/core/RequestContext.js.map +1 -0
  145. package/dist/core/Router.d.ts +45 -0
  146. package/dist/core/Router.d.ts.map +1 -0
  147. package/dist/core/Router.js +285 -0
  148. package/dist/core/Router.js.map +1 -0
  149. package/dist/core/RoutingStrategy.d.ts +116 -0
  150. package/dist/core/RoutingStrategy.d.ts.map +1 -0
  151. package/dist/core/RoutingStrategy.js +306 -0
  152. package/dist/core/RoutingStrategy.js.map +1 -0
  153. package/dist/core/RpcConfig.d.ts +72 -0
  154. package/dist/core/RpcConfig.d.ts.map +1 -0
  155. package/dist/core/RpcConfig.js +127 -0
  156. package/dist/core/RpcConfig.js.map +1 -0
  157. package/dist/core/SignatureCache.d.ts +81 -0
  158. package/dist/core/SignatureCache.d.ts.map +1 -0
  159. package/dist/core/SignatureCache.js +172 -0
  160. package/dist/core/SignatureCache.js.map +1 -0
  161. package/dist/core/StaticFileServer.d.ts +34 -0
  162. package/dist/core/StaticFileServer.d.ts.map +1 -0
  163. package/dist/core/StaticFileServer.js +497 -0
  164. package/dist/core/StaticFileServer.js.map +1 -0
  165. package/dist/core/Supervisor.d.ts +198 -0
  166. package/dist/core/Supervisor.d.ts.map +1 -0
  167. package/dist/core/Supervisor.js +1418 -0
  168. package/dist/core/Supervisor.js.map +1 -0
  169. package/dist/core/ThreadAllocator.d.ts +52 -0
  170. package/dist/core/ThreadAllocator.d.ts.map +1 -0
  171. package/dist/core/ThreadAllocator.js +174 -0
  172. package/dist/core/ThreadAllocator.js.map +1 -0
  173. package/dist/core/WorkerChannelManager.d.ts +130 -0
  174. package/dist/core/WorkerChannelManager.d.ts.map +1 -0
  175. package/dist/core/WorkerChannelManager.js +956 -0
  176. package/dist/core/WorkerChannelManager.js.map +1 -0
  177. package/dist/core/config-enums.d.ts +41 -0
  178. package/dist/core/config-enums.d.ts.map +1 -0
  179. package/dist/core/config-enums.js +59 -0
  180. package/dist/core/config-enums.js.map +1 -0
  181. package/dist/core/config.d.ts +159 -0
  182. package/dist/core/config.d.ts.map +1 -0
  183. package/dist/core/config.js +694 -0
  184. package/dist/core/config.js.map +1 -0
  185. package/dist/core/host-config.d.ts +146 -0
  186. package/dist/core/host-config.d.ts.map +1 -0
  187. package/dist/core/host-config.js +312 -0
  188. package/dist/core/host-config.js.map +1 -0
  189. package/dist/core/ipc-errors.d.ts +27 -0
  190. package/dist/core/ipc-errors.d.ts.map +1 -0
  191. package/dist/core/ipc-errors.js +36 -0
  192. package/dist/core/ipc-errors.js.map +1 -0
  193. package/dist/core/network-utils.d.ts +35 -0
  194. package/dist/core/network-utils.d.ts.map +1 -0
  195. package/dist/core/network-utils.js +145 -0
  196. package/dist/core/network-utils.js.map +1 -0
  197. package/dist/core/platform-config.d.ts +142 -0
  198. package/dist/core/platform-config.d.ts.map +1 -0
  199. package/dist/core/platform-config.js +299 -0
  200. package/dist/core/platform-config.js.map +1 -0
  201. package/dist/decorators/ServiceProxy.d.ts +175 -0
  202. package/dist/decorators/ServiceProxy.d.ts.map +1 -0
  203. package/dist/decorators/ServiceProxy.js +969 -0
  204. package/dist/decorators/ServiceProxy.js.map +1 -0
  205. package/dist/decorators/index.d.ts +146 -0
  206. package/dist/decorators/index.d.ts.map +1 -0
  207. package/dist/decorators/index.js +545 -0
  208. package/dist/decorators/index.js.map +1 -0
  209. package/dist/deploy/NginxGenerator.d.ts +165 -0
  210. package/dist/deploy/NginxGenerator.d.ts.map +1 -0
  211. package/dist/deploy/NginxGenerator.js +781 -0
  212. package/dist/deploy/NginxGenerator.js.map +1 -0
  213. package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
  214. package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
  215. package/dist/deploy/PlatformManifestGenerator.js +80 -0
  216. package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
  217. package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
  218. package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
  219. package/dist/deploy/RouteManifestGenerator.js +105 -0
  220. package/dist/deploy/RouteManifestGenerator.js.map +1 -0
  221. package/dist/deploy/index.d.ts +210 -0
  222. package/dist/deploy/index.d.ts.map +1 -0
  223. package/dist/deploy/index.js +918 -0
  224. package/dist/deploy/index.js.map +1 -0
  225. package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
  226. package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
  227. package/dist/frontend/FrontendDevLifecycle.js +60 -0
  228. package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
  229. package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
  230. package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
  231. package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
  232. package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
  233. package/dist/frontend/SiteResolver.d.ts +33 -0
  234. package/dist/frontend/SiteResolver.d.ts.map +1 -0
  235. package/dist/frontend/SiteResolver.js +53 -0
  236. package/dist/frontend/SiteResolver.js.map +1 -0
  237. package/dist/frontend/StaticMountRegistry.d.ts +36 -0
  238. package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
  239. package/dist/frontend/StaticMountRegistry.js +94 -0
  240. package/dist/frontend/StaticMountRegistry.js.map +1 -0
  241. package/dist/frontend/index.d.ts +7 -0
  242. package/dist/frontend/index.d.ts.map +1 -0
  243. package/{src → dist}/frontend/index.js +4 -2
  244. package/dist/frontend/index.js.map +1 -0
  245. package/dist/frontend/pathUtils.d.ts +8 -0
  246. package/dist/frontend/pathUtils.d.ts.map +1 -0
  247. package/dist/frontend/pathUtils.js +17 -0
  248. package/dist/frontend/pathUtils.js.map +1 -0
  249. package/dist/frontend/plugins/index.d.ts +2 -0
  250. package/dist/frontend/plugins/index.d.ts.map +1 -0
  251. package/{src → dist}/frontend/plugins/index.js +1 -1
  252. package/dist/frontend/plugins/index.js.map +1 -0
  253. package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
  254. package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
  255. package/dist/frontend/plugins/viteFrontend.js +134 -0
  256. package/dist/frontend/plugins/viteFrontend.js.map +1 -0
  257. package/dist/frontend/types.d.ts +25 -0
  258. package/dist/frontend/types.d.ts.map +1 -0
  259. package/dist/frontend/types.js +2 -0
  260. package/dist/frontend/types.js.map +1 -0
  261. package/dist/index.d.ts +17 -0
  262. package/dist/index.d.ts.map +1 -0
  263. package/dist/index.js +32 -0
  264. package/dist/index.js.map +1 -0
  265. package/dist/internals.d.ts +21 -0
  266. package/dist/internals.d.ts.map +1 -0
  267. package/{src → dist}/internals.js +12 -14
  268. package/dist/internals.js.map +1 -0
  269. package/dist/plugins/PluginManager.d.ts +209 -0
  270. package/dist/plugins/PluginManager.d.ts.map +1 -0
  271. package/dist/plugins/PluginManager.js +365 -0
  272. package/dist/plugins/PluginManager.js.map +1 -0
  273. package/dist/plugins/ScopedPostgres.d.ts +78 -0
  274. package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
  275. package/dist/plugins/ScopedPostgres.js +190 -0
  276. package/dist/plugins/ScopedPostgres.js.map +1 -0
  277. package/dist/plugins/ScopedRedis.d.ts +88 -0
  278. package/dist/plugins/ScopedRedis.d.ts.map +1 -0
  279. package/dist/plugins/ScopedRedis.js +169 -0
  280. package/dist/plugins/ScopedRedis.js.map +1 -0
  281. package/dist/plugins/index.d.ts +289 -0
  282. package/dist/plugins/index.d.ts.map +1 -0
  283. package/dist/plugins/index.js +1942 -0
  284. package/dist/plugins/index.js.map +1 -0
  285. package/dist/plugins/types.d.ts +59 -0
  286. package/dist/plugins/types.d.ts.map +1 -0
  287. package/dist/plugins/types.js +2 -0
  288. package/dist/plugins/types.js.map +1 -0
  289. package/dist/registry/ServiceRegistry.d.ts +305 -0
  290. package/dist/registry/ServiceRegistry.d.ts.map +1 -0
  291. package/dist/registry/ServiceRegistry.js +735 -0
  292. package/dist/registry/ServiceRegistry.js.map +1 -0
  293. package/dist/scaling/ScaleAdvisor.d.ts +214 -0
  294. package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
  295. package/dist/scaling/ScaleAdvisor.js +526 -0
  296. package/dist/scaling/ScaleAdvisor.js.map +1 -0
  297. package/dist/services/Service.d.ts +164 -0
  298. package/dist/services/Service.d.ts.map +1 -0
  299. package/dist/services/Service.js +106 -0
  300. package/dist/services/Service.js.map +1 -0
  301. package/dist/services/worker-bootstrap.d.ts +15 -0
  302. package/dist/services/worker-bootstrap.d.ts.map +1 -0
  303. package/dist/services/worker-bootstrap.js +744 -0
  304. package/dist/services/worker-bootstrap.js.map +1 -0
  305. package/dist/templates/auth-service.d.ts +42 -0
  306. package/dist/templates/auth-service.d.ts.map +1 -0
  307. package/dist/templates/auth-service.js +54 -0
  308. package/dist/templates/auth-service.js.map +1 -0
  309. package/dist/templates/identity-service.d.ts +50 -0
  310. package/dist/templates/identity-service.d.ts.map +1 -0
  311. package/dist/templates/identity-service.js +62 -0
  312. package/dist/templates/identity-service.js.map +1 -0
  313. package/dist/types/contract.d.ts +120 -0
  314. package/dist/types/contract.d.ts.map +1 -0
  315. package/dist/types/contract.js +69 -0
  316. package/dist/types/contract.js.map +1 -0
  317. package/package.json +78 -20
  318. package/src/core/DirectMessageBus.js +0 -364
  319. package/src/core/EndpointResolver.js +0 -259
  320. package/src/core/ForgeContext.js +0 -2236
  321. package/src/core/ForgeHost.js +0 -122
  322. package/src/core/ForgePlatform.js +0 -145
  323. package/src/core/Ingress.js +0 -768
  324. package/src/core/Interceptors.js +0 -420
  325. package/src/core/MessageBus.js +0 -321
  326. package/src/core/Prometheus.js +0 -305
  327. package/src/core/RequestContext.js +0 -413
  328. package/src/core/RoutingStrategy.js +0 -330
  329. package/src/core/Supervisor.js +0 -1349
  330. package/src/core/ThreadAllocator.js +0 -196
  331. package/src/core/WorkerChannelManager.js +0 -879
  332. package/src/core/config.js +0 -637
  333. package/src/core/host-config.js +0 -311
  334. package/src/core/network-utils.js +0 -166
  335. package/src/core/platform-config.js +0 -308
  336. package/src/decorators/ServiceProxy.js +0 -904
  337. package/src/decorators/index.js +0 -571
  338. package/src/deploy/NginxGenerator.js +0 -865
  339. package/src/deploy/PlatformManifestGenerator.js +0 -96
  340. package/src/deploy/RouteManifestGenerator.js +0 -112
  341. package/src/deploy/index.js +0 -984
  342. package/src/frontend/FrontendDevLifecycle.js +0 -65
  343. package/src/frontend/FrontendPluginOrchestrator.js +0 -187
  344. package/src/frontend/SiteResolver.js +0 -63
  345. package/src/frontend/StaticMountRegistry.js +0 -90
  346. package/src/frontend/plugins/viteFrontend.js +0 -79
  347. package/src/frontend/types.js +0 -35
  348. package/src/index.js +0 -58
  349. package/src/plugins/PluginManager.js +0 -537
  350. package/src/plugins/ScopedPostgres.js +0 -192
  351. package/src/plugins/ScopedRedis.js +0 -142
  352. package/src/plugins/index.js +0 -1756
  353. package/src/registry/ServiceRegistry.js +0 -797
  354. package/src/scaling/ScaleAdvisor.js +0 -442
  355. package/src/services/Service.js +0 -195
  356. package/src/services/worker-bootstrap.js +0 -679
  357. package/src/templates/auth-service.js +0 -65
  358. package/src/templates/identity-service.js +0 -75
@@ -0,0 +1,781 @@
1
+ import path from "node:path";
2
+ import { ServiceType } from "../core/config-enums.js";
3
+ // ── Regex constants ─────────────────────────────────────────────────
4
+ const SAFE_DOMAIN_RE = /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/;
5
+ const SAFE_PREFIX_RE = /^\/[a-zA-Z0-9/_-]*$/;
6
+ const SAFE_SERVICE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
7
+ const SAFE_PROXY_HOST_RE = /^[a-zA-Z0-9.-]+$/;
8
+ // ── Internal helpers ────────────────────────────────────────────────
9
+ function resolveStaticPath(staticPath, label = "staticDir") {
10
+ if (typeof staticPath !== "string" || staticPath.trim() === "") {
11
+ throw new Error(`${label} must be a non-empty string`);
12
+ }
13
+ if (staticPath.includes("\0")) {
14
+ throw new Error(`${label} contains an invalid null byte`);
15
+ }
16
+ const containsTraversal = staticPath
17
+ .replace(/\\/g, "/")
18
+ .split("/")
19
+ .some((segment) => segment === "..");
20
+ if (containsTraversal) {
21
+ throw new Error(`${label} must not include path traversal segments`);
22
+ }
23
+ return path.resolve(staticPath).replace(/\/+$/, "");
24
+ }
25
+ function validateDomain(domain) {
26
+ if (domain && !SAFE_DOMAIN_RE.test(domain)) {
27
+ throw new Error(`Invalid domain name: "${domain}". Only alphanumeric, dots, and hyphens allowed.`);
28
+ }
29
+ }
30
+ function validateStaticDir(staticDir, appId) {
31
+ try {
32
+ return resolveStaticPath(staticDir, `static dir for app "${appId}"`);
33
+ }
34
+ catch (err) {
35
+ if (err instanceof Error && /path traversal/i.test(err.message)) {
36
+ throw new Error(`Invalid static dir for app "${appId}": path traversal not allowed`);
37
+ }
38
+ throw new Error(`Invalid static dir for app "${appId}": ${err instanceof Error ? err.message : String(err)}`);
39
+ }
40
+ }
41
+ function normalizeMountBasePath(basePath = "/") {
42
+ if (!basePath || basePath === "/")
43
+ return "/";
44
+ let normalized = basePath;
45
+ if (!normalized.startsWith("/"))
46
+ normalized = `/${normalized}`;
47
+ if (normalized.length > 1 && normalized.endsWith("/"))
48
+ normalized = normalized.slice(0, -1);
49
+ return normalized;
50
+ }
51
+ function cacheControlFromPolicy(policy = "short") {
52
+ if (policy === "immutable")
53
+ return "public, max-age=31536000, immutable";
54
+ if (policy === "none")
55
+ return "no-store";
56
+ return "public, max-age=300";
57
+ }
58
+ function normalizeForgeProxyTargetConfig(forgeProxy, modeLabel = "this mode") {
59
+ const defaults = {
60
+ host: "127.0.0.1",
61
+ port: 8080,
62
+ keepalive: 64,
63
+ };
64
+ if (forgeProxy === undefined || forgeProxy === null || forgeProxy === true) {
65
+ return defaults;
66
+ }
67
+ if (forgeProxy === false) {
68
+ throw new Error(`forgeProxy cannot be false in ${modeLabel}`);
69
+ }
70
+ if (typeof forgeProxy !== "object" || Array.isArray(forgeProxy)) {
71
+ throw new Error("forgeProxy must be true or an object");
72
+ }
73
+ if (forgeProxy.enabled === false) {
74
+ throw new Error(`forgeProxy.enabled cannot be false in ${modeLabel}`);
75
+ }
76
+ if (forgeProxy.enabled !== undefined && forgeProxy.enabled !== true) {
77
+ throw new Error("forgeProxy.enabled must be true when provided");
78
+ }
79
+ const host = forgeProxy.host ?? defaults.host;
80
+ if (typeof host !== "string" || host.trim() === "" || !SAFE_PROXY_HOST_RE.test(host)) {
81
+ throw new Error("forgeProxy.host must be a valid hostname or IPv4 address");
82
+ }
83
+ const port = forgeProxy.port ?? defaults.port;
84
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
85
+ throw new Error("forgeProxy.port must be an integer between 1 and 65535");
86
+ }
87
+ const keepalive = forgeProxy.keepalive ?? defaults.keepalive;
88
+ if (!Number.isInteger(keepalive) || keepalive < 1) {
89
+ throw new Error("forgeProxy.keepalive must be a positive integer");
90
+ }
91
+ return {
92
+ host,
93
+ port,
94
+ keepalive,
95
+ };
96
+ }
97
+ // ── Exports ─────────────────────────────────────────────────────────
98
+ /**
99
+ * Generate nginx.conf for a standard (non-platform) deployment.
100
+ */
101
+ export function generateNginxConfig(options = {}) {
102
+ const domain = options.domain ?? "localhost";
103
+ validateDomain(domain);
104
+ const services = options.services ?? {};
105
+ const ssl = options.ssl;
106
+ const maxBodySize = options.maxBodySize ?? 10;
107
+ const rateLimits = options.rateLimits ?? {};
108
+ const forgeProxyTarget = normalizeForgeProxyTargetConfig(options.forgeProxy, "deploy mode");
109
+ let config = `# nginx.conf — Auto-generated by ThreadForge
110
+ # Domain: ${domain}
111
+ # Edge services: ${Object.keys(services).length}
112
+ #
113
+ # nginx routes ingress through ForgeProxy.
114
+ # Service-to-service calls still go via ThreadForge IPC/HTTP.
115
+
116
+ worker_processes auto;
117
+ worker_rlimit_nofile 65535;
118
+
119
+ events {
120
+ worker_connections 16384;
121
+ multi_accept on;
122
+ # Nginx auto-selects the optimal event method (epoll on Linux, kqueue on BSD)
123
+ }
124
+
125
+ http {
126
+ sendfile on;
127
+ tcp_nopush on;
128
+ tcp_nodelay on;
129
+ keepalive_timeout 65;
130
+ keepalive_requests 1000;
131
+ types_hash_max_size 2048;
132
+ server_tokens off;
133
+ include /etc/nginx/mime.types;
134
+ default_type application/json;
135
+
136
+ # Structured JSON logging
137
+ log_format json escape=json '{'
138
+ '"time":"$time_iso8601",'
139
+ '"addr":"$remote_addr",'
140
+ '"method":"$request_method",'
141
+ '"uri":"$request_uri",'
142
+ '"status":$status,'
143
+ '"bytes":$body_bytes_sent,'
144
+ '"ms":$request_time,'
145
+ '"upstream":"$upstream_addr",'
146
+ '"upstream_ms":"$upstream_response_time"'
147
+ '}';
148
+ access_log /var/log/nginx/access.log json;
149
+ error_log /var/log/nginx/error.log warn;
150
+
151
+ gzip on;
152
+ gzip_vary on;
153
+ gzip_proxied any;
154
+ gzip_comp_level 4;
155
+ gzip_types text/plain text/css application/json application/javascript text/xml;
156
+
157
+ # Rate limiting zones
158
+ limit_req_zone $binary_remote_addr zone=general:10m rate=${rateLimits.requestsPerSecond ?? 30}r/s;
159
+ limit_req_zone $binary_remote_addr zone=auth:10m rate=${rateLimits.authPerSecond ?? 5}r/s;
160
+ limit_conn_zone $binary_remote_addr zone=connlimit:10m;
161
+ `;
162
+ // Add real_ip configuration if proxy addresses provided
163
+ if (options.trustedProxies && options.trustedProxies.length > 0) {
164
+ for (const proxy of options.trustedProxies) {
165
+ config += ` set_real_ip_from ${proxy};\n`;
166
+ }
167
+ config += ` real_ip_header X-Forwarded-For;\n`;
168
+ config += ` real_ip_recursive on;\n`;
169
+ }
170
+ config += `
171
+
172
+ client_max_body_size ${maxBodySize}m;
173
+ client_body_timeout 15s;
174
+ client_header_timeout 15s;
175
+ send_timeout 30s;
176
+
177
+ `;
178
+ config += ` upstream forgeproxy {\n`;
179
+ config += ` server ${forgeProxyTarget.host}:${forgeProxyTarget.port};\n`;
180
+ config += ` keepalive ${forgeProxyTarget.keepalive};\n`;
181
+ config += ` }\n\n`;
182
+ // HTTPS redirect
183
+ if (ssl) {
184
+ config += ` server {
185
+ listen 80;
186
+ server_name ${domain};
187
+ return 301 https://$host$request_uri;
188
+ }
189
+
190
+ `;
191
+ }
192
+ // Main server block
193
+ config += ` server {\n`;
194
+ if (ssl) {
195
+ config += ` listen 443 ssl http2;\n`;
196
+ config += ` server_name ${domain};\n`;
197
+ config += ` ssl_certificate ${ssl.cert};\n`;
198
+ config += ` ssl_certificate_key ${ssl.key};\n`;
199
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`;
200
+ config += ` ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;\n`;
201
+ config += ` ssl_prefer_server_ciphers off;\n`;
202
+ config += ` ssl_session_cache shared:SSL:10m;\n`;
203
+ }
204
+ else {
205
+ config += ` listen 80;\n`;
206
+ config += ` server_name ${domain};\n`;
207
+ }
208
+ config += `
209
+ # Security headers
210
+ add_header X-Frame-Options "DENY" always;
211
+ add_header X-Content-Type-Options "nosniff" always;
212
+ add_header Content-Security-Policy "frame-ancestors 'none'" always;
213
+ `;
214
+ if (ssl) {
215
+ config += ` add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;\n`;
216
+ }
217
+ config += `
218
+ # Per-IP connection limit
219
+ limit_conn connlimit ${rateLimits.maxConnsPerIP ?? 100};
220
+
221
+ `;
222
+ // Static files (Bug #6: path traversal + Bug #2: security headers in child block)
223
+ if (options.staticDir) {
224
+ const staticDir = resolveStaticPath(options.staticDir, "staticDir");
225
+ config += ` # Static files (served by nginx, never touches Node)
226
+ location /static/ {
227
+ alias ${staticDir}/;
228
+ add_header Cache-Control "public, max-age=86400";
229
+ add_header X-Content-Type-Options nosniff always;
230
+ add_header X-Frame-Options DENY always;
231
+ add_header Content-Security-Policy "frame-ancestors 'none'" always;${ssl ? `\n add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;` : ""}
232
+ }
233
+
234
+ `;
235
+ }
236
+ // Route each service by its prefix
237
+ // Sort by prefix length descending so more specific prefixes match first
238
+ const sorted = Object.entries(services)
239
+ .filter(([, s]) => s.prefix)
240
+ .sort((a, b) => (b[1].prefix?.length ?? 0) - (a[1].prefix?.length ?? 0));
241
+ for (const [name, svc] of sorted) {
242
+ const safeName = name.replace(/:/g, "_");
243
+ if (!SAFE_SERVICE_NAME_RE.test(safeName)) {
244
+ throw new Error(`Invalid service name for nginx config: "${name}"`);
245
+ }
246
+ const prefix = svc.prefix;
247
+ if (prefix && !SAFE_PREFIX_RE.test(prefix)) {
248
+ throw new Error(`Invalid nginx location prefix: "${prefix}". Only alphanumeric, slashes, underscores, and hyphens allowed.`);
249
+ }
250
+ const isAuth = /auth|login|oauth/i.test(prefix ?? "");
251
+ const zone = isAuth ? "auth" : "general";
252
+ const burst = isAuth ? (rateLimits.authBurst ?? 10) : (rateLimits.burst ?? 50);
253
+ config += ` # ${safeName} service → ${prefix}
254
+ location ${prefix} {
255
+ limit_req zone=${zone} burst=${burst} nodelay;
256
+
257
+ proxy_pass http://forgeproxy;
258
+ proxy_http_version 1.1;
259
+ proxy_set_header Connection "";
260
+ proxy_set_header Host $host;
261
+ proxy_set_header X-Real-IP $remote_addr;
262
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
263
+ proxy_set_header X-Forwarded-Proto $scheme;
264
+ proxy_set_header X-Request-ID $request_id;
265
+
266
+ proxy_connect_timeout 5s;
267
+ proxy_read_timeout 30s;
268
+ proxy_buffering on;
269
+ proxy_buffer_size 8k;
270
+ proxy_buffers 8 8k;
271
+
272
+ proxy_next_upstream error timeout http_502 http_503 http_504;
273
+ proxy_next_upstream_tries 2;
274
+ proxy_next_upstream_timeout 5s;
275
+ }
276
+
277
+ `;
278
+ }
279
+ // WebSocket routes
280
+ const wsRoutes = Array.isArray(options.websocketRoutes) ? options.websocketRoutes : [];
281
+ if (wsRoutes.length > 0) {
282
+ for (const route of wsRoutes) {
283
+ const wsPath = route.path;
284
+ const wsService = route.service;
285
+ if (!services[wsService]) {
286
+ throw new Error(`WebSocket route "${wsPath}" references unknown nginx service "${wsService}"`);
287
+ }
288
+ if (!SAFE_PREFIX_RE.test(wsPath)) {
289
+ throw new Error(`Invalid websocket path "${wsPath}" for nginx location`);
290
+ }
291
+ config += ` # WebSocket — ${wsPath} -> ${wsService}
292
+ location ${wsPath} {
293
+ proxy_pass http://forgeproxy;
294
+ proxy_http_version 1.1;
295
+ proxy_set_header Upgrade $http_upgrade;
296
+ proxy_set_header Connection "upgrade";
297
+ proxy_set_header Host $host;
298
+ proxy_set_header X-Real-IP $remote_addr;
299
+ proxy_read_timeout 3600s;
300
+ proxy_send_timeout 3600s;
301
+ }
302
+
303
+ `;
304
+ }
305
+ }
306
+ else if (options.websockets) {
307
+ // Backward-compat fallback (manifest-level websocket toggles)
308
+ const wsService = Object.keys(services)[0];
309
+ if (!wsService) {
310
+ throw new Error("WebSocket routing requested but no edge services were provided");
311
+ }
312
+ const wsPaths = options.websocketPaths || ["/ws"];
313
+ for (const wsPath of wsPaths) {
314
+ config += ` # WebSocket (legacy) — ${wsPath}
315
+ location ${wsPath} {
316
+ proxy_pass http://forgeproxy;
317
+ proxy_http_version 1.1;
318
+ proxy_set_header Upgrade $http_upgrade;
319
+ proxy_set_header Connection "upgrade";
320
+ proxy_set_header Host $host;
321
+ proxy_set_header X-Real-IP $remote_addr;
322
+ proxy_read_timeout 3600s;
323
+ proxy_send_timeout 3600s;
324
+ }
325
+
326
+ `;
327
+ }
328
+ }
329
+ // Catch-all for services without a prefix (legacy gateway pattern)
330
+ const catchAll = Object.entries(services).find(([, s]) => !s.prefix);
331
+ if (catchAll) {
332
+ const [name] = catchAll;
333
+ config += ` # ${name} (catch-all)
334
+ location / {
335
+ limit_req zone=general burst=${rateLimits.burst ?? 50} nodelay;
336
+
337
+ proxy_pass http://forgeproxy;
338
+ proxy_http_version 1.1;
339
+ proxy_set_header Connection "";
340
+ proxy_set_header Host $host;
341
+ proxy_set_header X-Real-IP $remote_addr;
342
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
343
+ proxy_set_header X-Forwarded-Proto $scheme;
344
+ proxy_set_header X-Request-ID $request_id;
345
+
346
+ proxy_connect_timeout 5s;
347
+ proxy_read_timeout 30s;
348
+ proxy_buffering on;
349
+
350
+ proxy_next_upstream error timeout http_502 http_503 http_504;
351
+ proxy_next_upstream_tries 2;
352
+ proxy_next_upstream_timeout 5s;
353
+ }
354
+
355
+ `;
356
+ }
357
+ // Health check (all services — pick the first one)
358
+ config += ` # Health check
359
+ location /health {
360
+ proxy_pass http://forgeproxy;
361
+ proxy_http_version 1.1;
362
+ proxy_set_header Connection "";
363
+ access_log off;
364
+ }
365
+
366
+ `;
367
+ // Error pages
368
+ config += ` error_page 429 @rate_limited;
369
+ location @rate_limited {
370
+ default_type application/json;
371
+ return 429 '{"error":"Rate limit exceeded","retryAfter":1}';
372
+ }
373
+
374
+ error_page 502 503 504 @upstream_error;
375
+ location @upstream_error {
376
+ default_type application/json;
377
+ return 503 '{"error":"Service temporarily unavailable"}';
378
+ }
379
+ `;
380
+ // Plugin-contributed locations (Redis Commander, pgAdmin, etc.)
381
+ if (options.pluginLocations) {
382
+ for (const loc of options.pluginLocations) {
383
+ // Validate plugin config doesn't contain unbalanced braces or dangerous directives
384
+ const configText = loc.config.trim();
385
+ const openBraces = (configText.match(/\{/g) || []).length;
386
+ const closeBraces = (configText.match(/\}/g) || []).length;
387
+ if (openBraces !== closeBraces) {
388
+ throw new Error(`Plugin location "${loc.path}" has unbalanced braces in config — potential injection`);
389
+ }
390
+ config += `
391
+ # Plugin: ${loc.path}
392
+ location ${loc.path} {
393
+ ${configText
394
+ .split("\n")
395
+ .map((l) => l.trim())
396
+ .join("\n ")}
397
+ }
398
+ `;
399
+ }
400
+ }
401
+ config += ` }
402
+ }
403
+ `;
404
+ return config;
405
+ }
406
+ /**
407
+ * Build the nginx service map from a deployment manifest.
408
+ *
409
+ * Scans the manifest and service configs to determine:
410
+ * - Which services are edge (need nginx routing)
411
+ * - Their prefixes
412
+ * - Their upstream addresses (host:port per node)
413
+ */
414
+ export function buildNginxServiceMap(manifest, serviceConfigs, deployPorts = {}) {
415
+ const services = {};
416
+ for (const [svcName, svcConfig] of Object.entries(serviceConfigs)) {
417
+ if (svcConfig.type !== ServiceType.EDGE)
418
+ continue;
419
+ // Default to 3000 if no port specified in deploy overrides or service config.
420
+ // This fallback is common for single-service setups but may be wrong for multi-service deployments.
421
+ const port = deployPorts[svcName] ?? svcConfig.port ?? 3000;
422
+ if (!deployPorts[svcName] && !svcConfig.port) {
423
+ console.warn(`[Deploy] Edge service "${svcName}" has no explicit port — defaulting to 3000. Set a port in your service config or deploy manifest.`);
424
+ }
425
+ const upstreams = [];
426
+ // Find all nodes that run this service
427
+ for (const [, node] of Object.entries(manifest.nodes)) {
428
+ if (node.services.includes(svcName)) {
429
+ upstreams.push({
430
+ host: node.host,
431
+ port,
432
+ });
433
+ }
434
+ }
435
+ if (upstreams.length === 0) {
436
+ console.warn(`[Deploy] Edge service "${svcName}" is not assigned to any node`);
437
+ continue;
438
+ }
439
+ services[svcName] = {
440
+ prefix: svcConfig.prefix ?? null,
441
+ port,
442
+ upstreams,
443
+ };
444
+ }
445
+ return services;
446
+ }
447
+ /**
448
+ * Build websocket route mappings from per-service websocket config.
449
+ *
450
+ * Supports service-level config forms:
451
+ * websocket: true
452
+ * websocket: ["/ws", "/ws/chat"]
453
+ * websocket: { enabled: true, paths: ["/ws"] }
454
+ */
455
+ export function buildNginxWebSocketRoutes(serviceConfigs, nginxServices, legacy = {}) {
456
+ const routeMap = new Map(); // path -> service
457
+ for (const [svcName, svcConfig] of Object.entries(serviceConfigs)) {
458
+ if (svcConfig.type !== ServiceType.EDGE)
459
+ continue;
460
+ if (!nginxServices[svcName])
461
+ continue;
462
+ const raw = svcConfig.websocket;
463
+ if (!raw)
464
+ continue;
465
+ let enabled = true;
466
+ let paths = ["/ws"];
467
+ if (raw === true) {
468
+ paths = ["/ws"];
469
+ }
470
+ else if (Array.isArray(raw)) {
471
+ paths = raw;
472
+ }
473
+ else if (typeof raw === "object") {
474
+ enabled = raw.enabled !== false;
475
+ if (raw.paths !== undefined)
476
+ paths = raw.paths;
477
+ }
478
+ else {
479
+ continue;
480
+ }
481
+ if (!enabled)
482
+ continue;
483
+ for (const wsPath of paths) {
484
+ if (typeof wsPath !== "string" || !wsPath.startsWith("/")) {
485
+ throw new Error(`Invalid websocket path "${wsPath}" for service "${svcName}"`);
486
+ }
487
+ const existing = routeMap.get(wsPath);
488
+ if (existing && existing !== svcName) {
489
+ throw new Error(`WebSocket path collision: "${wsPath}" is configured for both "${existing}" and "${svcName}"`);
490
+ }
491
+ routeMap.set(wsPath, svcName);
492
+ }
493
+ }
494
+ // Backward compatibility for manifest-level websocket toggles
495
+ if (routeMap.size === 0 && legacy.websockets) {
496
+ const wsService = Object.keys(nginxServices)[0];
497
+ if (!wsService) {
498
+ throw new Error("WebSocket routes requested but no edge services are available for nginx routing");
499
+ }
500
+ for (const wsPath of legacy.websocketPaths ?? ["/ws"]) {
501
+ routeMap.set(wsPath, wsService);
502
+ }
503
+ }
504
+ return [...routeMap.entries()].map(([wsPath, service]) => ({ path: wsPath, service }));
505
+ }
506
+ /**
507
+ * Generate an nginx config for ForgeHost with per-project server blocks.
508
+ *
509
+ * Each project gets its own `server { server_name ... }` block. Requests
510
+ * are forwarded to ForgeProxy, with X-Forge-Project injected per host block.
511
+ */
512
+ export function generateHostNginxConfig(options) {
513
+ const { hostMeta, services, ssl, maxBodySize = 10, forgeProxy } = options;
514
+ const forgeProxyTarget = normalizeForgeProxyTargetConfig(forgeProxy, "host mode");
515
+ let config = `# ForgeHost nginx config — auto-generated
516
+ # Do not edit — regenerate with: forge host generate
517
+
518
+ events {
519
+ worker_connections 16384;
520
+ }
521
+
522
+ http {
523
+ gzip on;
524
+ gzip_types text/plain text/css application/json application/javascript text/xml;
525
+
526
+ client_max_body_size ${maxBodySize}m;
527
+ client_body_timeout 15s;
528
+ send_timeout 30s;
529
+
530
+ `;
531
+ config += ` upstream forgeproxy {\n`;
532
+ config += ` server ${forgeProxyTarget.host}:${forgeProxyTarget.port};\n`;
533
+ config += ` keepalive ${forgeProxyTarget.keepalive};\n`;
534
+ config += ` }\n\n`;
535
+ // Generate a server block per project
536
+ for (const [projectId, meta] of Object.entries(hostMeta)) {
537
+ if (!meta.domain)
538
+ continue;
539
+ // Validate domain (Bug #1)
540
+ validateDomain(meta.domain);
541
+ const projectServices = Object.entries(services).filter(([name]) => meta.services.includes(name));
542
+ const edgeServices = projectServices.filter(([, s]) => s.type === ServiceType.EDGE);
543
+ if (edgeServices.length === 0)
544
+ continue;
545
+ config += ` # ── Project: ${projectId} ──\n`;
546
+ config += ` server {\n`;
547
+ if (ssl) {
548
+ config += ` listen 443 ssl http2;\n`;
549
+ config += ` server_name ${meta.domain};\n`;
550
+ config += ` ssl_certificate ${ssl.cert};\n`;
551
+ config += ` ssl_certificate_key ${ssl.key};\n`;
552
+ }
553
+ else {
554
+ config += ` listen 80;\n`;
555
+ config += ` server_name ${meta.domain};\n`;
556
+ }
557
+ config += `\n`;
558
+ config += ` # Security headers\n`;
559
+ config += ` add_header X-Frame-Options "SAMEORIGIN" always;\n`;
560
+ config += ` add_header X-Content-Type-Options "nosniff" always;\n`;
561
+ config += `\n`;
562
+ for (const [, svc] of edgeServices) {
563
+ const prefix = svc.prefix ?? "/";
564
+ config += ` location ${prefix} {\n`;
565
+ config += ` proxy_pass http://forgeproxy;\n`;
566
+ config += ` proxy_http_version 1.1;\n`;
567
+ config += ` proxy_set_header Connection "";\n`;
568
+ config += ` proxy_set_header Host $host;\n`;
569
+ config += ` proxy_set_header X-Real-IP $remote_addr;\n`;
570
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
571
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`;
572
+ config += ` proxy_set_header X-Forge-Project ${projectId};\n`;
573
+ config += ` }\n\n`;
574
+ }
575
+ config += ` }\n\n`;
576
+ }
577
+ config += `}\n`;
578
+ return config;
579
+ }
580
+ import { resolveAppDomains } from "../core/platform-config.js";
581
+ /**
582
+ * Generate an nginx config for Platform Mode with per-domain TLS,
583
+ * per-app static dirs, shared auth mount, and X-Forwarded-Host.
584
+ *
585
+ * Each app gets its own `server { server_name ... }` block with
586
+ * individual SSL certificates (defaults to Let's Encrypt paths).
587
+ */
588
+ export function generatePlatformNginxConfig(options = {}) {
589
+ const { apps, services, hostMeta, defaultSsl, maxBodySize = 10, forgeProxy } = options;
590
+ const forgeProxyTarget = normalizeForgeProxyTargetConfig(forgeProxy, "platform mode");
591
+ // Validate all domains upfront
592
+ for (const app of Object.values(apps ?? {})) {
593
+ const domains = resolveAppDomains(app);
594
+ for (const d of domains) {
595
+ validateDomain(d);
596
+ }
597
+ }
598
+ let config = `# ForgePlatform nginx config — auto-generated
599
+ # Do not edit — regenerate with: forge platform generate
600
+
601
+ events {
602
+ worker_connections 16384;
603
+ }
604
+
605
+ http {
606
+ gzip on;
607
+ gzip_types text/plain text/css application/json application/javascript text/xml;
608
+
609
+ client_max_body_size ${maxBodySize}m;
610
+ client_body_timeout 15s;
611
+ send_timeout 30s;
612
+
613
+ `;
614
+ config += ` upstream forgeproxy {\n`;
615
+ config += ` server ${forgeProxyTarget.host}:${forgeProxyTarget.port};\n`;
616
+ config += ` keepalive ${forgeProxyTarget.keepalive};\n`;
617
+ config += ` }\n\n`;
618
+ // Collect all domains for HTTP->HTTPS redirect
619
+ const allDomains = [];
620
+ for (const app of Object.values(apps ?? {})) {
621
+ allDomains.push(...resolveAppDomains(app));
622
+ }
623
+ // Shared HTTP->HTTPS redirect block (only if any app has SSL)
624
+ const hasAnySsl = defaultSsl || Object.values(apps ?? {}).some((a) => a.ssl);
625
+ if (hasAnySsl && allDomains.length > 0) {
626
+ config += ` # HTTP → HTTPS redirect (all platform domains)\n`;
627
+ config += ` server {\n`;
628
+ config += ` listen 80;\n`;
629
+ config += ` server_name ${allDomains.join(" ")};\n`;
630
+ config += ` return 301 https://$host$request_uri;\n`;
631
+ config += ` }\n\n`;
632
+ }
633
+ // Find shared auth service (if any)
634
+ const authService = Object.entries(services ?? {}).find(([name, svc]) => (name === "auth" || name.endsWith(":auth")) && svc.type === ServiceType.EDGE);
635
+ // Generate a server block per app
636
+ for (const [appId, app] of Object.entries(apps ?? {})) {
637
+ const domains = resolveAppDomains(app);
638
+ if (domains.length === 0)
639
+ continue;
640
+ const meta = hostMeta?.[appId];
641
+ const projectServices = Object.entries(services ?? {}).filter(([name]) => meta?.services?.includes(name));
642
+ const edgeServices = projectServices.filter(([, s]) => s.type === ServiceType.EDGE);
643
+ config += ` # ── App: ${appId} ──\n`;
644
+ config += ` server {\n`;
645
+ const ssl = app.ssl ?? defaultSsl;
646
+ if (ssl) {
647
+ const certPath = ssl.cert ?? `/etc/letsencrypt/live/${domains[0]}/fullchain.pem`;
648
+ const keyPath = ssl.key ?? `/etc/letsencrypt/live/${domains[0]}/privkey.pem`;
649
+ config += ` listen 443 ssl http2;\n`;
650
+ config += ` server_name ${domains.join(" ")};\n`;
651
+ config += ` ssl_certificate ${certPath};\n`;
652
+ config += ` ssl_certificate_key ${keyPath};\n`;
653
+ config += ` ssl_protocols TLSv1.2 TLSv1.3;\n`;
654
+ }
655
+ else {
656
+ config += ` listen 80;\n`;
657
+ config += ` server_name ${domains.join(" ")};\n`;
658
+ }
659
+ config += `\n`;
660
+ config += ` # Security headers\n`;
661
+ config += ` add_header X-Frame-Options "SAMEORIGIN" always;\n`;
662
+ config += ` add_header X-Content-Type-Options "nosniff" always;\n`;
663
+ config += `\n`;
664
+ const rootEdgeServices = edgeServices.filter(([, svc]) => (svc.prefix ?? "/") === "/");
665
+ if (rootEdgeServices.length > 1) {
666
+ throw new Error(`App "${appId}" has multiple edge services mounted at "/". ` +
667
+ `Only one root edge prefix is supported per app.`);
668
+ }
669
+ const rootEdgeService = rootEdgeServices[0] ?? null;
670
+ const rootEdgeSafeName = rootEdgeService ? rootEdgeService[0].replace(/:/g, "_") : null;
671
+ let rootProxyViaNamedLocation = false;
672
+ let rootStaticMountDefined = false;
673
+ // Prefer explicit staticMounts (frontend plugin output), fallback to legacy static.
674
+ const staticMounts = Array.isArray(app.staticMounts) && app.staticMounts.length > 0
675
+ ? app.staticMounts
676
+ : app.static
677
+ ? [
678
+ {
679
+ dir: app.static,
680
+ basePath: "/static",
681
+ spaFallback: false,
682
+ cachePolicy: "short",
683
+ },
684
+ ]
685
+ : [];
686
+ for (const mount of staticMounts) {
687
+ const safeStatic = validateStaticDir(mount.dir, appId);
688
+ const basePath = normalizeMountBasePath(mount.basePath ?? "/");
689
+ const cacheControl = cacheControlFromPolicy(mount.cachePolicy);
690
+ const spaFallback = Boolean(mount.spaFallback);
691
+ if (basePath === "/") {
692
+ if (rootStaticMountDefined) {
693
+ throw new Error(`App "${appId}" defines multiple static mounts at "/"`);
694
+ }
695
+ rootStaticMountDefined = true;
696
+ config += ` location / {\n`;
697
+ config += ` root ${safeStatic};\n`;
698
+ if (rootEdgeSafeName) {
699
+ rootProxyViaNamedLocation = true;
700
+ if (spaFallback) {
701
+ config += ` try_files $uri $uri/ /index.html @forge_${rootEdgeSafeName}_root;\n`;
702
+ }
703
+ else {
704
+ config += ` try_files $uri $uri/ @forge_${rootEdgeSafeName}_root;\n`;
705
+ }
706
+ }
707
+ else {
708
+ if (spaFallback) {
709
+ config += ` try_files $uri $uri/ /index.html;\n`;
710
+ }
711
+ else {
712
+ config += ` try_files $uri $uri/ =404;\n`;
713
+ }
714
+ }
715
+ config += ` add_header Cache-Control "${cacheControl}";\n`;
716
+ config += ` add_header X-Content-Type-Options "nosniff" always;\n`;
717
+ config += ` }\n\n`;
718
+ }
719
+ else {
720
+ config += ` location ${basePath}/ {\n`;
721
+ config += ` alias ${safeStatic}/;\n`;
722
+ if (spaFallback) {
723
+ config += ` try_files $uri $uri/ ${basePath}/index.html;\n`;
724
+ }
725
+ else {
726
+ config += ` try_files $uri $uri/ =404;\n`;
727
+ }
728
+ config += ` add_header Cache-Control "${cacheControl}";\n`;
729
+ config += ` add_header X-Content-Type-Options "nosniff" always;\n`;
730
+ config += ` }\n\n`;
731
+ }
732
+ }
733
+ // Shared auth mount (if auth service exists and this isn't the auth service itself)
734
+ if (authService) {
735
+ config += ` location /auth/ {\n`;
736
+ config += ` proxy_pass http://forgeproxy;\n`;
737
+ config += ` proxy_http_version 1.1;\n`;
738
+ config += ` proxy_set_header Connection "";\n`;
739
+ config += ` proxy_set_header Host $host;\n`;
740
+ config += ` proxy_set_header X-Forwarded-Host $host;\n`;
741
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
742
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`;
743
+ config += ` proxy_set_header X-Forge-Project ${appId};\n`;
744
+ config += ` }\n\n`;
745
+ }
746
+ // App-specific edge service routes
747
+ for (const [svcName, svc] of edgeServices) {
748
+ const safeName = svcName.replace(/:/g, "_");
749
+ const prefix = svc.prefix ?? "/";
750
+ if (prefix === "/" && rootProxyViaNamedLocation) {
751
+ config += ` location @forge_${safeName}_root {\n`;
752
+ config += ` proxy_pass http://forgeproxy;\n`;
753
+ config += ` proxy_http_version 1.1;\n`;
754
+ config += ` proxy_set_header Connection "";\n`;
755
+ config += ` proxy_set_header Host $host;\n`;
756
+ config += ` proxy_set_header X-Real-IP $remote_addr;\n`;
757
+ config += ` proxy_set_header X-Forwarded-Host $host;\n`;
758
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
759
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`;
760
+ config += ` proxy_set_header X-Forge-Project ${appId};\n`;
761
+ config += ` }\n\n`;
762
+ continue;
763
+ }
764
+ config += ` location ${prefix} {\n`;
765
+ config += ` proxy_pass http://forgeproxy;\n`;
766
+ config += ` proxy_http_version 1.1;\n`;
767
+ config += ` proxy_set_header Connection "";\n`;
768
+ config += ` proxy_set_header Host $host;\n`;
769
+ config += ` proxy_set_header X-Real-IP $remote_addr;\n`;
770
+ config += ` proxy_set_header X-Forwarded-Host $host;\n`;
771
+ config += ` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n`;
772
+ config += ` proxy_set_header X-Forwarded-Proto $scheme;\n`;
773
+ config += ` proxy_set_header X-Forge-Project ${appId};\n`;
774
+ config += ` }\n\n`;
775
+ }
776
+ config += ` }\n\n`;
777
+ }
778
+ config += `}\n`;
779
+ return config;
780
+ }
781
+ //# sourceMappingURL=NginxGenerator.js.map