@c9up/ream 0.1.3

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 (451) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/Application.d.ts +71 -0
  4. package/dist/Application.d.ts.map +1 -0
  5. package/dist/Application.js +151 -0
  6. package/dist/Application.js.map +1 -0
  7. package/dist/Codemods.d.ts +16 -0
  8. package/dist/Codemods.d.ts.map +1 -0
  9. package/dist/Codemods.js +277 -0
  10. package/dist/Codemods.js.map +1 -0
  11. package/dist/ConfigLoader.d.ts +29 -0
  12. package/dist/ConfigLoader.d.ts.map +1 -0
  13. package/dist/ConfigLoader.js +43 -0
  14. package/dist/ConfigLoader.js.map +1 -0
  15. package/dist/ErrorBoundary.d.ts +51 -0
  16. package/dist/ErrorBoundary.d.ts.map +1 -0
  17. package/dist/ErrorBoundary.js +122 -0
  18. package/dist/ErrorBoundary.js.map +1 -0
  19. package/dist/GracefulShutdown.d.ts +28 -0
  20. package/dist/GracefulShutdown.d.ts.map +1 -0
  21. package/dist/GracefulShutdown.js +55 -0
  22. package/dist/GracefulShutdown.js.map +1 -0
  23. package/dist/HealthCheck.d.ts +42 -0
  24. package/dist/HealthCheck.d.ts.map +1 -0
  25. package/dist/HealthCheck.js +65 -0
  26. package/dist/HealthCheck.js.map +1 -0
  27. package/dist/HotReload.d.ts +28 -0
  28. package/dist/HotReload.d.ts.map +1 -0
  29. package/dist/HotReload.js +60 -0
  30. package/dist/HotReload.js.map +1 -0
  31. package/dist/HttpKernel.d.ts +62 -0
  32. package/dist/HttpKernel.d.ts.map +1 -0
  33. package/dist/HttpKernel.js +210 -0
  34. package/dist/HttpKernel.js.map +1 -0
  35. package/dist/Ignitor.d.ts +195 -0
  36. package/dist/Ignitor.d.ts.map +1 -0
  37. package/dist/Ignitor.js +505 -0
  38. package/dist/Ignitor.js.map +1 -0
  39. package/dist/Provider.d.ts +54 -0
  40. package/dist/Provider.d.ts.map +1 -0
  41. package/dist/Provider.js +37 -0
  42. package/dist/Provider.js.map +1 -0
  43. package/dist/bodyparser/BodyParserMiddleware.d.ts +38 -0
  44. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -0
  45. package/dist/bodyparser/BodyParserMiddleware.js +143 -0
  46. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -0
  47. package/dist/bodyparser/MultipartFile.d.ts +77 -0
  48. package/dist/bodyparser/MultipartFile.d.ts.map +1 -0
  49. package/dist/bodyparser/MultipartFile.js +138 -0
  50. package/dist/bodyparser/MultipartFile.js.map +1 -0
  51. package/dist/bodyparser/parseSize.d.ts +6 -0
  52. package/dist/bodyparser/parseSize.d.ts.map +1 -0
  53. package/dist/bodyparser/parseSize.js +21 -0
  54. package/dist/bodyparser/parseSize.js.map +1 -0
  55. package/dist/bootstrap/hyper-server.d.ts +27 -0
  56. package/dist/bootstrap/hyper-server.d.ts.map +1 -0
  57. package/dist/bootstrap/hyper-server.js +63 -0
  58. package/dist/bootstrap/hyper-server.js.map +1 -0
  59. package/dist/console/CommandRunner.d.ts +18 -0
  60. package/dist/console/CommandRunner.d.ts.map +1 -0
  61. package/dist/console/CommandRunner.js +65 -0
  62. package/dist/console/CommandRunner.js.map +1 -0
  63. package/dist/container/Container.d.ts +60 -0
  64. package/dist/container/Container.d.ts.map +1 -0
  65. package/dist/container/Container.js +329 -0
  66. package/dist/container/Container.js.map +1 -0
  67. package/dist/container/types.d.ts +25 -0
  68. package/dist/container/types.d.ts.map +1 -0
  69. package/dist/container/types.js +6 -0
  70. package/dist/container/types.js.map +1 -0
  71. package/dist/decorators/Lazy.d.ts +19 -0
  72. package/dist/decorators/Lazy.d.ts.map +1 -0
  73. package/dist/decorators/Lazy.js +53 -0
  74. package/dist/decorators/Lazy.js.map +1 -0
  75. package/dist/decorators/Service.d.ts +48 -0
  76. package/dist/decorators/Service.d.ts.map +1 -0
  77. package/dist/decorators/Service.js +70 -0
  78. package/dist/decorators/Service.js.map +1 -0
  79. package/dist/errors/FuzzyMatcher.d.ts +41 -0
  80. package/dist/errors/FuzzyMatcher.d.ts.map +1 -0
  81. package/dist/errors/FuzzyMatcher.js +81 -0
  82. package/dist/errors/FuzzyMatcher.js.map +1 -0
  83. package/dist/errors/ModuleErrors.d.ts +41 -0
  84. package/dist/errors/ModuleErrors.d.ts.map +1 -0
  85. package/dist/errors/ModuleErrors.js +65 -0
  86. package/dist/errors/ModuleErrors.js.map +1 -0
  87. package/dist/errors/PipelineStageError.d.ts +31 -0
  88. package/dist/errors/PipelineStageError.d.ts.map +1 -0
  89. package/dist/errors/PipelineStageError.js +77 -0
  90. package/dist/errors/PipelineStageError.js.map +1 -0
  91. package/dist/errors/ReamError.d.ts +50 -0
  92. package/dist/errors/ReamError.d.ts.map +1 -0
  93. package/dist/errors/ReamError.js +108 -0
  94. package/dist/errors/ReamError.js.map +1 -0
  95. package/dist/events/Emitter.d.ts +124 -0
  96. package/dist/events/Emitter.d.ts.map +1 -0
  97. package/dist/events/Emitter.js +271 -0
  98. package/dist/events/Emitter.js.map +1 -0
  99. package/dist/events/EventsProvider.d.ts +28 -0
  100. package/dist/events/EventsProvider.d.ts.map +1 -0
  101. package/dist/events/EventsProvider.js +36 -0
  102. package/dist/events/EventsProvider.js.map +1 -0
  103. package/dist/events/config.d.ts +8 -0
  104. package/dist/events/config.d.ts.map +1 -0
  105. package/dist/events/config.js +4 -0
  106. package/dist/events/config.js.map +1 -0
  107. package/dist/events/configure.d.ts +10 -0
  108. package/dist/events/configure.d.ts.map +1 -0
  109. package/dist/events/configure.js +11 -0
  110. package/dist/events/configure.js.map +1 -0
  111. package/dist/events/helix/helpers.d.ts +62 -0
  112. package/dist/events/helix/helpers.d.ts.map +1 -0
  113. package/dist/events/helix/helpers.js +187 -0
  114. package/dist/events/helix/helpers.js.map +1 -0
  115. package/dist/events/index.d.ts +17 -0
  116. package/dist/events/index.d.ts.map +1 -0
  117. package/dist/events/index.js +15 -0
  118. package/dist/events/index.js.map +1 -0
  119. package/dist/events/native.d.ts +21 -0
  120. package/dist/events/native.d.ts.map +1 -0
  121. package/dist/events/native.js +17 -0
  122. package/dist/events/native.js.map +1 -0
  123. package/dist/events/services/main.d.ts +18 -0
  124. package/dist/events/services/main.d.ts.map +1 -0
  125. package/dist/events/services/main.js +31 -0
  126. package/dist/events/services/main.js.map +1 -0
  127. package/dist/events/testing/FakeBus.d.ts +66 -0
  128. package/dist/events/testing/FakeBus.d.ts.map +1 -0
  129. package/dist/events/testing/FakeBus.js +356 -0
  130. package/dist/events/testing/FakeBus.js.map +1 -0
  131. package/dist/graphql/GraphQLEngine.d.ts +69 -0
  132. package/dist/graphql/GraphQLEngine.d.ts.map +1 -0
  133. package/dist/graphql/GraphQLEngine.js +309 -0
  134. package/dist/graphql/GraphQLEngine.js.map +1 -0
  135. package/dist/graphql/GraphQLProvider.d.ts +32 -0
  136. package/dist/graphql/GraphQLProvider.d.ts.map +1 -0
  137. package/dist/graphql/GraphQLProvider.js +49 -0
  138. package/dist/graphql/GraphQLProvider.js.map +1 -0
  139. package/dist/helpers/base64.d.ts +40 -0
  140. package/dist/helpers/base64.d.ts.map +1 -0
  141. package/dist/helpers/base64.js +70 -0
  142. package/dist/helpers/base64.js.map +1 -0
  143. package/dist/helpers/index.d.ts +7 -0
  144. package/dist/helpers/index.d.ts.map +1 -0
  145. package/dist/helpers/index.js +5 -0
  146. package/dist/helpers/index.js.map +1 -0
  147. package/dist/helpers/is.d.ts +101 -0
  148. package/dist/helpers/is.d.ts.map +1 -0
  149. package/dist/helpers/is.js +151 -0
  150. package/dist/helpers/is.js.map +1 -0
  151. package/dist/helpers/napi-loader.d.ts +46 -0
  152. package/dist/helpers/napi-loader.d.ts.map +1 -0
  153. package/dist/helpers/napi-loader.js +69 -0
  154. package/dist/helpers/napi-loader.js.map +1 -0
  155. package/dist/helpers/safeEqual.d.ts +9 -0
  156. package/dist/helpers/safeEqual.d.ts.map +1 -0
  157. package/dist/helpers/safeEqual.js +16 -0
  158. package/dist/helpers/safeEqual.js.map +1 -0
  159. package/dist/helpers/string.d.ts +161 -0
  160. package/dist/helpers/string.d.ts.map +1 -0
  161. package/dist/helpers/string.js +429 -0
  162. package/dist/helpers/string.js.map +1 -0
  163. package/dist/helpers/types.d.ts +83 -0
  164. package/dist/helpers/types.d.ts.map +1 -0
  165. package/dist/helpers/types.js +6 -0
  166. package/dist/helpers/types.js.map +1 -0
  167. package/dist/http/Exception.d.ts +103 -0
  168. package/dist/http/Exception.d.ts.map +1 -0
  169. package/dist/http/Exception.js +228 -0
  170. package/dist/http/Exception.js.map +1 -0
  171. package/dist/http/HttpContext.d.ts +69 -0
  172. package/dist/http/HttpContext.d.ts.map +1 -0
  173. package/dist/http/HttpContext.js +55 -0
  174. package/dist/http/HttpContext.js.map +1 -0
  175. package/dist/http/RedirectBuilder.d.ts +31 -0
  176. package/dist/http/RedirectBuilder.d.ts.map +1 -0
  177. package/dist/http/RedirectBuilder.js +75 -0
  178. package/dist/http/RedirectBuilder.js.map +1 -0
  179. package/dist/http/Request.d.ts +160 -0
  180. package/dist/http/Request.d.ts.map +1 -0
  181. package/dist/http/Request.js +484 -0
  182. package/dist/http/Request.js.map +1 -0
  183. package/dist/http/Response.d.ts +114 -0
  184. package/dist/http/Response.d.ts.map +1 -0
  185. package/dist/http/Response.js +303 -0
  186. package/dist/http/Response.js.map +1 -0
  187. package/dist/http/SseStream.d.ts +89 -0
  188. package/dist/http/SseStream.d.ts.map +1 -0
  189. package/dist/http/SseStream.js +228 -0
  190. package/dist/http/SseStream.js.map +1 -0
  191. package/dist/http/urlDecode.d.ts +8 -0
  192. package/dist/http/urlDecode.d.ts.map +1 -0
  193. package/dist/http/urlDecode.js +18 -0
  194. package/dist/http/urlDecode.js.map +1 -0
  195. package/dist/index.d.ts +92 -0
  196. package/dist/index.d.ts.map +1 -0
  197. package/dist/index.js +67 -0
  198. package/dist/index.js.map +1 -0
  199. package/dist/middleware/Pipeline.d.ts +36 -0
  200. package/dist/middleware/Pipeline.d.ts.map +1 -0
  201. package/dist/middleware/Pipeline.js +104 -0
  202. package/dist/middleware/Pipeline.js.map +1 -0
  203. package/dist/openapi/OpenApiGenerator.d.ts +58 -0
  204. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -0
  205. package/dist/openapi/OpenApiGenerator.js +192 -0
  206. package/dist/openapi/OpenApiGenerator.js.map +1 -0
  207. package/dist/openapi/OpenApiMiddleware.d.ts +24 -0
  208. package/dist/openapi/OpenApiMiddleware.d.ts.map +1 -0
  209. package/dist/openapi/OpenApiMiddleware.js +78 -0
  210. package/dist/openapi/OpenApiMiddleware.js.map +1 -0
  211. package/dist/openapi/OpenApiProvider.d.ts +36 -0
  212. package/dist/openapi/OpenApiProvider.d.ts.map +1 -0
  213. package/dist/openapi/OpenApiProvider.js +42 -0
  214. package/dist/openapi/OpenApiProvider.js.map +1 -0
  215. package/dist/resources/ApiResource.d.ts +45 -0
  216. package/dist/resources/ApiResource.d.ts.map +1 -0
  217. package/dist/resources/ApiResource.js +58 -0
  218. package/dist/resources/ApiResource.js.map +1 -0
  219. package/dist/router/Router.d.ts +205 -0
  220. package/dist/router/Router.d.ts.map +1 -0
  221. package/dist/router/Router.js +552 -0
  222. package/dist/router/Router.js.map +1 -0
  223. package/dist/rpc/RpcProvider.d.ts +34 -0
  224. package/dist/rpc/RpcProvider.d.ts.map +1 -0
  225. package/dist/rpc/RpcProvider.js +37 -0
  226. package/dist/rpc/RpcProvider.js.map +1 -0
  227. package/dist/rpc/RpcRouter.d.ts +61 -0
  228. package/dist/rpc/RpcRouter.d.ts.map +1 -0
  229. package/dist/rpc/RpcRouter.js +187 -0
  230. package/dist/rpc/RpcRouter.js.map +1 -0
  231. package/dist/scheduler/Schedule.d.ts +53 -0
  232. package/dist/scheduler/Schedule.d.ts.map +1 -0
  233. package/dist/scheduler/Schedule.js +76 -0
  234. package/dist/scheduler/Schedule.js.map +1 -0
  235. package/dist/scheduler/ScheduleProvider.d.ts +56 -0
  236. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -0
  237. package/dist/scheduler/ScheduleProvider.js +145 -0
  238. package/dist/scheduler/ScheduleProvider.js.map +1 -0
  239. package/dist/scheduler/Scheduler.d.ts +209 -0
  240. package/dist/scheduler/Scheduler.d.ts.map +1 -0
  241. package/dist/scheduler/Scheduler.js +432 -0
  242. package/dist/scheduler/Scheduler.js.map +1 -0
  243. package/dist/scheduler/index.d.ts +14 -0
  244. package/dist/scheduler/index.d.ts.map +1 -0
  245. package/dist/scheduler/index.js +6 -0
  246. package/dist/scheduler/index.js.map +1 -0
  247. package/dist/scheduler/locks/LockBackend.d.ts +37 -0
  248. package/dist/scheduler/locks/LockBackend.d.ts.map +1 -0
  249. package/dist/scheduler/locks/LockBackend.js +13 -0
  250. package/dist/scheduler/locks/LockBackend.js.map +1 -0
  251. package/dist/scheduler/locks/MemoryLockBackend.d.ts +39 -0
  252. package/dist/scheduler/locks/MemoryLockBackend.d.ts.map +1 -0
  253. package/dist/scheduler/locks/MemoryLockBackend.js +71 -0
  254. package/dist/scheduler/locks/MemoryLockBackend.js.map +1 -0
  255. package/dist/scheduler/observability/ErrorReporter.d.ts +15 -0
  256. package/dist/scheduler/observability/ErrorReporter.d.ts.map +1 -0
  257. package/dist/scheduler/observability/ErrorReporter.js +13 -0
  258. package/dist/scheduler/observability/ErrorReporter.js.map +1 -0
  259. package/dist/scheduler/observability/ScheduleEvent.d.ts +57 -0
  260. package/dist/scheduler/observability/ScheduleEvent.d.ts.map +1 -0
  261. package/dist/scheduler/observability/ScheduleEvent.js +12 -0
  262. package/dist/scheduler/observability/ScheduleEvent.js.map +1 -0
  263. package/dist/scheduler/observability/ScheduleEventSink.d.ts +20 -0
  264. package/dist/scheduler/observability/ScheduleEventSink.d.ts.map +1 -0
  265. package/dist/scheduler/observability/ScheduleEventSink.js +17 -0
  266. package/dist/scheduler/observability/ScheduleEventSink.js.map +1 -0
  267. package/dist/scheduler/observability/StatsTracker.d.ts +50 -0
  268. package/dist/scheduler/observability/StatsTracker.d.ts.map +1 -0
  269. package/dist/scheduler/observability/StatsTracker.js +96 -0
  270. package/dist/scheduler/observability/StatsTracker.js.map +1 -0
  271. package/dist/security/CookieSigner.d.ts +13 -0
  272. package/dist/security/CookieSigner.d.ts.map +1 -0
  273. package/dist/security/CookieSigner.js +51 -0
  274. package/dist/security/CookieSigner.js.map +1 -0
  275. package/dist/security/SignedUrl.d.ts +17 -0
  276. package/dist/security/SignedUrl.d.ts.map +1 -0
  277. package/dist/security/SignedUrl.js +63 -0
  278. package/dist/security/SignedUrl.js.map +1 -0
  279. package/dist/security/crypto.d.ts +25 -0
  280. package/dist/security/crypto.d.ts.map +1 -0
  281. package/dist/security/crypto.js +82 -0
  282. package/dist/security/crypto.js.map +1 -0
  283. package/dist/server/HyperServer.d.ts +64 -0
  284. package/dist/server/HyperServer.d.ts.map +1 -0
  285. package/dist/server/HyperServer.js +54 -0
  286. package/dist/server/HyperServer.js.map +1 -0
  287. package/dist/server/Server.d.ts +68 -0
  288. package/dist/server/Server.d.ts.map +1 -0
  289. package/dist/server/Server.js +113 -0
  290. package/dist/server/Server.js.map +1 -0
  291. package/dist/services/app.d.ts +14 -0
  292. package/dist/services/app.d.ts.map +1 -0
  293. package/dist/services/app.js +27 -0
  294. package/dist/services/app.js.map +1 -0
  295. package/dist/services/router.d.ts +21 -0
  296. package/dist/services/router.d.ts.map +1 -0
  297. package/dist/services/router.js +36 -0
  298. package/dist/services/router.js.map +1 -0
  299. package/dist/services/server.d.ts +16 -0
  300. package/dist/services/server.d.ts.map +1 -0
  301. package/dist/services/server.js +32 -0
  302. package/dist/services/server.js.map +1 -0
  303. package/dist/session/Session.d.ts +88 -0
  304. package/dist/session/Session.d.ts.map +1 -0
  305. package/dist/session/Session.js +186 -0
  306. package/dist/session/Session.js.map +1 -0
  307. package/dist/session/SessionMiddleware.d.ts +16 -0
  308. package/dist/session/SessionMiddleware.d.ts.map +1 -0
  309. package/dist/session/SessionMiddleware.js +132 -0
  310. package/dist/session/SessionMiddleware.js.map +1 -0
  311. package/dist/session/drivers/CookieDriver.d.ts +17 -0
  312. package/dist/session/drivers/CookieDriver.d.ts.map +1 -0
  313. package/dist/session/drivers/CookieDriver.js +42 -0
  314. package/dist/session/drivers/CookieDriver.js.map +1 -0
  315. package/dist/session/drivers/MemoryDriver.d.ts +9 -0
  316. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -0
  317. package/dist/session/drivers/MemoryDriver.js +24 -0
  318. package/dist/session/drivers/MemoryDriver.js.map +1 -0
  319. package/dist/storage/S3Driver.d.ts +27 -0
  320. package/dist/storage/S3Driver.d.ts.map +1 -0
  321. package/dist/storage/S3Driver.js +135 -0
  322. package/dist/storage/S3Driver.js.map +1 -0
  323. package/dist/storage/StaticMiddleware.d.ts +27 -0
  324. package/dist/storage/StaticMiddleware.d.ts.map +1 -0
  325. package/dist/storage/StaticMiddleware.js +153 -0
  326. package/dist/storage/StaticMiddleware.js.map +1 -0
  327. package/dist/storage/StaticProvider.d.ts +26 -0
  328. package/dist/storage/StaticProvider.d.ts.map +1 -0
  329. package/dist/storage/StaticProvider.js +31 -0
  330. package/dist/storage/StaticProvider.js.map +1 -0
  331. package/dist/storage/StorageManager.d.ts +31 -0
  332. package/dist/storage/StorageManager.d.ts.map +1 -0
  333. package/dist/storage/StorageManager.js +120 -0
  334. package/dist/storage/StorageManager.js.map +1 -0
  335. package/dist/testing/RequestBuilder.d.ts +67 -0
  336. package/dist/testing/RequestBuilder.d.ts.map +1 -0
  337. package/dist/testing/RequestBuilder.js +202 -0
  338. package/dist/testing/RequestBuilder.js.map +1 -0
  339. package/dist/testing/TestClient.d.ts +96 -0
  340. package/dist/testing/TestClient.d.ts.map +1 -0
  341. package/dist/testing/TestClient.js +263 -0
  342. package/dist/testing/TestClient.js.map +1 -0
  343. package/dist/types/helpers.d.ts +47 -0
  344. package/dist/types/helpers.d.ts.map +1 -0
  345. package/dist/types/helpers.js +5 -0
  346. package/dist/types/helpers.js.map +1 -0
  347. package/events.darwin-arm64.node +0 -0
  348. package/events.darwin-x64.node +0 -0
  349. package/events.linux-arm64-gnu.node +0 -0
  350. package/events.linux-x64-gnu.node +0 -0
  351. package/events.win32-x64-msvc.node +0 -0
  352. package/index.darwin-arm64.node +0 -0
  353. package/index.darwin-x64.node +0 -0
  354. package/index.linux-arm64-gnu.node +0 -0
  355. package/index.linux-x64-gnu.node +0 -0
  356. package/index.win32-x64-msvc.node +0 -0
  357. package/package.json +106 -0
  358. package/scheduler.darwin-arm64.node +0 -0
  359. package/scheduler.darwin-x64.node +0 -0
  360. package/scheduler.linux-arm64-gnu.node +0 -0
  361. package/scheduler.linux-x64-gnu.node +0 -0
  362. package/scheduler.win32-x64-msvc.node +0 -0
  363. package/scripts/copy-napi.mjs +113 -0
  364. package/src/Application.ts +177 -0
  365. package/src/Codemods.ts +343 -0
  366. package/src/ConfigLoader.ts +49 -0
  367. package/src/ErrorBoundary.ts +157 -0
  368. package/src/GracefulShutdown.ts +80 -0
  369. package/src/HealthCheck.ts +96 -0
  370. package/src/HotReload.ts +78 -0
  371. package/src/HttpKernel.ts +305 -0
  372. package/src/Ignitor.ts +656 -0
  373. package/src/Provider.ts +74 -0
  374. package/src/bodyparser/BodyParserMiddleware.ts +183 -0
  375. package/src/bodyparser/MultipartFile.ts +181 -0
  376. package/src/bodyparser/parseSize.ts +19 -0
  377. package/src/bootstrap/hyper-server.ts +78 -0
  378. package/src/console/CommandRunner.ts +82 -0
  379. package/src/container/Container.ts +383 -0
  380. package/src/container/types.ts +30 -0
  381. package/src/decorators/Lazy.ts +57 -0
  382. package/src/decorators/Service.ts +85 -0
  383. package/src/errors/FuzzyMatcher.ts +93 -0
  384. package/src/errors/ModuleErrors.ts +73 -0
  385. package/src/errors/PipelineStageError.ts +97 -0
  386. package/src/errors/ReamError.ts +138 -0
  387. package/src/events/Emitter.ts +333 -0
  388. package/src/events/EventsProvider.ts +58 -0
  389. package/src/events/config.ts +10 -0
  390. package/src/events/configure.ts +19 -0
  391. package/src/events/helix/helpers.ts +260 -0
  392. package/src/events/index.ts +22 -0
  393. package/src/events/native.ts +37 -0
  394. package/src/events/services/main.ts +39 -0
  395. package/src/events/testing/FakeBus.ts +420 -0
  396. package/src/graphql/GraphQLEngine.ts +421 -0
  397. package/src/graphql/GraphQLProvider.ts +59 -0
  398. package/src/helpers/base64.ts +72 -0
  399. package/src/helpers/index.ts +6 -0
  400. package/src/helpers/is.ts +162 -0
  401. package/src/helpers/napi-loader.ts +106 -0
  402. package/src/helpers/safeEqual.ts +15 -0
  403. package/src/helpers/string.ts +469 -0
  404. package/src/helpers/types.ts +100 -0
  405. package/src/http/Exception.ts +272 -0
  406. package/src/http/HttpContext.ts +108 -0
  407. package/src/http/RedirectBuilder.ts +89 -0
  408. package/src/http/Request.ts +540 -0
  409. package/src/http/Response.ts +351 -0
  410. package/src/http/SseStream.ts +250 -0
  411. package/src/http/urlDecode.ts +18 -0
  412. package/src/index.ts +263 -0
  413. package/src/middleware/Pipeline.ts +136 -0
  414. package/src/openapi/OpenApiGenerator.ts +254 -0
  415. package/src/openapi/OpenApiMiddleware.ts +101 -0
  416. package/src/openapi/OpenApiProvider.ts +67 -0
  417. package/src/resources/ApiResource.ts +79 -0
  418. package/src/router/Router.ts +733 -0
  419. package/src/rpc/RpcProvider.ts +54 -0
  420. package/src/rpc/RpcRouter.ts +243 -0
  421. package/src/scheduler/Schedule.ts +96 -0
  422. package/src/scheduler/ScheduleProvider.ts +177 -0
  423. package/src/scheduler/Scheduler.ts +601 -0
  424. package/src/scheduler/index.ts +24 -0
  425. package/src/scheduler/locks/LockBackend.ts +38 -0
  426. package/src/scheduler/locks/MemoryLockBackend.ts +85 -0
  427. package/src/scheduler/observability/ErrorReporter.ts +13 -0
  428. package/src/scheduler/observability/ScheduleEvent.ts +61 -0
  429. package/src/scheduler/observability/ScheduleEventSink.ts +21 -0
  430. package/src/scheduler/observability/StatsTracker.ts +134 -0
  431. package/src/security/CookieSigner.ts +54 -0
  432. package/src/security/SignedUrl.ts +71 -0
  433. package/src/security/crypto.ts +98 -0
  434. package/src/server/HyperServer.ts +121 -0
  435. package/src/server/Server.ts +147 -0
  436. package/src/services/app.ts +34 -0
  437. package/src/services/router.ts +44 -0
  438. package/src/services/server.ts +40 -0
  439. package/src/session/Session.ts +231 -0
  440. package/src/session/SessionMiddleware.ts +153 -0
  441. package/src/session/drivers/CookieDriver.ts +48 -0
  442. package/src/session/drivers/MemoryDriver.ts +29 -0
  443. package/src/storage/S3Driver.ts +171 -0
  444. package/src/storage/StaticMiddleware.ts +182 -0
  445. package/src/storage/StaticProvider.ts +40 -0
  446. package/src/storage/StorageManager.ts +142 -0
  447. package/src/testing/RequestBuilder.ts +275 -0
  448. package/src/testing/TestClient.ts +344 -0
  449. package/src/types/helpers.ts +52 -0
  450. package/swcrc.app.json +17 -0
  451. package/tsconfig.app.json +21 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Response — accumulates HTTP response state with a fluent API.
