@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/Application.d.ts +71 -0
- package/dist/Application.d.ts.map +1 -0
- package/dist/Application.js +151 -0
- package/dist/Application.js.map +1 -0
- package/dist/Codemods.d.ts +16 -0
- package/dist/Codemods.d.ts.map +1 -0
- package/dist/Codemods.js +277 -0
- package/dist/Codemods.js.map +1 -0
- package/dist/ConfigLoader.d.ts +29 -0
- package/dist/ConfigLoader.d.ts.map +1 -0
- package/dist/ConfigLoader.js +43 -0
- package/dist/ConfigLoader.js.map +1 -0
- package/dist/ErrorBoundary.d.ts +51 -0
- package/dist/ErrorBoundary.d.ts.map +1 -0
- package/dist/ErrorBoundary.js +122 -0
- package/dist/ErrorBoundary.js.map +1 -0
- package/dist/GracefulShutdown.d.ts +28 -0
- package/dist/GracefulShutdown.d.ts.map +1 -0
- package/dist/GracefulShutdown.js +55 -0
- package/dist/GracefulShutdown.js.map +1 -0
- package/dist/HealthCheck.d.ts +42 -0
- package/dist/HealthCheck.d.ts.map +1 -0
- package/dist/HealthCheck.js +65 -0
- package/dist/HealthCheck.js.map +1 -0
- package/dist/HotReload.d.ts +28 -0
- package/dist/HotReload.d.ts.map +1 -0
- package/dist/HotReload.js +60 -0
- package/dist/HotReload.js.map +1 -0
- package/dist/HttpKernel.d.ts +62 -0
- package/dist/HttpKernel.d.ts.map +1 -0
- package/dist/HttpKernel.js +210 -0
- package/dist/HttpKernel.js.map +1 -0
- package/dist/Ignitor.d.ts +195 -0
- package/dist/Ignitor.d.ts.map +1 -0
- package/dist/Ignitor.js +505 -0
- package/dist/Ignitor.js.map +1 -0
- package/dist/Provider.d.ts +54 -0
- package/dist/Provider.d.ts.map +1 -0
- package/dist/Provider.js +37 -0
- package/dist/Provider.js.map +1 -0
- package/dist/bodyparser/BodyParserMiddleware.d.ts +38 -0
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -0
- package/dist/bodyparser/BodyParserMiddleware.js +143 -0
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -0
- package/dist/bodyparser/MultipartFile.d.ts +77 -0
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -0
- package/dist/bodyparser/MultipartFile.js +138 -0
- package/dist/bodyparser/MultipartFile.js.map +1 -0
- package/dist/bodyparser/parseSize.d.ts +6 -0
- package/dist/bodyparser/parseSize.d.ts.map +1 -0
- package/dist/bodyparser/parseSize.js +21 -0
- package/dist/bodyparser/parseSize.js.map +1 -0
- package/dist/bootstrap/hyper-server.d.ts +27 -0
- package/dist/bootstrap/hyper-server.d.ts.map +1 -0
- package/dist/bootstrap/hyper-server.js +63 -0
- package/dist/bootstrap/hyper-server.js.map +1 -0
- package/dist/console/CommandRunner.d.ts +18 -0
- package/dist/console/CommandRunner.d.ts.map +1 -0
- package/dist/console/CommandRunner.js +65 -0
- package/dist/console/CommandRunner.js.map +1 -0
- package/dist/container/Container.d.ts +60 -0
- package/dist/container/Container.d.ts.map +1 -0
- package/dist/container/Container.js +329 -0
- package/dist/container/Container.js.map +1 -0
- package/dist/container/types.d.ts +25 -0
- package/dist/container/types.d.ts.map +1 -0
- package/dist/container/types.js +6 -0
- package/dist/container/types.js.map +1 -0
- package/dist/decorators/Lazy.d.ts +19 -0
- package/dist/decorators/Lazy.d.ts.map +1 -0
- package/dist/decorators/Lazy.js +53 -0
- package/dist/decorators/Lazy.js.map +1 -0
- package/dist/decorators/Service.d.ts +48 -0
- package/dist/decorators/Service.d.ts.map +1 -0
- package/dist/decorators/Service.js +70 -0
- package/dist/decorators/Service.js.map +1 -0
- package/dist/errors/FuzzyMatcher.d.ts +41 -0
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -0
- package/dist/errors/FuzzyMatcher.js +81 -0
- package/dist/errors/FuzzyMatcher.js.map +1 -0
- package/dist/errors/ModuleErrors.d.ts +41 -0
- package/dist/errors/ModuleErrors.d.ts.map +1 -0
- package/dist/errors/ModuleErrors.js +65 -0
- package/dist/errors/ModuleErrors.js.map +1 -0
- package/dist/errors/PipelineStageError.d.ts +31 -0
- package/dist/errors/PipelineStageError.d.ts.map +1 -0
- package/dist/errors/PipelineStageError.js +77 -0
- package/dist/errors/PipelineStageError.js.map +1 -0
- package/dist/errors/ReamError.d.ts +50 -0
- package/dist/errors/ReamError.d.ts.map +1 -0
- package/dist/errors/ReamError.js +108 -0
- package/dist/errors/ReamError.js.map +1 -0
- package/dist/events/Emitter.d.ts +124 -0
- package/dist/events/Emitter.d.ts.map +1 -0
- package/dist/events/Emitter.js +271 -0
- package/dist/events/Emitter.js.map +1 -0
- package/dist/events/EventsProvider.d.ts +28 -0
- package/dist/events/EventsProvider.d.ts.map +1 -0
- package/dist/events/EventsProvider.js +36 -0
- package/dist/events/EventsProvider.js.map +1 -0
- package/dist/events/config.d.ts +8 -0
- package/dist/events/config.d.ts.map +1 -0
- package/dist/events/config.js +4 -0
- package/dist/events/config.js.map +1 -0
- package/dist/events/configure.d.ts +10 -0
- package/dist/events/configure.d.ts.map +1 -0
- package/dist/events/configure.js +11 -0
- package/dist/events/configure.js.map +1 -0
- package/dist/events/helix/helpers.d.ts +62 -0
- package/dist/events/helix/helpers.d.ts.map +1 -0
- package/dist/events/helix/helpers.js +187 -0
- package/dist/events/helix/helpers.js.map +1 -0
- package/dist/events/index.d.ts +17 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +15 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/native.d.ts +21 -0
- package/dist/events/native.d.ts.map +1 -0
- package/dist/events/native.js +17 -0
- package/dist/events/native.js.map +1 -0
- package/dist/events/services/main.d.ts +18 -0
- package/dist/events/services/main.d.ts.map +1 -0
- package/dist/events/services/main.js +31 -0
- package/dist/events/services/main.js.map +1 -0
- package/dist/events/testing/FakeBus.d.ts +66 -0
- package/dist/events/testing/FakeBus.d.ts.map +1 -0
- package/dist/events/testing/FakeBus.js +356 -0
- package/dist/events/testing/FakeBus.js.map +1 -0
- package/dist/graphql/GraphQLEngine.d.ts +69 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -0
- package/dist/graphql/GraphQLEngine.js +309 -0
- package/dist/graphql/GraphQLEngine.js.map +1 -0
- package/dist/graphql/GraphQLProvider.d.ts +32 -0
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -0
- package/dist/graphql/GraphQLProvider.js +49 -0
- package/dist/graphql/GraphQLProvider.js.map +1 -0
- package/dist/helpers/base64.d.ts +40 -0
- package/dist/helpers/base64.d.ts.map +1 -0
- package/dist/helpers/base64.js +70 -0
- package/dist/helpers/base64.js.map +1 -0
- package/dist/helpers/index.d.ts +7 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +5 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/helpers/is.d.ts +101 -0
- package/dist/helpers/is.d.ts.map +1 -0
- package/dist/helpers/is.js +151 -0
- package/dist/helpers/is.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts +46 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -0
- package/dist/helpers/napi-loader.js +69 -0
- package/dist/helpers/napi-loader.js.map +1 -0
- package/dist/helpers/safeEqual.d.ts +9 -0
- package/dist/helpers/safeEqual.d.ts.map +1 -0
- package/dist/helpers/safeEqual.js +16 -0
- package/dist/helpers/safeEqual.js.map +1 -0
- package/dist/helpers/string.d.ts +161 -0
- package/dist/helpers/string.d.ts.map +1 -0
- package/dist/helpers/string.js +429 -0
- package/dist/helpers/string.js.map +1 -0
- package/dist/helpers/types.d.ts +83 -0
- package/dist/helpers/types.d.ts.map +1 -0
- package/dist/helpers/types.js +6 -0
- package/dist/helpers/types.js.map +1 -0
- package/dist/http/Exception.d.ts +103 -0
- package/dist/http/Exception.d.ts.map +1 -0
- package/dist/http/Exception.js +228 -0
- package/dist/http/Exception.js.map +1 -0
- package/dist/http/HttpContext.d.ts +69 -0
- package/dist/http/HttpContext.d.ts.map +1 -0
- package/dist/http/HttpContext.js +55 -0
- package/dist/http/HttpContext.js.map +1 -0
- package/dist/http/RedirectBuilder.d.ts +31 -0
- package/dist/http/RedirectBuilder.d.ts.map +1 -0
- package/dist/http/RedirectBuilder.js +75 -0
- package/dist/http/RedirectBuilder.js.map +1 -0
- package/dist/http/Request.d.ts +160 -0
- package/dist/http/Request.d.ts.map +1 -0
- package/dist/http/Request.js +484 -0
- package/dist/http/Request.js.map +1 -0
- package/dist/http/Response.d.ts +114 -0
- package/dist/http/Response.d.ts.map +1 -0
- package/dist/http/Response.js +303 -0
- package/dist/http/Response.js.map +1 -0
- package/dist/http/SseStream.d.ts +89 -0
- package/dist/http/SseStream.d.ts.map +1 -0
- package/dist/http/SseStream.js +228 -0
- package/dist/http/SseStream.js.map +1 -0
- package/dist/http/urlDecode.d.ts +8 -0
- package/dist/http/urlDecode.d.ts.map +1 -0
- package/dist/http/urlDecode.js +18 -0
- package/dist/http/urlDecode.js.map +1 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/Pipeline.d.ts +36 -0
- package/dist/middleware/Pipeline.d.ts.map +1 -0
- package/dist/middleware/Pipeline.js +104 -0
- package/dist/middleware/Pipeline.js.map +1 -0
- package/dist/openapi/OpenApiGenerator.d.ts +58 -0
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -0
- package/dist/openapi/OpenApiGenerator.js +192 -0
- package/dist/openapi/OpenApiGenerator.js.map +1 -0
- package/dist/openapi/OpenApiMiddleware.d.ts +24 -0
- package/dist/openapi/OpenApiMiddleware.d.ts.map +1 -0
- package/dist/openapi/OpenApiMiddleware.js +78 -0
- package/dist/openapi/OpenApiMiddleware.js.map +1 -0
- package/dist/openapi/OpenApiProvider.d.ts +36 -0
- package/dist/openapi/OpenApiProvider.d.ts.map +1 -0
- package/dist/openapi/OpenApiProvider.js +42 -0
- package/dist/openapi/OpenApiProvider.js.map +1 -0
- package/dist/resources/ApiResource.d.ts +45 -0
- package/dist/resources/ApiResource.d.ts.map +1 -0
- package/dist/resources/ApiResource.js +58 -0
- package/dist/resources/ApiResource.js.map +1 -0
- package/dist/router/Router.d.ts +205 -0
- package/dist/router/Router.d.ts.map +1 -0
- package/dist/router/Router.js +552 -0
- package/dist/router/Router.js.map +1 -0
- package/dist/rpc/RpcProvider.d.ts +34 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -0
- package/dist/rpc/RpcProvider.js +37 -0
- package/dist/rpc/RpcProvider.js.map +1 -0
- package/dist/rpc/RpcRouter.d.ts +61 -0
- package/dist/rpc/RpcRouter.d.ts.map +1 -0
- package/dist/rpc/RpcRouter.js +187 -0
- package/dist/rpc/RpcRouter.js.map +1 -0
- package/dist/scheduler/Schedule.d.ts +53 -0
- package/dist/scheduler/Schedule.d.ts.map +1 -0
- package/dist/scheduler/Schedule.js +76 -0
- package/dist/scheduler/Schedule.js.map +1 -0
- package/dist/scheduler/ScheduleProvider.d.ts +56 -0
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -0
- package/dist/scheduler/ScheduleProvider.js +145 -0
- package/dist/scheduler/ScheduleProvider.js.map +1 -0
- package/dist/scheduler/Scheduler.d.ts +209 -0
- package/dist/scheduler/Scheduler.d.ts.map +1 -0
- package/dist/scheduler/Scheduler.js +432 -0
- package/dist/scheduler/Scheduler.js.map +1 -0
- package/dist/scheduler/index.d.ts +14 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +6 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/locks/LockBackend.d.ts +37 -0
- package/dist/scheduler/locks/LockBackend.d.ts.map +1 -0
- package/dist/scheduler/locks/LockBackend.js +13 -0
- package/dist/scheduler/locks/LockBackend.js.map +1 -0
- package/dist/scheduler/locks/MemoryLockBackend.d.ts +39 -0
- package/dist/scheduler/locks/MemoryLockBackend.d.ts.map +1 -0
- package/dist/scheduler/locks/MemoryLockBackend.js +71 -0
- package/dist/scheduler/locks/MemoryLockBackend.js.map +1 -0
- package/dist/scheduler/observability/ErrorReporter.d.ts +15 -0
- package/dist/scheduler/observability/ErrorReporter.d.ts.map +1 -0
- package/dist/scheduler/observability/ErrorReporter.js +13 -0
- package/dist/scheduler/observability/ErrorReporter.js.map +1 -0
- package/dist/scheduler/observability/ScheduleEvent.d.ts +57 -0
- package/dist/scheduler/observability/ScheduleEvent.d.ts.map +1 -0
- package/dist/scheduler/observability/ScheduleEvent.js +12 -0
- package/dist/scheduler/observability/ScheduleEvent.js.map +1 -0
- package/dist/scheduler/observability/ScheduleEventSink.d.ts +20 -0
- package/dist/scheduler/observability/ScheduleEventSink.d.ts.map +1 -0
- package/dist/scheduler/observability/ScheduleEventSink.js +17 -0
- package/dist/scheduler/observability/ScheduleEventSink.js.map +1 -0
- package/dist/scheduler/observability/StatsTracker.d.ts +50 -0
- package/dist/scheduler/observability/StatsTracker.d.ts.map +1 -0
- package/dist/scheduler/observability/StatsTracker.js +96 -0
- package/dist/scheduler/observability/StatsTracker.js.map +1 -0
- package/dist/security/CookieSigner.d.ts +13 -0
- package/dist/security/CookieSigner.d.ts.map +1 -0
- package/dist/security/CookieSigner.js +51 -0
- package/dist/security/CookieSigner.js.map +1 -0
- package/dist/security/SignedUrl.d.ts +17 -0
- package/dist/security/SignedUrl.d.ts.map +1 -0
- package/dist/security/SignedUrl.js +63 -0
- package/dist/security/SignedUrl.js.map +1 -0
- package/dist/security/crypto.d.ts +25 -0
- package/dist/security/crypto.d.ts.map +1 -0
- package/dist/security/crypto.js +82 -0
- package/dist/security/crypto.js.map +1 -0
- package/dist/server/HyperServer.d.ts +64 -0
- package/dist/server/HyperServer.d.ts.map +1 -0
- package/dist/server/HyperServer.js +54 -0
- package/dist/server/HyperServer.js.map +1 -0
- package/dist/server/Server.d.ts +68 -0
- package/dist/server/Server.d.ts.map +1 -0
- package/dist/server/Server.js +113 -0
- package/dist/server/Server.js.map +1 -0
- package/dist/services/app.d.ts +14 -0
- package/dist/services/app.d.ts.map +1 -0
- package/dist/services/app.js +27 -0
- package/dist/services/app.js.map +1 -0
- package/dist/services/router.d.ts +21 -0
- package/dist/services/router.d.ts.map +1 -0
- package/dist/services/router.js +36 -0
- package/dist/services/router.js.map +1 -0
- package/dist/services/server.d.ts +16 -0
- package/dist/services/server.d.ts.map +1 -0
- package/dist/services/server.js +32 -0
- package/dist/services/server.js.map +1 -0
- package/dist/session/Session.d.ts +88 -0
- package/dist/session/Session.d.ts.map +1 -0
- package/dist/session/Session.js +186 -0
- package/dist/session/Session.js.map +1 -0
- package/dist/session/SessionMiddleware.d.ts +16 -0
- package/dist/session/SessionMiddleware.d.ts.map +1 -0
- package/dist/session/SessionMiddleware.js +132 -0
- package/dist/session/SessionMiddleware.js.map +1 -0
- package/dist/session/drivers/CookieDriver.d.ts +17 -0
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -0
- package/dist/session/drivers/CookieDriver.js +42 -0
- package/dist/session/drivers/CookieDriver.js.map +1 -0
- package/dist/session/drivers/MemoryDriver.d.ts +9 -0
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -0
- package/dist/session/drivers/MemoryDriver.js +24 -0
- package/dist/session/drivers/MemoryDriver.js.map +1 -0
- package/dist/storage/S3Driver.d.ts +27 -0
- package/dist/storage/S3Driver.d.ts.map +1 -0
- package/dist/storage/S3Driver.js +135 -0
- package/dist/storage/S3Driver.js.map +1 -0
- package/dist/storage/StaticMiddleware.d.ts +27 -0
- package/dist/storage/StaticMiddleware.d.ts.map +1 -0
- package/dist/storage/StaticMiddleware.js +153 -0
- package/dist/storage/StaticMiddleware.js.map +1 -0
- package/dist/storage/StaticProvider.d.ts +26 -0
- package/dist/storage/StaticProvider.d.ts.map +1 -0
- package/dist/storage/StaticProvider.js +31 -0
- package/dist/storage/StaticProvider.js.map +1 -0
- package/dist/storage/StorageManager.d.ts +31 -0
- package/dist/storage/StorageManager.d.ts.map +1 -0
- package/dist/storage/StorageManager.js +120 -0
- package/dist/storage/StorageManager.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +67 -0
- package/dist/testing/RequestBuilder.d.ts.map +1 -0
- package/dist/testing/RequestBuilder.js +202 -0
- package/dist/testing/RequestBuilder.js.map +1 -0
- package/dist/testing/TestClient.d.ts +96 -0
- package/dist/testing/TestClient.d.ts.map +1 -0
- package/dist/testing/TestClient.js +263 -0
- package/dist/testing/TestClient.js.map +1 -0
- package/dist/types/helpers.d.ts +47 -0
- package/dist/types/helpers.d.ts.map +1 -0
- package/dist/types/helpers.js +5 -0
- package/dist/types/helpers.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +106 -0
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/copy-napi.mjs +113 -0
- package/src/Application.ts +177 -0
- package/src/Codemods.ts +343 -0
- package/src/ConfigLoader.ts +49 -0
- package/src/ErrorBoundary.ts +157 -0
- package/src/GracefulShutdown.ts +80 -0
- package/src/HealthCheck.ts +96 -0
- package/src/HotReload.ts +78 -0
- package/src/HttpKernel.ts +305 -0
- package/src/Ignitor.ts +656 -0
- package/src/Provider.ts +74 -0
- package/src/bodyparser/BodyParserMiddleware.ts +183 -0
- package/src/bodyparser/MultipartFile.ts +181 -0
- package/src/bodyparser/parseSize.ts +19 -0
- package/src/bootstrap/hyper-server.ts +78 -0
- package/src/console/CommandRunner.ts +82 -0
- package/src/container/Container.ts +383 -0
- package/src/container/types.ts +30 -0
- package/src/decorators/Lazy.ts +57 -0
- package/src/decorators/Service.ts +85 -0
- package/src/errors/FuzzyMatcher.ts +93 -0
- package/src/errors/ModuleErrors.ts +73 -0
- package/src/errors/PipelineStageError.ts +97 -0
- package/src/errors/ReamError.ts +138 -0
- package/src/events/Emitter.ts +333 -0
- package/src/events/EventsProvider.ts +58 -0
- package/src/events/config.ts +10 -0
- package/src/events/configure.ts +19 -0
- package/src/events/helix/helpers.ts +260 -0
- package/src/events/index.ts +22 -0
- package/src/events/native.ts +37 -0
- package/src/events/services/main.ts +39 -0
- package/src/events/testing/FakeBus.ts +420 -0
- package/src/graphql/GraphQLEngine.ts +421 -0
- package/src/graphql/GraphQLProvider.ts +59 -0
- package/src/helpers/base64.ts +72 -0
- package/src/helpers/index.ts +6 -0
- package/src/helpers/is.ts +162 -0
- package/src/helpers/napi-loader.ts +106 -0
- package/src/helpers/safeEqual.ts +15 -0
- package/src/helpers/string.ts +469 -0
- package/src/helpers/types.ts +100 -0
- package/src/http/Exception.ts +272 -0
- package/src/http/HttpContext.ts +108 -0
- package/src/http/RedirectBuilder.ts +89 -0
- package/src/http/Request.ts +540 -0
- package/src/http/Response.ts +351 -0
- package/src/http/SseStream.ts +250 -0
- package/src/http/urlDecode.ts +18 -0
- package/src/index.ts +263 -0
- package/src/middleware/Pipeline.ts +136 -0
- package/src/openapi/OpenApiGenerator.ts +254 -0
- package/src/openapi/OpenApiMiddleware.ts +101 -0
- package/src/openapi/OpenApiProvider.ts +67 -0
- package/src/resources/ApiResource.ts +79 -0
- package/src/router/Router.ts +733 -0
- package/src/rpc/RpcProvider.ts +54 -0
- package/src/rpc/RpcRouter.ts +243 -0
- package/src/scheduler/Schedule.ts +96 -0
- package/src/scheduler/ScheduleProvider.ts +177 -0
- package/src/scheduler/Scheduler.ts +601 -0
- package/src/scheduler/index.ts +24 -0
- package/src/scheduler/locks/LockBackend.ts +38 -0
- package/src/scheduler/locks/MemoryLockBackend.ts +85 -0
- package/src/scheduler/observability/ErrorReporter.ts +13 -0
- package/src/scheduler/observability/ScheduleEvent.ts +61 -0
- package/src/scheduler/observability/ScheduleEventSink.ts +21 -0
- package/src/scheduler/observability/StatsTracker.ts +134 -0
- package/src/security/CookieSigner.ts +54 -0
- package/src/security/SignedUrl.ts +71 -0
- package/src/security/crypto.ts +98 -0
- package/src/server/HyperServer.ts +121 -0
- package/src/server/Server.ts +147 -0
- package/src/services/app.ts +34 -0
- package/src/services/router.ts +44 -0
- package/src/services/server.ts +40 -0
- package/src/session/Session.ts +231 -0
- package/src/session/SessionMiddleware.ts +153 -0
- package/src/session/drivers/CookieDriver.ts +48 -0
- package/src/session/drivers/MemoryDriver.ts +29 -0
- package/src/storage/S3Driver.ts +171 -0
- package/src/storage/StaticMiddleware.ts +182 -0
- package/src/storage/StaticProvider.ts +40 -0
- package/src/storage/StorageManager.ts +142 -0
- package/src/testing/RequestBuilder.ts +275 -0
- package/src/testing/TestClient.ts +344 -0
- package/src/types/helpers.ts +52 -0
- package/swcrc.app.json +17 -0
- 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
|
+
}
|