threadforge 0.1.1 → 0.2.2

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 +69 -42
  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 +79 -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,1169 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import net from "node:net";
4
+ import tls from "node:tls";
5
+ import { StaticMountRegistry } from "../frontend/StaticMountRegistry.js";
6
+ import { ForgeEndpoints, verifyJwt } from "./ForgeEndpoints.js";
7
+ import { ForgeWebSocket } from "./ForgeWebSocket.js";
8
+ import { IngressProtection } from "./Ingress.js";
9
+ import { Logger } from "./Logger.js";
10
+ // A10: Network utilities extracted to network-utils.js
11
+ import { isPrivateNetwork, isTrustedProxy } from "./network-utils.js";
12
+ import { PrometheusMetrics } from "./Prometheus.js";
13
+ import { RequestContext } from "./RequestContext.js";
14
+ import { Router } from "./Router.js";
15
+ import { StaticFileServer } from "./StaticFileServer.js";
16
+ import { WorkerChannelManager } from "./WorkerChannelManager.js";
17
+ import { RegistryMode, ServiceType, } from "./config-enums.js";
18
+ // Re-export for backward compatibility (A10)
19
+ export { isPrivateNetwork, isTrustedProxy };
20
+ const MAX_BODY_SIZE = 1_048_576; // 1MB
21
+ const VALID_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
22
+ // S11: Body timeout reduced from 30s to 10s
23
+ const BODY_TIMEOUT_MS = 10_000;
24
+ export const NOT_HANDLED = Symbol("NOT_HANDLED");
25
+ // Error messages for fatal bind errors
26
+ const BIND_ERROR_MESSAGES = {
27
+ EPERM: (port) => `Permission denied binding to port ${port}. Run with elevated privileges or use a port >= 1024.`,
28
+ EACCES: (port) => `Access denied binding to port ${port}. Run with elevated privileges or use a port >= 1024.`,
29
+ EADDRNOTAVAIL: (port) => `Cannot bind to port ${port}. Address not available (restricted environment or invalid configuration).`,
30
+ EADDRINUSE: (port) => `Port ${port} is already in use. Stop the conflicting process or choose a different port.`,
31
+ };
32
+ /**
33
+ * ForgeContext
34
+ *
35
+ * Injected into every Service instance. Provides:
36
+ * - HTTP router
37
+ * - IPC messaging helpers (direct worker-to-worker when available)
38
+ * - Metrics collection
39
+ * - Structured logger
40
+ * - Runtime metadata (service name, thread count, worker id, etc.)
41
+ */
42
+ export class ForgeContext {
43
+ serviceName;
44
+ port;
45
+ workerId;
46
+ threadCount;
47
+ mode;
48
+ serviceType;
49
+ router;
50
+ logger;
51
+ metrics;
52
+ channels;
53
+ ingress;
54
+ _sendIPC;
55
+ _localSend;
56
+ _localRequest;
57
+ _ingressConfig;
58
+ _ingressApplied;
59
+ _forgeProxy;
60
+ _serviceInstance;
61
+ _servicePorts;
62
+ _endpointResolver;
63
+ _staticMounts;
64
+ _staticRegistry;
65
+ _staticFileServer;
66
+ _forgeEndpoints;
67
+ _wsConnections;
68
+ _wsPerIpCounts;
69
+ _wsMaxPerIp;
70
+ _wsPerIpCleanupTimer;
71
+ _wsHandlers;
72
+ _wsPluginHooks;
73
+ _server;
74
+ _needsHttpServer;
75
+ _activeRequests;
76
+ _messageHandlers;
77
+ _onMessage;
78
+ _onRequest;
79
+ _projectId;
80
+ _projectSchema;
81
+ _projectKeyPrefix;
82
+ _emitEvent;
83
+ constructor(options) {
84
+ this.serviceName = options.serviceName;
85
+ this.port = options.port;
86
+ this.workerId = options.workerId;
87
+ this.threadCount = options.threadCount;
88
+ this.mode = options.mode;
89
+ this.serviceType = options.serviceType ?? ServiceType.INTERNAL;
90
+ this._sendIPC = options.sendIPC;
91
+ this._localSend = options.localSend ?? null;
92
+ this._localRequest = options.localRequest ?? null;
93
+ this._ingressConfig = options.ingress ?? {};
94
+ this._ingressApplied = false;
95
+ this._forgeProxy = options.forgeProxy ?? process.env.FORGE_PROXY_URL ?? null;
96
+ this._serviceInstance = null; // set by worker-bootstrap after service.onStart
97
+ // Port map for HTTP-based service calls: { serviceName: port }
98
+ try {
99
+ this._servicePorts = JSON.parse(process.env.FORGE_SERVICE_PORTS || "{}");
100
+ }
101
+ catch {
102
+ this._servicePorts = {};
103
+ }
104
+ this._endpointResolver = null;
105
+ this.router = new Router();
106
+ this.logger = new Logger(this.serviceName, this.workerId);
107
+ this.metrics = new PrometheusMetrics(this.serviceName, this.workerId);
108
+ this._staticMounts = options.staticMounts ?? [];
109
+ this._staticRegistry = new StaticMountRegistry(this._staticMounts);
110
+ this._staticFileServer = new StaticFileServer({
111
+ staticRegistry: this._staticRegistry,
112
+ logger: this.logger,
113
+ metrics: this.metrics,
114
+ });
115
+ this._forgeEndpoints = new ForgeEndpoints({
116
+ serviceName: this.serviceName,
117
+ logger: this.logger,
118
+ getServiceInstance: () => this._serviceInstance,
119
+ });
120
+ this._wsConnections = new Set();
121
+ this._wsPerIpCounts = new Map();
122
+ this._wsMaxPerIp = parseInt(process.env.FORGE_WS_MAX_PER_IP || "100", 10) || 100;
123
+ this._wsPerIpCleanupTimer = setInterval(() => {
124
+ if (this._wsPerIpCounts.size > 50_000) {
125
+ // Nuclear option: map exceeded 50K entries — clear entirely
126
+ this._wsPerIpCounts.clear();
127
+ return;
128
+ }
129
+ for (const [ip, count] of this._wsPerIpCounts) {
130
+ if (count <= 0)
131
+ this._wsPerIpCounts.delete(ip);
132
+ }
133
+ }, 60_000);
134
+ this._wsPerIpCleanupTimer.unref();
135
+ this._wsHandlers = new Map();
136
+ this._wsPluginHooks = [];
137
+ /**
138
+ * Direct channel manager — handles MessagePort connections
139
+ * to other services, bypassing the supervisor for all
140
+ * inter-service communication.
141
+ */
142
+ this.channels = new WorkerChannelManager(this.serviceName, this.workerId);
143
+ this.channels.init(this._sendIPC);
144
+ this._server = null;
145
+ // Compute once whether this service needs an HTTP server
146
+ const isEdge = this.serviceType === ServiceType.EDGE && this.port > 0;
147
+ const isMultiMachine = process.env.FORGE_REGISTRY_MODE && process.env.FORGE_REGISTRY_MODE !== RegistryMode.EMBEDDED;
148
+ let hasRemoteEndpoints = false;
149
+ try {
150
+ const eps = JSON.parse(process.env.FORGE_SERVICE_ENDPOINTS || "{}");
151
+ hasRemoteEndpoints = Object.values(eps).some((ep) => Array.isArray(ep) ? ep.some((e) => e.remote) : ep?.remote);
152
+ }
153
+ catch { }
154
+ this._needsHttpServer = isEdge || ((isMultiMachine || hasRemoteEndpoints) && this.port > 0);
155
+ this._activeRequests = 0;
156
+ this._messageHandlers = new Map();
157
+ }
158
+ setStaticMounts(mounts = []) {
159
+ this._staticMounts = mounts;
160
+ this._staticRegistry.setMounts(mounts);
161
+ }
162
+ /**
163
+ * Send a message to another service.
164
+ *
165
+ * Resolution order:
166
+ * 1. Local dispatch (colocated service in same process) — zero overhead
167
+ * 2. Direct UDS connection — bypasses supervisor
168
+ * 3. Supervisor IPC fallback — only during startup
169
+ */
170
+ async send(target, payload) {
171
+ // Try colocated first (direct function call, no serialization)
172
+ if (this._localSend?.(target, payload)) {
173
+ return;
174
+ }
175
+ // Fall through to UDS / supervisor IPC
176
+ this.channels.send(target, payload);
177
+ }
178
+ /**
179
+ * Broadcast to all workers of a target service.
180
+ * Note: channels.broadcast delivers to all workers including local via UDS.
181
+ * We only use _localSend for colocated services that share this process
182
+ * (no UDS needed), then broadcast to remote workers via channels.
183
+ */
184
+ async broadcast(target, payload) {
185
+ // Local colocated dispatch (same process, direct call)
186
+ this._localSend?.(target, payload);
187
+ // UDS broadcast to all OTHER workers (channels excludes self)
188
+ this.channels.broadcast(target, payload);
189
+ }
190
+ /**
191
+ * Send a request to another service and await a response.
192
+ *
193
+ * If the target is colocated, this is a direct async function call
194
+ * with zero serialization overhead.
195
+ */
196
+ async request(target, payload, timeoutMs = 5000) {
197
+ // Try colocated first
198
+ if (this._localRequest) {
199
+ const result = await this._localRequest(target, payload);
200
+ if (result !== NOT_HANDLED)
201
+ return result;
202
+ }
203
+ // Fall through to UDS / supervisor IPC
204
+ return this.channels.request(target, payload, timeoutMs);
205
+ }
206
+ /**
207
+ * Start the HTTP server for this service.
208
+ */
209
+ async startServer() {
210
+ // Auto-apply ingress protection for edge services (when no ForgeProxy)
211
+ if (this.serviceType === ServiceType.EDGE && !this._ingressApplied && !this._forgeProxy) {
212
+ this.ingress = new IngressProtection(this._ingressConfig ?? {});
213
+ this.router.use(this.ingress.middleware());
214
+ this._ingressApplied = true;
215
+ }
216
+ // ── S7: Prometheus metrics endpoint with optional auth ──
217
+ this.router.get("/metrics", (_req, res) => {
218
+ const metricsToken = process.env.FORGE_METRICS_TOKEN;
219
+ if (metricsToken) {
220
+ const auth = _req.headers.authorization;
221
+ const expected = `Bearer ${metricsToken}`;
222
+ if (!auth || auth.length !== expected.length || !timingSafeEqual(Buffer.from(auth), Buffer.from(expected))) {
223
+ res.json({ error: "Unauthorized" }, 401);
224
+ return;
225
+ }
226
+ }
227
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4; charset=utf-8" });
228
+ res.end(this.metrics.expose());
229
+ });
230
+ // ── Internal forge endpoints (/__forge/*) ──
231
+ this._forgeEndpoints.registerRoutes(this.router);
232
+ return new Promise((resolve, reject) => {
233
+ this._server = createServer((req, res) => {
234
+ const start = performance.now();
235
+ const forgeReq = req;
236
+ const forgeRes = res;
237
+ // H8: Validate HTTP method early
238
+ if (!VALID_METHODS.has(req.method)) {
239
+ res.writeHead(405, {
240
+ "Content-Type": "application/json",
241
+ Allow: "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS",
242
+ });
243
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
244
+ return;
245
+ }
246
+ // H11: Strip internal forge headers from external requests BEFORE body parsing
247
+ // L-SEC-1: Also strip internal signature/timestamp headers for defense-in-depth
248
+ const remoteAddrEarly = req.socket?.remoteAddress ?? "";
249
+ if (!isPrivateNetwork(remoteAddrEarly)) {
250
+ delete req.headers["x-forge-auth"];
251
+ delete req.headers["x-forge-tenant"];
252
+ delete req.headers["x-forge-user"];
253
+ delete req.headers["x-forge-deadline"];
254
+ delete req.headers["x-forge-internal-sig"];
255
+ delete req.headers["x-forge-internal-ts"];
256
+ }
257
+ if (["POST", "PUT", "PATCH"].includes(req.method)) {
258
+ const ct = (req.headers["content-type"] ?? "").toLowerCase();
259
+ if (ct &&
260
+ !ct.includes("application/json") &&
261
+ !ct.includes("text/") &&
262
+ !ct.includes("multipart/form-data") &&
263
+ !ct.includes("application/x-www-form-urlencoded")) {
264
+ res.writeHead(415, { "Content-Type": "application/json" });
265
+ res.end(JSON.stringify({ error: "Unsupported media type" }));
266
+ return;
267
+ }
268
+ // Early Content-Length check — reject oversized requests without reading body
269
+ const declaredLength = req.headers["content-length"];
270
+ if (declaredLength != null) {
271
+ const cl = parseInt(declaredLength, 10);
272
+ if (!Number.isNaN(cl) && cl > MAX_BODY_SIZE) {
273
+ res.writeHead(413, { "Content-Type": "application/json" });
274
+ res.end(JSON.stringify({ error: "Request body too large" }));
275
+ req.destroy();
276
+ return;
277
+ }
278
+ }
279
+ // P7: Collect chunks in array, single Buffer.concat at end
280
+ const chunks = [];
281
+ let bodySize = 0;
282
+ let rejected = false;
283
+ // S11: Reduced body timeout from 30s to 10s
284
+ const bodyTimeout = setTimeout(() => {
285
+ if (rejected)
286
+ return;
287
+ rejected = true;
288
+ if (!res.headersSent) {
289
+ res.writeHead(408, { "Content-Type": "application/json" });
290
+ res.end(JSON.stringify({ error: "Request Timeout" }));
291
+ }
292
+ req.destroy();
293
+ }, BODY_TIMEOUT_MS);
294
+ req.on("data", (chunk) => {
295
+ if (rejected)
296
+ return;
297
+ bodySize += chunk.length;
298
+ if (bodySize > MAX_BODY_SIZE) {
299
+ rejected = true;
300
+ clearTimeout(bodyTimeout);
301
+ if (!res.headersSent) {
302
+ res.writeHead(413, { "Content-Type": "application/json" });
303
+ res.end(JSON.stringify({ error: "Request body too large" }));
304
+ }
305
+ req.destroy();
306
+ return;
307
+ }
308
+ chunks.push(chunk);
309
+ });
310
+ req.on("end", () => {
311
+ clearTimeout(bodyTimeout);
312
+ if (rejected)
313
+ return;
314
+ const body = Buffer.concat(chunks).toString("utf-8");
315
+ // Only JSON.parse when content-type is application/json or absent (backward compat)
316
+ if (!ct || ct.includes("application/json")) {
317
+ try {
318
+ forgeReq.body = body
319
+ ? JSON.parse(body, (key, value) => {
320
+ if (key === "__proto__" || key === "constructor" || key === "prototype")
321
+ return undefined;
322
+ return value;
323
+ })
324
+ : {};
325
+ }
326
+ catch {
327
+ // For internal forge endpoints, return 400 on malformed JSON
328
+ const urlPath = new URL(req.url, "http://localhost").pathname;
329
+ if (urlPath.startsWith("/__forge/")) {
330
+ if (!res.headersSent) {
331
+ res.writeHead(400, { "Content-Type": "application/json" });
332
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
333
+ }
334
+ return;
335
+ }
336
+ // Fall back to raw string for user routes
337
+ forgeReq.body = body;
338
+ }
339
+ }
340
+ else {
341
+ forgeReq.body = body;
342
+ }
343
+ this._handleRequest(forgeReq, forgeRes, start);
344
+ });
345
+ }
346
+ else {
347
+ forgeReq.body = {};
348
+ this._handleRequest(forgeReq, forgeRes, start);
349
+ }
350
+ });
351
+ // ── Client error handling ──
352
+ this._server.on("clientError", (err, socket) => {
353
+ this.logger.error("Client error", { error: err.message, code: err.code });
354
+ if (socket && !socket.destroyed) {
355
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
356
+ socket.destroy();
357
+ }
358
+ });
359
+ // ── WebSocket upgrade handling ──
360
+ this._server.on("upgrade", (req, socket, head) => {
361
+ this._handleWsUpgrade(req, socket, head).catch((err) => {
362
+ this.logger.error("WebSocket upgrade failed", { error: err.message, path: req.url });
363
+ if (!socket.destroyed) {
364
+ try {
365
+ socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n");
366
+ }
367
+ catch { }
368
+ socket.destroy();
369
+ }
370
+ });
371
+ });
372
+ // Set request timeouts to prevent slowloris attacks
373
+ this._server.timeout = 30000;
374
+ this._server.requestTimeout = 30000;
375
+ this._server.headersTimeout = 10000;
376
+ this._server.listen(this.port, () => {
377
+ // Reduce startup noise in multi-worker mode.
378
+ if (this.workerId === 0) {
379
+ this.logger.info(`Listening on port ${this.port}`);
380
+ }
381
+ // Auto-register with ForgeProxy if configured
382
+ if (this._forgeProxy) {
383
+ this._registerWithForgeProxy().catch((err) => {
384
+ this.logger.warn(`ForgeProxy registration failed: ${err.message}`);
385
+ });
386
+ }
387
+ resolve();
388
+ });
389
+ this._server.on("error", (err) => {
390
+ // Detect fatal bind errors that should not trigger restart loops
391
+ const isFatalBindError = err.code === "EPERM" || err.code === "EACCES" || err.code === "EADDRNOTAVAIL" || err.code === "EADDRINUSE";
392
+ if (isFatalBindError) {
393
+ // Notify supervisor this is a fatal error — don't restart
394
+ if (this._sendIPC) {
395
+ this._sendIPC({
396
+ type: "forge:fatal-error",
397
+ error: err.code,
398
+ message: err.message,
399
+ port: this.port,
400
+ });
401
+ }
402
+ // Augment error with clear message using constant map
403
+ const messageGenerator = BIND_ERROR_MESSAGES[err.code];
404
+ err.fatalBindError = true;
405
+ err.userMessage = messageGenerator
406
+ ? messageGenerator(this.port)
407
+ : `Failed to bind to port ${this.port}: ${err.message}`;
408
+ }
409
+ reject(err);
410
+ });
411
+ });
412
+ }
413
+ /**
414
+ * Route an incoming HTTP request.
415
+ * Creates a RequestContext that flows through the entire call chain.
416
+ */
417
+ _handleRequest(req, res, start) {
418
+ this._activeRequests++;
419
+ res._forgeTracked = true;
420
+ res.once("close", () => {
421
+ if (res._forgeTracked) {
422
+ this._activeRequests--;
423
+ res._forgeTracked = false;
424
+ }
425
+ });
426
+ // Convenience methods on response
427
+ res.json = (data, statusCode = 200) => {
428
+ if (res.headersSent)
429
+ return;
430
+ let body;
431
+ try {
432
+ body = JSON.stringify(data ?? null);
433
+ }
434
+ catch {
435
+ body = JSON.stringify({ error: "Response serialization failed" });
436
+ statusCode = 500;
437
+ }
438
+ const len = Buffer.byteLength(body);
439
+ res.writeHead(statusCode, { "Content-Type": "application/json", "Content-Length": len });
440
+ res.end(body);
441
+ };
442
+ res.status = (code) => {
443
+ res.statusCode = code;
444
+ return res;
445
+ };
446
+ // HTTP-M5: Streaming response support — sets Transfer-Encoding: chunked
447
+ // Handlers can call res.stream() then use res.write() / res.end() for large payloads.
448
+ res.stream = (statusCode = 200, headers = {}) => {
449
+ if (!res.headersSent) {
450
+ res.writeHead(statusCode, {
451
+ "Transfer-Encoding": "chunked",
452
+ ...headers,
453
+ });
454
+ }
455
+ return res;
456
+ };
457
+ // ── Build RequestContext from incoming headers ──
458
+ // Note: internal forge headers already stripped in createServer callback (H11)
459
+ const remoteAddr = req.socket?.remoteAddress ?? "";
460
+ const rctx = RequestContext.fromPropagation(req.headers);
461
+ rctx.service = this.serviceName;
462
+ // Security headers
463
+ res.setHeader("X-Frame-Options", "DENY");
464
+ res.setHeader("X-Content-Type-Options", "nosniff");
465
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
466
+ // HSTS should only be sent over HTTPS (RFC 6797 § 7.2)
467
+ // Only trust x-forwarded-proto from trusted proxies (FORGE_TRUSTED_PROXIES), not just any private network
468
+ if (req.socket?.encrypted ||
469
+ (isTrustedProxy(remoteAddr) && req.headers["x-forwarded-proto"] === "https")) {
470
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
471
+ }
472
+ // Always set the correlation ID response header
473
+ res.setHeader("x-correlation-id", rctx.correlationId);
474
+ res.setHeader("x-trace-id", rctx.traceId);
475
+ // In host mode, propagate project ID to the request context
476
+ rctx._projectId ??= this._projectId;
477
+ // Attach context to request for handlers to use
478
+ req.ctx = rctx;
479
+ req.auth = rctx.auth;
480
+ req.tenantId = rctx.tenantId;
481
+ req.projectId = rctx.projectId;
482
+ req.userId = rctx.userId;
483
+ req.correlationId = rctx.correlationId;
484
+ this.metrics.httpRequestStart();
485
+ const matched = this.router.match(req.method, req.url);
486
+ if (!matched) {
487
+ this._staticFileServer
488
+ .tryServeStatic(req, res, start)
489
+ .then((served) => {
490
+ if (served)
491
+ return;
492
+ this.metrics.httpRequestEnd((performance.now() - start) / 1000, {
493
+ method: req.method,
494
+ route: "unmatched",
495
+ status: 404,
496
+ });
497
+ res.json({ error: "Not Found" }, 404);
498
+ })
499
+ .catch((err) => {
500
+ this.logger.error("Static serve failure", {
501
+ error: err.message,
502
+ url: req.url,
503
+ });
504
+ if (!res.headersSent) {
505
+ res.json({ error: "Internal server error" }, 500);
506
+ }
507
+ });
508
+ return;
509
+ }
510
+ req.params = matched.params;
511
+ req.query = matched.query;
512
+ // HEAD responses MUST NOT include a message body (RFC 7231 § 4.3.2)
513
+ if (req.method === "HEAD") {
514
+ let headContentLength = 0;
515
+ const origEnd = res.end.bind(res);
516
+ res.end = (_chunk, encoding, callback) => {
517
+ // Account for any final chunk passed to end()
518
+ if (_chunk != null) {
519
+ headContentLength += Buffer.byteLength(_chunk, typeof encoding === "string" ? encoding : undefined);
520
+ }
521
+ if (!res.headersSent && headContentLength > 0 && !res.getHeader("content-length")) {
522
+ res.setHeader("Content-Length", headContentLength);
523
+ }
524
+ return origEnd(null, encoding, callback);
525
+ };
526
+ res.write = (chunk, encodingOrCb, cb) => {
527
+ if (chunk != null) {
528
+ headContentLength += Buffer.byteLength(chunk, typeof encodingOrCb === "string" ? encodingOrCb : undefined);
529
+ }
530
+ if (typeof encodingOrCb === "function")
531
+ encodingOrCb(null);
532
+ else if (typeof cb === "function")
533
+ cb(null);
534
+ return true;
535
+ };
536
+ }
537
+ // Execute middleware chain + handler INSIDE RequestContext
538
+ const handlers = [...this.router.middleware, matched.handler];
539
+ let i = 0;
540
+ const next = (err) => {
541
+ if (err) {
542
+ this.logger.error("Request error", {
543
+ error: err.message,
544
+ url: req.url,
545
+ ...rctx.toLogFields(),
546
+ });
547
+ if (!res.headersSent) {
548
+ const status = err.statusCode || 500;
549
+ const message = status >= 500 ? "Internal server error" : err.message || "Internal error";
550
+ res.json({ error: message }, status);
551
+ }
552
+ return;
553
+ }
554
+ const handler = handlers[i++];
555
+ if (!handler)
556
+ return;
557
+ let stepCalled = false;
558
+ const stepNext = (stepErr) => {
559
+ if (stepCalled) {
560
+ if (process.env.NODE_ENV !== "production") {
561
+ process.stderr.write(`${JSON.stringify({
562
+ timestamp: new Date().toISOString(),
563
+ level: "warn",
564
+ message: `Middleware called next() more than once (handler index ${i - 1}). This is likely a bug.`,
565
+ })}\n`);
566
+ }
567
+ return;
568
+ }
569
+ stepCalled = true;
570
+ next(stepErr);
571
+ };
572
+ try {
573
+ const result = handler(req, res, stepNext);
574
+ if (result && typeof result.catch === "function") {
575
+ result.catch(stepNext);
576
+ }
577
+ }
578
+ catch (e) {
579
+ stepNext(e);
580
+ }
581
+ };
582
+ // Run entire handler chain within the RequestContext.
583
+ // AsyncLocalStorage.run() properly propagates through the entire async chain,
584
+ // including middleware that does `await next()` followed by post-next work.
585
+ RequestContext.run(rctx, () => next());
586
+ // Track metrics on response finish
587
+ const onFinish = () => {
588
+ const duration = (performance.now() - start) / 1000;
589
+ const labels = {
590
+ method: req.method,
591
+ route_pattern: matched.pattern,
592
+ status: res.statusCode,
593
+ };
594
+ this.metrics.httpRequestEnd(duration, labels);
595
+ res.removeListener("finish", onFinish);
596
+ };
597
+ res.on("finish", onFinish);
598
+ }
599
+ /**
600
+ * Wire message/request handlers to both the direct channel manager
601
+ * AND the supervisor fallback IPC path.
602
+ */
603
+ _wireMessageHandlers() {
604
+ // Direct channel path (primary — used once ports are established)
605
+ this.channels.onMessage = (from, payload) => {
606
+ if (this._onMessage)
607
+ this._onMessage(from, payload);
608
+ };
609
+ this.channels.onRequest = (from, payload) => {
610
+ if (this._onRequest)
611
+ return this._onRequest(from, payload);
612
+ return null;
613
+ };
614
+ }
615
+ /**
616
+ * Handle an incoming IPC message from the supervisor.
617
+ * This is the FALLBACK path — only used during startup before
618
+ * direct MessagePorts are established, and for supervisor-level
619
+ * commands (health checks, shutdown, etc.)
620
+ */
621
+ _handleIPCMessage(msg) {
622
+ if (!msg || !msg.type)
623
+ return;
624
+ // Supervisor-level messages
625
+ if (msg.type === "forge:health-check") {
626
+ this._sendIPC({
627
+ type: "forge:health-response",
628
+ timestamp: msg.timestamp,
629
+ uptime: process.uptime(),
630
+ memory: process.memoryUsage(),
631
+ pid: process.pid,
632
+ });
633
+ return;
634
+ }
635
+ // Fallback message routing (before direct ports are ready)
636
+ if (msg.type === "forge:message" && this._onMessage) {
637
+ this._onMessage(msg.from, msg.payload);
638
+ }
639
+ if (msg.type === "forge:request" && this._onRequest) {
640
+ Promise.resolve(this._onRequest(msg.from, msg.payload))
641
+ .then((result) => {
642
+ this._sendIPC({
643
+ type: "forge:response",
644
+ requestId: msg.requestId,
645
+ payload: result,
646
+ error: null,
647
+ });
648
+ })
649
+ .catch((err) => {
650
+ this._sendIPC({
651
+ type: "forge:response",
652
+ requestId: msg.requestId,
653
+ payload: null,
654
+ error: err.message,
655
+ });
656
+ });
657
+ }
658
+ if (msg.type === "forge:response") {
659
+ const handler = this._messageHandlers.get(msg.requestId);
660
+ if (handler)
661
+ handler(msg);
662
+ }
663
+ }
664
+ _isMethodAllowed(method) {
665
+ return this._forgeEndpoints.isMethodAllowed(method);
666
+ }
667
+ async _executeForgeEndpoint(res, rctx, handler, method, { args, logPrefix }) {
668
+ return this._forgeEndpoints.execute(res, rctx, handler, method, { args, logPrefix });
669
+ }
670
+ /**
671
+ * Gracefully shut down.
672
+ */
673
+ async stop() {
674
+ // M-13: Stop IngressProtection timers if active
675
+ if (this.ingress) {
676
+ this.ingress.stop();
677
+ }
678
+ // Close direct channels
679
+ this.channels.destroy();
680
+ // Close all active WebSocket connections and clean up timers
681
+ for (const ws of this._wsConnections) {
682
+ if (ws._closeTimer) {
683
+ clearTimeout(ws._closeTimer);
684
+ ws._closeTimer = null;
685
+ }
686
+ if (ws._pingTimer) {
687
+ clearInterval(ws._pingTimer);
688
+ ws._pingTimer = null;
689
+ }
690
+ if (!ws._closed) {
691
+ ws._closed = true;
692
+ }
693
+ if (!ws.socket.destroyed) {
694
+ ws.socket.destroy();
695
+ }
696
+ }
697
+ this._wsConnections.clear();
698
+ if (this._server) {
699
+ // Deregister from ForgeProxy on shutdown
700
+ if (this._forgeProxy) {
701
+ try {
702
+ const data = JSON.stringify({
703
+ service: this.serviceName,
704
+ host: this._getHost(),
705
+ port: this.port,
706
+ });
707
+ const url = new URL(`${this._forgeProxy}/deregister`);
708
+ await fetch(url, { method: "POST", body: data, headers: { "Content-Type": "application/json" } }).catch(() => { });
709
+ }
710
+ catch { }
711
+ }
712
+ return new Promise((resolve) => {
713
+ let drainInterval = null;
714
+ // Stop accepting new connections and terminate idle keep-alive sockets
715
+ this._server.close(() => {
716
+ if (drainInterval)
717
+ clearInterval(drainInterval);
718
+ resolve();
719
+ });
720
+ // Force-close idle keep-alive connections (Node 18.2+)
721
+ if (typeof this._server.closeAllConnections === "function") {
722
+ // Wait briefly for in-flight requests, then force-close
723
+ const drainStart = Date.now();
724
+ drainInterval = setInterval(() => {
725
+ if (this._activeRequests <= 0 || Date.now() - drainStart >= 5000) {
726
+ clearInterval(drainInterval);
727
+ drainInterval = null;
728
+ this._server.closeAllConnections();
729
+ }
730
+ }, 50);
731
+ drainInterval.unref();
732
+ }
733
+ });
734
+ }
735
+ }
736
+ // ── WebSocket Support ──
737
+ /**
738
+ * Register a WebSocket handler for a path.
739
+ *
740
+ * ctx.ws('/ws', (socket, req) => {
741
+ * socket.on('message', (data) => {
742
+ * const msg = JSON.parse(data);
743
+ * // req.ctx has the RequestContext with auth, correlationId
744
+ * socket.send(JSON.stringify({ echo: msg }));
745
+ * });
746
+ * });
747
+ */
748
+ ws(path, handlerOrOptions, maybeHandler) {
749
+ let handler;
750
+ let options;
751
+ if (typeof handlerOrOptions === "function") {
752
+ handler = handlerOrOptions;
753
+ options = {};
754
+ }
755
+ else {
756
+ options = handlerOrOptions ?? {};
757
+ handler = maybeHandler;
758
+ }
759
+ // S8: WebSocket endpoints require auth by default
760
+ if (options.auth === undefined) {
761
+ options.auth = "required";
762
+ }
763
+ this._wsHandlers.set(path, { handler, options });
764
+ }
765
+ /**
766
+ * Write a short HTTP response over a raw upgrade socket and close it.
767
+ */
768
+ _writeWsUpgradeError(socket, statusCode, reason, headers = {}) {
769
+ const statusText = String(reason || "Upgrade Rejected").replace(/[\r\n]/g, " ");
770
+ let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`;
771
+ for (const [key, value] of Object.entries(headers)) {
772
+ const safeKey = String(key).replace(/[\r\n:]/g, "");
773
+ const safeVal = String(value).replace(/[\r\n]/g, " ");
774
+ response += `${safeKey}: ${safeVal}\r\n`;
775
+ }
776
+ response += "\r\n";
777
+ socket.write(response);
778
+ socket.destroy();
779
+ }
780
+ /**
781
+ * Run websocket plugin hooks for a lifecycle stage.
782
+ */
783
+ async _runWsPluginHooks(stage, payload) {
784
+ const methodByStage = {
785
+ upgrade: "onWsUpgrade",
786
+ connect: "onWsConnect",
787
+ message: "onWsMessage",
788
+ close: "onWsClose",
789
+ };
790
+ const method = methodByStage[stage];
791
+ if (!method)
792
+ return [];
793
+ const results = [];
794
+ for (const hook of this._wsPluginHooks) {
795
+ const fn = hook?.[method];
796
+ if (typeof fn !== "function")
797
+ continue;
798
+ try {
799
+ const result = await fn(payload);
800
+ results.push({ plugin: hook.name, ok: true, result });
801
+ }
802
+ catch (err) {
803
+ this.logger.warn(`WebSocket plugin hook failed`, {
804
+ plugin: hook.name,
805
+ stage,
806
+ error: err.message,
807
+ });
808
+ results.push({ plugin: hook.name, ok: false, error: err });
809
+ }
810
+ }
811
+ return results;
812
+ }
813
+ /**
814
+ * Handle HTTP→WebSocket upgrade.
815
+ * Implements RFC 6455 handshake without external dependencies.
816
+ */
817
+ async _handleWsUpgrade(req, socket, head) {
818
+ // H-SEC-2: Per-IP WebSocket connection limit to prevent file descriptor exhaustion
819
+ // HTTP-M4: Use X-Forwarded-For from trusted proxies for accurate per-IP tracking
820
+ let wsRemoteAddr = req.socket?.remoteAddress ?? "unknown";
821
+ const rawAddr = req.socket?.remoteAddress;
822
+ if (rawAddr && (process.env.FORGE_TRUSTED_PROXIES ? isTrustedProxy(rawAddr) : isPrivateNetwork(rawAddr))) {
823
+ const xff = req.headers["x-forwarded-for"];
824
+ if (xff) {
825
+ const clientIp = (Array.isArray(xff) ? xff[0] : xff).split(",")[0]?.trim();
826
+ if (clientIp)
827
+ wsRemoteAddr = clientIp;
828
+ }
829
+ }
830
+ const currentCount = this._wsPerIpCounts.get(wsRemoteAddr) ?? 0;
831
+ if (currentCount >= this._wsMaxPerIp) {
832
+ socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
833
+ socket.destroy();
834
+ return;
835
+ }
836
+ const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
837
+ const entry = this._wsHandlers.get(url.pathname);
838
+ if (!entry) {
839
+ const proxied = await this._proxyWsUpgradeToDevServer(req, socket, head, url);
840
+ if (proxied)
841
+ return;
842
+ socket.destroy();
843
+ return;
844
+ }
845
+ const handler = typeof entry === "function" ? entry : entry.handler;
846
+ const wsOptions = typeof entry === "function" ? {} : (entry.options ?? {});
847
+ // S3: WebSocket Origin Validation — default deny
848
+ const allowedOrigins = wsOptions.allowedOrigins;
849
+ if (allowedOrigins && allowedOrigins.length > 0) {
850
+ // Explicit allowlist: check if '*' is included (opt-out of security)
851
+ if (!allowedOrigins.includes("*")) {
852
+ const origin = req.headers.origin;
853
+ if (!origin || !allowedOrigins.includes(origin)) {
854
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
855
+ socket.destroy();
856
+ return;
857
+ }
858
+ }
859
+ }
860
+ else {
861
+ // No allowedOrigins configured — default deny (reject cross-origin)
862
+ const origin = req.headers.origin;
863
+ if (origin) {
864
+ // Compare origin host to request host
865
+ try {
866
+ const originHost = new URL(origin).host;
867
+ const reqHost = req.headers.host?.split(":")[0];
868
+ if (originHost !== reqHost && originHost !== req.headers.host) {
869
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
870
+ socket.destroy();
871
+ return;
872
+ }
873
+ }
874
+ catch {
875
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
876
+ socket.destroy();
877
+ return;
878
+ }
879
+ }
880
+ }
881
+ // Strip internal forge headers from external WebSocket upgrades
882
+ let headers = req.headers;
883
+ const remoteAddr = req.socket?.remoteAddress ?? "";
884
+ if (!isPrivateNetwork(remoteAddr)) {
885
+ headers = { ...req.headers };
886
+ delete headers["x-forge-auth"];
887
+ delete headers["x-forge-tenant"];
888
+ delete headers["x-forge-user"];
889
+ delete headers["x-forge-deadline"];
890
+ }
891
+ const rctx = RequestContext.fromPropagation(headers);
892
+ rctx.service = this.serviceName;
893
+ rctx.method = `ws:${url.pathname}`;
894
+ req.ctx = rctx;
895
+ req.auth = rctx.auth;
896
+ req.tenantId = rctx.tenantId;
897
+ // S8: Enforce auth by default (auth defaults to 'required' from ws() method)
898
+ if (wsOptions.auth === "required") {
899
+ if (!rctx.auth) {
900
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
901
+ socket.destroy();
902
+ return;
903
+ }
904
+ // M-3 Security: When JWT_SECRET is set, verify the JWT signature — not just auth presence
905
+ if (process.env.JWT_SECRET) {
906
+ const rawToken = headers["x-forge-auth"] ??
907
+ headers.authorization ??
908
+ url.searchParams.get("token");
909
+ const verified = rawToken ? verifyJwt(rawToken) : null;
910
+ if (!verified) {
911
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
912
+ socket.destroy();
913
+ return;
914
+ }
915
+ }
916
+ }
917
+ const wsMeta = {
918
+ service: this.serviceName,
919
+ workerId: this.workerId,
920
+ path: url.pathname,
921
+ options: wsOptions,
922
+ requestContext: rctx,
923
+ };
924
+ const upgradeResults = await this._runWsPluginHooks("upgrade", {
925
+ req,
926
+ socket,
927
+ meta: wsMeta,
928
+ });
929
+ for (const res of upgradeResults) {
930
+ if (!res.ok) {
931
+ this._writeWsUpgradeError(socket, 503, "Service Unavailable");
932
+ return;
933
+ }
934
+ const decision = res.result;
935
+ if (decision === false || (decision && decision.allow === false)) {
936
+ this._writeWsUpgradeError(socket, decision && typeof decision === "object" ? (decision.statusCode ?? 403) : 403, decision && typeof decision === "object" ? (decision.reason ?? "Forbidden") : "Forbidden", decision && typeof decision === "object" ? (decision.headers ?? {}) : {});
937
+ return;
938
+ }
939
+ }
940
+ // M6: Validate Sec-WebSocket-Version (must be 13 per RFC 6455)
941
+ if (req.headers["sec-websocket-version"] !== "13") {
942
+ socket.write("HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\n\r\n");
943
+ socket.destroy();
944
+ return;
945
+ }
946
+ // RFC 6455 handshake
947
+ const key = req.headers["sec-websocket-key"];
948
+ if (!key) {
949
+ socket.destroy();
950
+ return;
951
+ }
952
+ const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-5AB4085B9976`).digest("base64");
953
+ const safeCorrelationId = (rctx.correlationId || "").replace(/[\r\n]/g, "");
954
+ socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
955
+ "Upgrade: websocket\r\n" +
956
+ "Connection: Upgrade\r\n" +
957
+ `Sec-WebSocket-Accept: ${accept}\r\n` +
958
+ `X-Correlation-ID: ${safeCorrelationId}\r\n` +
959
+ "\r\n");
960
+ // Create a minimal WebSocket wrapper
961
+ const ws = new ForgeWebSocket(socket, rctx, wsOptions);
962
+ // H1: If _head buffer has data, prepend it so the first frame isn't lost
963
+ if (head && head.length > 0) {
964
+ ws._onData(head);
965
+ }
966
+ this._wsConnections.add(ws);
967
+ this.metrics.wsConnectionOpen();
968
+ // H-SEC-2: Track per-IP count
969
+ this._wsPerIpCounts.set(wsRemoteAddr, (this._wsPerIpCounts.get(wsRemoteAddr) ?? 0) + 1);
970
+ ws.on("close", () => {
971
+ this._wsConnections.delete(ws);
972
+ this.metrics.wsConnectionClose();
973
+ // H-SEC-2: Decrement per-IP count
974
+ const count = (this._wsPerIpCounts.get(wsRemoteAddr) ?? 1) - 1;
975
+ if (count <= 0) {
976
+ this._wsPerIpCounts.delete(wsRemoteAddr);
977
+ }
978
+ else {
979
+ this._wsPerIpCounts.set(wsRemoteAddr, count);
980
+ }
981
+ });
982
+ ws.on("message", () => {
983
+ this.metrics.wsMessage("inbound");
984
+ });
985
+ ws.on("message", (message) => {
986
+ void this._runWsPluginHooks("message", {
987
+ ws,
988
+ req,
989
+ data: message,
990
+ meta: wsMeta,
991
+ });
992
+ });
993
+ ws.on("close", (code, reason) => {
994
+ void this._runWsPluginHooks("close", {
995
+ ws,
996
+ req,
997
+ code,
998
+ reason,
999
+ meta: wsMeta,
1000
+ });
1001
+ });
1002
+ const connectResults = await this._runWsPluginHooks("connect", {
1003
+ ws,
1004
+ req,
1005
+ meta: wsMeta,
1006
+ });
1007
+ for (const res of connectResults) {
1008
+ if (!res.ok) {
1009
+ try {
1010
+ ws.close?.(1011, "Internal plugin error");
1011
+ }
1012
+ catch { }
1013
+ return;
1014
+ }
1015
+ const decision = res.result;
1016
+ if (decision === false || (decision && decision.allow === false)) {
1017
+ try {
1018
+ ws.close?.(decision && typeof decision === "object" ? (decision.closeCode ?? 1008) : 1008, decision && typeof decision === "object"
1019
+ ? (decision.closeReason ?? "Policy violation")
1020
+ : "Policy violation");
1021
+ }
1022
+ catch { }
1023
+ return;
1024
+ }
1025
+ }
1026
+ // Run handler within RequestContext
1027
+ RequestContext.run(rctx, () => handler(ws, req));
1028
+ }
1029
+ async _proxyWsUpgradeToDevServer(req, socket, head, url) {
1030
+ const mount = this._staticFileServer.resolveMountForRequest(req, url.pathname);
1031
+ if (!mount?.devProxyTarget)
1032
+ return false;
1033
+ let target;
1034
+ try {
1035
+ target = new URL(mount.devProxyTarget);
1036
+ }
1037
+ catch (err) {
1038
+ this.logger.warn("Invalid frontend dev proxy target for websocket upgrade", {
1039
+ siteId: mount.siteId,
1040
+ proxyTarget: mount.devProxyTarget,
1041
+ error: err.message,
1042
+ });
1043
+ return false;
1044
+ }
1045
+ const isTls = target.protocol === "https:" || target.protocol === "wss:";
1046
+ const isTcp = target.protocol === "http:" || target.protocol === "ws:";
1047
+ if (!isTls && !isTcp) {
1048
+ this.logger.warn("Unsupported websocket dev proxy protocol", {
1049
+ siteId: mount.siteId,
1050
+ proxyTarget: mount.devProxyTarget,
1051
+ protocol: target.protocol,
1052
+ });
1053
+ return false;
1054
+ }
1055
+ const targetPort = target.port ? Number.parseInt(target.port, 10) : isTls ? 443 : 80;
1056
+ const upstream = isTls
1057
+ ? tls.connect({ host: target.hostname, port: targetPort })
1058
+ : net.connect({ host: target.hostname, port: targetPort });
1059
+ const forwardedHeaders = [];
1060
+ const rawHeaders = Array.isArray(req.rawHeaders) ? req.rawHeaders : [];
1061
+ for (let i = 0; i < rawHeaders.length; i += 2) {
1062
+ const key = rawHeaders[i];
1063
+ const value = rawHeaders[i + 1];
1064
+ const keyLower = String(key ?? "").toLowerCase();
1065
+ if (!keyLower)
1066
+ continue;
1067
+ if (keyLower === "host")
1068
+ continue;
1069
+ if (keyLower.startsWith("x-forge-"))
1070
+ continue;
1071
+ forwardedHeaders.push([String(key), String(value ?? "")]);
1072
+ }
1073
+ forwardedHeaders.push(["Host", target.host]);
1074
+ if (req.headers.host) {
1075
+ forwardedHeaders.push(["X-Forwarded-Host", String(req.headers.host)]);
1076
+ }
1077
+ forwardedHeaders.push(["X-Forwarded-Proto", "http"]);
1078
+ const requestLine = `${req.method} ${url.pathname}${url.search} HTTP/${req.httpVersion}\r\n`;
1079
+ const headerLines = forwardedHeaders.map(([k, v]) => `${k}: ${v}`).join("\r\n");
1080
+ const upgradePayload = `${requestLine}${headerLines}\r\n\r\n`;
1081
+ const connectEvent = isTls ? "secureConnect" : "connect";
1082
+ return await new Promise((resolve) => {
1083
+ let settled = false;
1084
+ const finish = (value) => {
1085
+ if (settled)
1086
+ return;
1087
+ settled = true;
1088
+ resolve(value);
1089
+ };
1090
+ const fail = (statusCode, statusText, err = null) => {
1091
+ if (err) {
1092
+ this.logger.warn("Frontend websocket dev proxy failed", {
1093
+ siteId: mount.siteId,
1094
+ proxyTarget: mount.devProxyTarget,
1095
+ error: err.message,
1096
+ });
1097
+ }
1098
+ if (!socket.destroyed) {
1099
+ try {
1100
+ socket.write(`HTTP/1.1 ${statusCode} ${statusText}\r\n\r\n`);
1101
+ }
1102
+ catch { }
1103
+ socket.destroy();
1104
+ }
1105
+ if (!upstream.destroyed)
1106
+ upstream.destroy();
1107
+ finish(true);
1108
+ };
1109
+ socket.once("error", (err) => fail(502, "Bad Gateway", err));
1110
+ socket.once("close", () => {
1111
+ if (!upstream.destroyed)
1112
+ upstream.destroy();
1113
+ });
1114
+ upstream.once("error", (err) => fail(502, "Bad Gateway", err));
1115
+ upstream.once("close", () => {
1116
+ if (!socket.destroyed)
1117
+ socket.destroy();
1118
+ });
1119
+ upstream.once(connectEvent, () => {
1120
+ try {
1121
+ upstream.write(upgradePayload);
1122
+ if (head && head.length > 0) {
1123
+ upstream.write(head);
1124
+ }
1125
+ socket.pipe(upstream);
1126
+ upstream.pipe(socket);
1127
+ finish(true);
1128
+ }
1129
+ catch (err) {
1130
+ fail(502, "Bad Gateway", err);
1131
+ }
1132
+ });
1133
+ });
1134
+ }
1135
+ /**
1136
+ * Register this service with ForgeProxy.
1137
+ * ForgeProxy then routes external traffic to us.
1138
+ */
1139
+ async _registerWithForgeProxy() {
1140
+ const contract = this._serviceInstance?.constructor?.contract;
1141
+ const methods = contract?.expose ?? [];
1142
+ const registration = {
1143
+ service: this.serviceName,
1144
+ host: this._getHost(),
1145
+ port: this.port,
1146
+ workers: this.threadCount,
1147
+ methods,
1148
+ health_endpoint: "/health",
1149
+ };
1150
+ const resp = await fetch(`${this._forgeProxy}/register`, {
1151
+ method: "POST",
1152
+ headers: { "Content-Type": "application/json" },
1153
+ body: JSON.stringify(registration),
1154
+ });
1155
+ if (resp.ok) {
1156
+ this.logger.info(`Registered with ForgeProxy at ${this._forgeProxy}`, {
1157
+ methods: methods.length,
1158
+ });
1159
+ }
1160
+ else {
1161
+ throw new Error(`ForgeProxy registration failed: ${resp.status}`);
1162
+ }
1163
+ }
1164
+ _getHost() {
1165
+ return process.env.FORGE_HOST ?? process.env.HOSTNAME ?? "127.0.0.1";
1166
+ }
1167
+ }
1168
+ export { ForgeWebSocket };
1169
+ //# sourceMappingURL=ForgeContext.js.map