3
+ *
4
+ * Wraps the same { status, headers, body } wire format expected by the NAPI layer.
5
+ * Provides AdonisJS-compatible methods: json(), send(), status(), header(), etc.
6
+ *
7
+ * @implements FR21
8
+ */
9
+
10
+ import type { RedirectBuilder } from './RedirectBuilder.js'
11
+ import {
12
+ openSseStream,
13
+ type SseStream,
14
+ type SseStreamOptions,
15
+ type StreamBackend,
16
+ } from './SseStream.js'
17
+
18
+ /**
19
+ * Reject CR / LF / NUL bytes in header names or values. Without this guard
20
+ * an attacker controlling part of a header (e.g. a redirect URL parameter
21
+ * echoed into `Location`) could splice extra response headers — the classic
22
+ * CRLF injection / response-splitting attack (CWE-113).
23
+ */
24
+ function assertNoCRLF(name: string, value: string): void {
25
+ if (/[\r\n\0]/.test(name) || /[\r\n\0]/.test(value)) {
26
+ throw new Error(
27
+ `CRLF / NUL byte in header '${name}' is not allowed (response-splitting protection).`,
28
+ )
29
+ }
30
+ }
31
+
32
+ export class Response {
33
+ #status = 200
34
+ #headers: Record<string, string> = {}
35
+ #cookies: string[] = []
36
+ #body = ''
37
+ #finished = false
38
+ #redirectBuilderFactory?: () => RedirectBuilder
39
+ #streamBackend?: StreamBackend
40
+ #streamId?: string
41
+
42
+ /**
43
+ * CSP nonce for this request (AdonisJS idiom: `response.nonce`). Seeded by
44
+ * the `@c9up/blackhole` middleware when the CSP uses `@nonce`; `undefined`
45
+ * otherwise. Also published to `ctx.store` as `cspNonce` for templating.
46
+ */
47
+ nonce?: string
48
+
49
+ // ─── Status ───────────────────────────────────────────────
50
+
51
+ /** Set the HTTP status code. Chainable. */
52
+ status(code: number): this {
53
+ this.#status = code
54
+ return this
55
+ }
56
+
57
+ /** Set status only if not already set (still 200). */
58
+ safeStatus(code: number): this {
59
+ if (this.#status === 200) {
60
+ this.#status = code
61
+ }
62
+ return this
63
+ }
64
+
65
+ // ─── Headers ──────────────────────────────────────────────
66
+
67
+ /** Set a response header. Chainable. */
68
+ header(key: string, value: string): this {
69
+ assertNoCRLF(key, value)
70
+ this.#headers[key.toLowerCase()] = value
71
+ return this
72
+ }
73
+
74
+ /** Append to a response header (for multi-value like Set-Cookie). */
75
+ append(key: string, value: string): this {
76
+ assertNoCRLF(key, value)
77
+ const k = key.toLowerCase()
78
+ if (k === 'set-cookie') {
79
+ // Set-Cookie must NOT be comma-joined — each cookie is its own
80
+ // header line, or browsers/proxies mis-parse the lot. Route into
81
+ // the same `#cookies` channel `cookie()` uses; `getHeaders()`
82
+ // emits them newline-separated for the serializer.
83
+ this.#cookies.push(value)
84
+ return this
85
+ }
86
+ const existing = this.#headers[k]
87
+ this.#headers[k] = existing ? `${existing}, ${value}` : value
88
+ return this
89
+ }
90
+
91
+ /** Remove a response header. */
92
+ removeHeader(key: string): this {
93
+ delete this.#headers[key.toLowerCase()]
94
+ return this
95
+ }
96
+
97
+ /** Set the Content-Type header. Chainable. */
98
+ type(contentType: string): this {
99
+ // Same CRLF/NUL guard as header() — `type(userValue)` must not be a
100
+ // response-splitting hole just because it writes a fixed header key.
101
+ assertNoCRLF('content-type', contentType)
102
+ this.#headers['content-type'] = contentType
103
+ return this
104
+ }
105
+
106
+ // ─── Body ─────────────────────────────────────────────────
107
+
108
+ /** Send a JSON response. Sets content-type and stringifies. */
109
+ json(data: unknown): void {
110
+ this.#headers['content-type'] = 'application/json'
111
+ this.#body = JSON.stringify(data)
112
+ this.#finished = true
113
+ }
114
+
115
+ /** Send a response body. Auto-detects content type if not set. */
116
+ send(data: unknown): void {
117
+ if (typeof data === 'string') {
118
+ if (!this.#headers['content-type']) {
119
+ this.#headers['content-type'] = 'text/html; charset=utf-8'
120
+ }
121
+ this.#body = data
122
+ } else if (Buffer.isBuffer(data)) {
123
+ // Without this branch a Buffer would hit the `typeof === 'object'`
124
+ // path and be JSON-stringified into `{"type":"Buffer","data":[...]}`
125
+ // — every binary asset (PNG, PDF, ...) would arrive corrupted.
126
+ this.sendBuffer(data)
127
+ return
128
+ } else if (typeof data === 'object' && data !== null) {
129
+ this.#headers['content-type'] = 'application/json'
130
+ this.#body = JSON.stringify(data)
131
+ } else if (data !== undefined && data !== null) {
132
+ if (!this.#headers['content-type']) {
133
+ this.#headers['content-type'] = 'text/plain'
134
+ }
135
+ this.#body = String(data)
136
+ }
137
+ this.#finished = true
138
+ }
139
+
140
+ /**
141
+ * Send a binary body as-is. Encodes to base64 across the NAPI bridge
142
+ * with the `x-ream-body-encoding: base64` marker so the Rust HTTP
143
+ * layer decodes it before writing to the socket. The marker header is
144
+ * stripped server-side and never leaks to the client.
145
+ *
146
+ * Use this for any non-UTF-8 payload (images, PDFs, archives, etc.) —
147
+ * `send()` auto-routes Buffers here, but call directly when intent is
148
+ * unambiguous.
149
+ */
150
+ sendBuffer(buffer: Buffer): void {
151
+ if (!this.#headers['content-type']) {
152
+ this.#headers['content-type'] = 'application/octet-stream'
153
+ }
154
+ this.#headers['x-ream-body-encoding'] = 'base64'
155
+ this.#body = buffer.toString('base64')
156
+ this.#finished = true
157
+ }
158
+
159
+ /** Send 204 No Content. */
160
+ noContent(): void {
161
+ this.#status = 204
162
+ this.#body = ''
163
+ this.#finished = true
164
+ }
165
+
166
+ // ─── Redirect ─────────────────────────────────────────────
167
+
168
+ /** Get a redirect builder. */
169
+ redirect(): RedirectBuilder {
170
+ if (this.#redirectBuilderFactory) {
171
+ return this.#redirectBuilderFactory()
172
+ }
173
+ throw new Error(
174
+ 'redirect() requires an HttpContext. Response was created outside a request handler.',
175
+ )
176
+ }
177
+
178
+ /** @internal Set the redirect builder factory (injected by HttpContext). */
179
+ setRedirectFactory(factory: () => RedirectBuilder): void {
180
+ this.#redirectBuilderFactory = factory
181
+ }
182
+
183
+ /** @internal Wire the SSE backend (HyperServer NAPI). Injected by HttpKernel. */
184
+ setStreamBackend(backend: StreamBackend): void {
185
+ this.#streamBackend = backend
186
+ }
187
+
188
+ // ─── Streaming (SSE) ──────────────────────────────────────
189
+
190
+ /**
191
+ * Open a Server-Sent Events stream. The returned `SseStream` lets the
192
+ * handler push named events for as long as the client stays
193
+ * connected. Headers are pre-set to the SSE wire format; the host
194
+ * router serializes the response with the reserved stream id so the
195
+ * Rust HyperServer keeps the connection open.
196
+ *
197
+ * const sse = await ctx.response.sse()
198
+ * sse.send('connected', { uid: caller.id })
199
+ * onTaskAssigned((evt) => sse.send('task.assigned', evt))
200
+ *
201
+ * Throws `STREAMING_UNSUPPORTED` if the underlying HyperServer host
202
+ * does not implement the streaming NAPI (e.g. a mock server in unit
203
+ * tests). The host capability is opt-in: implement
204
+ * `registerStream`/`writeStream`/`closeStream`/`onStreamDisconnect`
205
+ * to participate.
206
+ */
207
+ async sse(options?: SseStreamOptions): Promise<SseStream> {
208
+ if (!this.#streamBackend) {
209
+ throw new Error(
210
+ '[ream] response.sse() requires a streaming-capable HyperServer host. ' +
211
+ 'The current server has no stream backend wired.',
212
+ )
213
+ }
214
+ // SSE wire format + safety defaults. The buffered body string stays
215
+ // empty — every chunk goes through the registry instead.
216
+ this.#headers['content-type'] = 'text/event-stream'
217
+ if (!this.#headers['cache-control']) {
218
+ this.#headers['cache-control'] = 'no-cache, no-transform'
219
+ }
220
+ if (!this.#headers['x-accel-buffering']) {
221
+ this.#headers['x-accel-buffering'] = 'no'
222
+ }
223
+ const stream = await openSseStream(this.#streamBackend, options)
224
+ this.#streamId = stream.id
225
+ // Mark the response as "finished" so middleware that bails when
226
+ // `isFinished()` is true (the standard short-circuit) doesn't
227
+ // re-touch the body or headers after the SSE pipe has been set up.
228
+ this.#finished = true
229
+ return stream
230
+ }
231
+
232
+ // ─── Cookies ──────────────────────────────────────────────
233
+
234
+ /** Set a response cookie. */
235
+ cookie(
236
+ name: string,
237
+ value: string,
238
+ options?: {
239
+ maxAge?: number
240
+ path?: string
241
+ httpOnly?: boolean
242
+ secure?: boolean
243
+ sameSite?: 'lax' | 'strict' | 'none'
244
+ },
245
+ ): this {
246
+ // SameSite=None cookies REQUIRE the Secure attribute — modern browsers
247
+ // (Chrome 80+, Firefox, Safari) silently reject the cookie otherwise, so
248
+ // a missing Secure here turns OAuth callbacks / iframe sessions into a
249
+ // "works in dev (localhost relaxes Secure), breaks in prod" foot-gun.
250
+ if (options?.sameSite === 'none' && !options.secure) {
251
+ throw new Error(
252
+ `Cookie '${name}': SameSite=None requires Secure: true (browsers reject SameSite=None cookies without Secure).`,
253
+ )
254
+ }
255
+ const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
256
+ // `maxAge: 0` is the RFC 6265 "delete-now" signal used by logout
257
+ // flows. A truthiness check would skip it and leave the cookie
258
+ // alive for the session — explicit `!== undefined` covers both
259
+ // 0 and negative values that browsers also treat as immediate
260
+ // expiry.
261
+ if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`)
262
+ if (options?.path) {
263
+ // `Path` is concatenated raw (encodeURIComponent would mangle the `/`
264
+ // separators a path legitimately contains), so it needs the same
265
+ // CRLF/NUL guard as header()/append() — otherwise a controlled path
266
+ // injects into the `\n`-joined Set-Cookie header (response-splitting).
267
+ assertNoCRLF(`Set-Cookie path for '${name}'`, options.path)
268
+ parts.push(`Path=${options.path}`)
269
+ }
270
+ if (options?.httpOnly !== false) parts.push('HttpOnly')
271
+ if (options?.secure) parts.push('Secure')
272
+ if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`)
273
+ this.#cookies.push(parts.join('; '))
274
+ return this
275
+ }
276
+
277
+ // ─── Internals (used by HttpKernel for NAPI serialization) ─
278
+
279
+ /** @internal Get the accumulated status code. */
280
+ getStatus(): number {
281
+ return this.#status
282
+ }
283
+
284
+ /** @internal Get all accumulated headers (Set-Cookie as separate entries joined by newline). */
285
+ getHeaders(): Record<string, string> {
286
+ const headers = { ...this.#headers }
287
+ if (this.#cookies.length > 0) {
288
+ // Multiple Set-Cookie headers must be separate — we join with \n for the serializer
289
+ headers['set-cookie'] = this.#cookies.join('\n')
290
+ }
291
+ return headers
292
+ }
293
+
294
+ /**
295
+ * Read a single accumulated response header by name (case-insensitive).
296
+ * Returns `undefined` when not set — used by middleware that conditionally
297
+ * inspects the outgoing content-type before sanitisation.
298
+ */
299
+ getHeader(name: string): string | undefined {
300
+ return this.#headers[name.toLowerCase()]
301
+ }
302
+
303
+ /** @internal Get the accumulated body string. */
304
+ getBody(): string {
305
+ return this.#body
306
+ }
307
+
308
+ /** @internal Check if response has been finalized. */
309
+ isFinished(): boolean {
310
+ return this.#finished
311
+ }
312
+
313
+ /** @internal Set body directly (used by redirect, exception handler). */
314
+ setBody(body: string): void {
315
+ this.#body = body
316
+ this.#finished = true
317
+ }
318
+
319
+ /**
320
+ * @internal Streaming-response handle, set by `sse()`. The host
321
+ * router forwards this id to the Rust HyperServer so the connection
322
+ * stays open and chunks flow from the JS-side writer.
323
+ */
324
+ getStreamId(): string | undefined {
325
+ return this.#streamId
326
+ }
327
+
328
+ /**
329
+ * @internal Tear down a stream the handler opened via `sse()` but
330
+ * then abandoned by throwing. Closes the backend registry slot and
331
+ * clears the stream id + finished flag so the kernel's error path
332
+ * serializes a normal buffered error response instead of emitting
333
+ * `streamId` alongside an error body (which would leave the
334
+ * HyperServer feeding a dead/empty stream forever). Best-effort: a
335
+ * closeStream failure is swallowed — the slot's TTL/disconnect will
336
+ * reclaim it.
337
+ */
338
+ async abortStream(): Promise<void> {
339
+ const id = this.#streamId
340
+ if (id === undefined) return
341
+ this.#streamId = undefined
342
+ this.#finished = false
343
+ if (this.#streamBackend) {
344
+ try {
345
+ await this.#streamBackend.closeStream(id)
346
+ } catch {
347
+ // benign — registry TTL / client disconnect reclaims the slot.
348
+ }
349
+ }
350
+ }
351
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Server-Sent Events writer — owns one logical SSE stream and pushes
3
+ * encoded events through the underlying `StreamBackend` (a thin
4
+ * abstraction over the HyperServer NAPI `registerStream` / `writeStream`
5
+ * / `closeStream` trio).
6
+ *
7
+ * Lifecycle:
8
+ *
9
+ * 1. `ctx.response.sse()` returns a ready-to-use writer. Internally it
10
+ * asks the backend to reserve a stream id and stamps the response
11
+ * with the right `Content-Type` + caching headers so the host
12
+ * router knows to switch into streaming mode.
13
+ * 2. The handler calls `writer.send(event, data)` / `writer.comment(...)`
14
+ * to push frames. Each call returns a boolean — `false` once the
15
+ * client has disconnected, so callers can bail out of long loops
16
+ * without polling.
17
+ * 3. The handler RETURNS the Response from the route — buffered body is
18
+ * empty, but `response.getStreamId()` is set, so Ream tells the
19
+ * Rust HyperServer to keep the connection open and feed the body
20
+ * from the stream registry.
21
+ * 4. Background tasks keep pushing through `writer.send(...)` until
22
+ * either the client leaves (`onClose` callback fires) or the
23
+ * server calls `writer.end()`.
24
+ *
25
+ * The encoding follows the SSE wire format (RFC chez WHATWG):
26
+ *
27
+ * event: <name>\n
28
+ * data: <stringified payload>\n
29
+ * id: <optional event id>\n
30
+ * \n
31
+ *
32
+ * Multi-line payloads are split on `\n` and each line is prefixed with
33
+ * `data: ` so the consumer browser reassembles them with newlines.
34
+ */
35
+
36
+ /** Minimal backend the writer needs — the HyperServer NAPI fits exactly. */
37
+ export interface StreamBackend {
38
+ registerStream(streamId: string): Promise<boolean>
39
+ writeStream(streamId: string, chunk: string): Promise<boolean>
40
+ closeStream(streamId: string): Promise<boolean>
41
+ onStreamDisconnect(streamId: string, callback: () => void): void
42
+ }
43
+
44
+ export interface SseStreamOptions {
45
+ /** Keep-alive ping interval (ms). Defaults to 30s. Set to 0 to disable. */
46
+ pingInterval?: number
47
+ /** Initial `retry: <ms>` directive sent before any event. Defaults to 5_000. */
48
+ retry?: number
49
+ }
50
+
51
+ const DEFAULT_PING_INTERVAL = 30_000
52
+ const DEFAULT_RETRY = 5_000
53
+
54
+ /** Cryptographic-quality stream id — 16 random bytes, hex-encoded. */
55
+ function generateStreamId(): string {
56
+ const bytes = new Uint8Array(16)
57
+ // `crypto.getRandomValues` is the WHATWG primitive available on every
58
+ // supported Node version (>=22). Avoids pulling in `node:crypto` for
59
+ // a 32-char hex string.
60
+ globalThis.crypto.getRandomValues(bytes)
61
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
62
+ }
63
+
64
+ /** Active SSE writer returned by `response.sse()`. */
65
+ export class SseStream {
66
+ readonly id: string
67
+ #backend: StreamBackend
68
+ #open = true
69
+ #closeFired = false
70
+ #pingTimer: ReturnType<typeof setInterval> | null = null
71
+ #closeListeners: Array<() => void> = []
72
+
73
+ constructor(id: string, backend: StreamBackend, options?: SseStreamOptions) {
74
+ this.id = id
75
+ this.#backend = backend
76
+
77
+ // The backend disconnect signal and a server-side end() can BOTH
78
+ // fire (e.g. server end() followed by the client-drop signal). The
79
+ // shared #fireClose() guards idempotency so user cleanups run once.
80
+ backend.onStreamDisconnect(id, () => {
81
+ this.#open = false
82
+ this.#stopPing()
83
+ this.#fireClose()
84
+ })
85
+
86
+ const ping = options?.pingInterval ?? DEFAULT_PING_INTERVAL
87
+ if (ping > 0) {
88
+ this.#pingTimer = setInterval(() => {
89
+ if (!this.#open) return
90
+ // Comment frames keep the connection alive without firing a
91
+ // JS event on the client. `writeStream` returns false once the
92
+ // client has disconnected (per the NAPI contract); if we see
93
+ // that while still "open" the disconnect signal was missed —
94
+ // e.g. the client dropped in the window between registerStream
95
+ // and the constructor wiring onStreamDisconnect. Self-heal:
96
+ // stop the timer and run close listeners once, so a dropped-
97
+ // at-open stream can't leave an orphaned keepalive ticking
98
+ // against a dead registry slot.
99
+ void this.#backend.writeStream(this.id, ':keepalive\n\n').then((ok) => {
100
+ if (!ok && this.#open) {
101
+ this.#open = false
102
+ this.#stopPing()
103
+ this.#fireClose()
104
+ }
105
+ })
106
+ }, ping)
107
+ // The keepalive must not pin the Node event loop — letting the
108
+ // process exit on its own when nothing else is keeping it busy
109
+ // matches Adonis Transmit's behavior.
110
+ if (typeof this.#pingTimer === 'object' && 'unref' in this.#pingTimer) {
111
+ ;(this.#pingTimer as { unref: () => void }).unref()
112
+ }
113
+ }
114
+ }
115
+
116
+ /** True until the client disconnects or the server calls `end()`. */
117
+ isOpen(): boolean {
118
+ return this.#open
119
+ }
120
+
121
+ /**
122
+ * Send a named SSE event. `data` is JSON-stringified unless it is
123
+ * already a string. Returns `false` if the stream is closed.
124
+ *
125
+ * `eventId` is optional — set it when you want browsers to send
126
+ * `Last-Event-ID` on reconnection. Useful for resumable streams.
127
+ */
128
+ async send(event: string, data: unknown, eventId?: string): Promise<boolean> {
129
+ if (!this.#open) return false
130
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
131
+ return this.#backend.writeStream(this.id, encodeFrame(event, payload, eventId))
132
+ }
133
+
134
+ /**
135
+ * Send a comment frame (`:text\n\n`) — useful for keep-alives and
136
+ * for human-readable inline debug. Comments are NOT delivered to
137
+ * `EventSource.onmessage`; the browser silently consumes them.
138
+ */
139
+ async comment(text: string): Promise<boolean> {
140
+ if (!this.#open) return false
141
+ // Strip newlines so a malicious comment cannot inject `\n\n` and
142
+ // close a frame early.
143
+ const sanitized = text.replace(/[\r\n]+/g, ' ')
144
+ return this.#backend.writeStream(this.id, `:${sanitized}\n\n`)
145
+ }
146
+
147
+ /** Register a callback fired once the underlying connection drops. */
148
+ onClose(callback: () => void): void {
149
+ if (!this.#open) {
150
+ // Already closed — run the callback synchronously so behavior
151
+ // matches the standard observer pattern.
152
+ try {
153
+ callback()
154
+ } catch {
155
+ // Swallow — same isolation as the normal listener loop.
156
+ }
157
+ return
158
+ }
159
+ this.#closeListeners.push(callback)
160
+ }
161
+
162
+ /**
163
+ * End the stream from the server side. Idempotent. The matching
164
+ * NAPI receiver finishes its work and hyper closes the body.
165
+ */
166
+ async end(): Promise<void> {
167
+ if (!this.#open) return
168
+ this.#open = false
169
+ this.#stopPing()
170
+ await this.#backend.closeStream(this.id)
171
+ this.#fireClose()
172
+ }
173
+
174
+ #stopPing(): void {
175
+ if (this.#pingTimer !== null) {
176
+ clearInterval(this.#pingTimer)
177
+ this.#pingTimer = null
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Run every close listener exactly once. Both end() (server-side) and
183
+ * the backend disconnect signal funnel through here; the #closeFired
184
+ * latch stops user cleanups from being replayed when both fire.
185
+ */
186
+ #fireClose(): void {
187
+ if (this.#closeFired) return
188
+ this.#closeFired = true
189
+ for (const cb of this.#closeListeners) {
190
+ try {
191
+ cb()
192
+ } catch {
193
+ // Listener errors are isolated — one bad cleanup callback
194
+ // mustn't take down the rest.
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Open a new SSE stream against the given backend. Returns once the
202
+ * registry slot is reserved — safe to call `send()` immediately, even
203
+ * before the handler has returned (frames buffer in the bounded mpsc
204
+ * inside Rust).
205
+ *
206
+ * The `retry: <ms>` directive is sent as the first thing on the wire
207
+ * (a comment frame would be invisible to the client; the retry hint
208
+ * needs to land before any real event so browsers know how long to
209
+ * wait before reconnecting if the connection drops).
210
+ */
211
+ export async function openSseStream(
212
+ backend: StreamBackend,
213
+ options?: SseStreamOptions,
214
+ ): Promise<SseStream> {
215
+ const id = generateStreamId()
216
+ const registered = await backend.registerStream(id)
217
+ if (!registered) {
218
+ // Hex collision is astronomically improbable, but surfacing it
219
+ // cleanly beats a silent stream-id mismatch (the response builder
220
+ // would then 500 with E_STREAM_UNKNOWN, which is far less helpful).
221
+ throw new Error(`[ream] failed to reserve SSE stream id ${id} — collision in the registry`)
222
+ }
223
+ // First frame: the recommended retry hint. Conforms to the EventSource
224
+ // reconnection backoff spec; falling through with the default 5s.
225
+ await backend.writeStream(id, `retry: ${options?.retry ?? DEFAULT_RETRY}\n\n`)
226
+ return new SseStream(id, backend, options)
227
+ }
228
+
229
+ /** Encode a single SSE event in the wire format. */
230
+ function encodeFrame(event: string, data: string, eventId?: string): string {
231
+ // `event:` line first if we have a name (a frame without `event:` is
232
+ // delivered as a generic `message` event by the browser — both forms
233
+ // are valid).
234
+ const lines: string[] = []
235
+ // `event:` and `id:` are single-line tokens — strip CR/LF so a
236
+ // caller-supplied event name or id can't splice extra SSE frames
237
+ // (the same injection `comment()` guards against). `data` is safe:
238
+ // it's split on `\n` and each line re-prefixed, so newlines there
239
+ // stay inside one logical event.
240
+ if (event) lines.push(`event: ${event.replace(/[\r\n]+/g, ' ')}`)
241
+ // Multi-line data is allowed by SSE but each line must repeat the
242
+ // `data: ` prefix. Splitting on `\n` keeps the browser's
243
+ // reassembly intact (it joins back with `\n`).
244
+ for (const line of data.split('\n')) {
245
+ lines.push(`data: ${line}`)
246
+ }
247
+ if (eventId) lines.push(`id: ${eventId.replace(/[\r\n]+/g, ' ')}`)
248
+ // Trailing blank line terminates the event.
249
+ return `${lines.join('\n')}\n\n`
250
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Safe URL decoding helpers for untrusted request inputs.
3
+ */
4
+
5
+ /** Decode URI component and keep raw value on malformed escapes. */
6
+ export function safeDecodeURIComponent(value: string): string {
7
+ try {
8
+ return decodeURIComponent(value)
9
+ } catch {
10
+ return value
11
+ }
12
+ }
13
+
14
+ /** Decode x-www-form-urlencoded component (`+` => space) safely. */
15
+ export function safeDecodeFormComponent(value: string): string {
16
+ const normalized = value.replace(/\+/g, ' ')
17
+ return safeDecodeURIComponent(normalized)
18
+ }