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