@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server — manages HTTP server middleware, error handler, and router.
|
|
3
|
+
*
|
|
4
|
+
* Like AdonisJS Server class:
|
|
5
|
+
* - server.use([() => import('#middleware/...')]) — lazy class middleware
|
|
6
|
+
* - server.errorHandler(() => import('#exceptions/handler')) — custom exception handler
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExceptionHandler } from '../http/Exception.js'
|
|
10
|
+
import type { HttpContext } from '../http/HttpContext.js'
|
|
11
|
+
import type { MiddlewareFunction } from '../middleware/Pipeline.js'
|
|
12
|
+
import type { Router } from '../router/Router.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A middleware class with a handle method.
|
|
16
|
+
* This is the pattern used by AdonisJS middleware.
|
|
17
|
+
*/
|
|
18
|
+
export interface MiddlewareClass {
|
|
19
|
+
handle(ctx: HttpContext, next: () => Promise<void>): Promise<void> | void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Lazy import returning a module with a default export. */
|
|
23
|
+
export type LazyImport<T> = () => Promise<{ default: T }>
|
|
24
|
+
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: contravariance — same as ControllerAction
|
|
26
|
+
export type ErrorHandlerClass = new (...args: any[]) => ExceptionHandler
|
|
27
|
+
|
|
28
|
+
// biome-ignore lint/suspicious/noExplicitAny: contravariance — middleware class constructors
|
|
29
|
+
type MiddlewareClassConstructor = new (...args: any[]) => MiddlewareClass
|
|
30
|
+
|
|
31
|
+
/** What server.use() and router.use() accept: lazy imports or direct functions. */
|
|
32
|
+
export type MiddlewareEntry = LazyImport<MiddlewareClassConstructor> | MiddlewareFunction
|
|
33
|
+
|
|
34
|
+
export class Server {
|
|
35
|
+
#router: Router
|
|
36
|
+
#serverMiddleware: MiddlewareFunction[] = []
|
|
37
|
+
#errorHandlerImporter?: LazyImport<ErrorHandlerClass>
|
|
38
|
+
#resolvedErrorHandler?: ExceptionHandler
|
|
39
|
+
#trustedProxies: string[] = []
|
|
40
|
+
|
|
41
|
+
constructor(router: Router) {
|
|
42
|
+
this.#router = router
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configure the trusted-proxy CIDR list. The Rust HyperServer uses it to
|
|
47
|
+
* pre-resolve `request.ip` once per request — JS reads the field, no CIDR
|
|
48
|
+
* matching in TS.
|
|
49
|
+
*
|
|
50
|
+
* Empty list (default, 2026-05+) is **strict fail-closed**: `X-Forwarded-For`
|
|
51
|
+
* and `X-Real-IP` are ignored entirely; only the socket peer is used.
|
|
52
|
+
* Apps deployed behind a reverse proxy must list the proxy's CIDR
|
|
53
|
+
* (`'10.0.0.0/8'`, `'192.0.2.0/24'`, etc.) or pass the `'*'` sentinel
|
|
54
|
+
* for permissive mode (accepts spoofing as the explicit trade-off when
|
|
55
|
+
* proxy CIDRs can't be enumerated).
|
|
56
|
+
*/
|
|
57
|
+
trustedProxies(cidrs: string[]): this {
|
|
58
|
+
this.#trustedProxies = [...cidrs]
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read the current trusted-proxy list (consumed by Ignitor at boot). */
|
|
63
|
+
getTrustedProxies(): readonly string[] {
|
|
64
|
+
return this.#trustedProxies
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register global server middleware.
|
|
69
|
+
* Accepts lazy imports of middleware classes (AdonisJS pattern):
|
|
70
|
+
* server.use([() => import('#middleware/log_request_middleware')])
|
|
71
|
+
*/
|
|
72
|
+
use(middleware: MiddlewareEntry[]): this {
|
|
73
|
+
for (const mw of middleware) {
|
|
74
|
+
this.#serverMiddleware.push(resolveMiddlewareEntry(mw))
|
|
75
|
+
}
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register a custom error handler.
|
|
81
|
+
* server.errorHandler(() => import('#exceptions/handler'))
|
|
82
|
+
*/
|
|
83
|
+
errorHandler(handler: LazyImport<ErrorHandlerClass>): this {
|
|
84
|
+
this.#errorHandlerImporter = handler
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Boot — resolve the error handler. Called by Ignitor during ready phase. */
|
|
89
|
+
async boot(): Promise<void> {
|
|
90
|
+
if (this.#errorHandlerImporter) {
|
|
91
|
+
const mod = await this.#errorHandlerImporter()
|
|
92
|
+
const HandlerClass = mod.default
|
|
93
|
+
this.#resolvedErrorHandler = new HandlerClass()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getServerMiddleware(): MiddlewareFunction[] {
|
|
98
|
+
return [...this.#serverMiddleware]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getErrorHandler(): ExceptionHandler | undefined {
|
|
102
|
+
return this.#resolvedErrorHandler
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getRouter(): Router {
|
|
106
|
+
return this.#router
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert a MiddlewareEntry (lazy import or function) into a MiddlewareFunction.
|
|
112
|
+
* Lazy imports are resolved on first call and cached.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveMiddlewareEntry(entry: MiddlewareEntry): MiddlewareFunction {
|
|
115
|
+
// Direct middleware function (2 params: ctx, next)
|
|
116
|
+
if (isMiddlewareFunction(entry)) {
|
|
117
|
+
return entry
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Lazy import of a middleware class — resolve + cache on first invocation
|
|
121
|
+
// After isMiddlewareFunction returns false, entry is narrowed to LazyImport<MiddlewareClassConstructor>
|
|
122
|
+
const lazyImport = entry
|
|
123
|
+
let cached: MiddlewareClass | undefined
|
|
124
|
+
return async (ctx: HttpContext, next: () => Promise<void>) => {
|
|
125
|
+
if (!cached) {
|
|
126
|
+
const mod = await lazyImport()
|
|
127
|
+
cached = new mod.default()
|
|
128
|
+
}
|
|
129
|
+
await cached.handle(ctx, next)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Check if an entry is a direct middleware function (not a lazy import). */
|
|
134
|
+
function isMiddlewareFunction(entry: MiddlewareEntry): entry is MiddlewareFunction {
|
|
135
|
+
// A lazy import factory returns a promise (dynamic import). Middleware functions don't.
|
|
136
|
+
// We check: if calling it with no args returns a thenable, it's a lazy import.
|
|
137
|
+
// Better heuristic: lazy imports are () => import(...) which have .length === 0
|
|
138
|
+
// and middleware functions have .length >= 2 (ctx, next).
|
|
139
|
+
// Edge case: arrow with defaults has length 1. But lazy import() always has length 0.
|
|
140
|
+
if (typeof entry !== 'function') return false
|
|
141
|
+
// Functions with 2+ params are definitely middleware
|
|
142
|
+
if (entry.length >= 2) return true
|
|
143
|
+
// Functions with 0 params are likely lazy imports
|
|
144
|
+
if (entry.length === 0) return false
|
|
145
|
+
// 1 param is ambiguous — treat as middleware (ctx only, no next)
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application singleton service.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import app from '@c9up/ream/services/app'
|
|
6
|
+
* const logger = app.container.make('logger')
|
|
7
|
+
* if (app.inProduction) { ... }
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Application } from '../Application.js'
|
|
11
|
+
|
|
12
|
+
let instance: Application | undefined
|
|
13
|
+
|
|
14
|
+
/** @internal Set the app instance (called by Ignitor). */
|
|
15
|
+
export function setApp(app: Application): void {
|
|
16
|
+
instance = app
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const app: Application = new Proxy({} as Application, {
|
|
20
|
+
get(_target, prop) {
|
|
21
|
+
if (!instance) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Application accessed before initialization. ' +
|
|
24
|
+
'Ensure this code runs during or after the boot phase.',
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
// Bind methods so private-field writes inside the class resolve
|
|
28
|
+
// against the real instance brand instead of the Proxy.
|
|
29
|
+
const value = Reflect.get(instance, prop, instance)
|
|
30
|
+
return typeof value === 'function' ? value.bind(instance) : value
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export default app
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router singleton service — proxy to the Ignitor's router instance.
|
|
3
|
+
*
|
|
4
|
+
* Usage in module route files:
|
|
5
|
+
* import router from '@c9up/ream/services/router'
|
|
6
|
+
* router.get('/tasks', [TasksController, 'index'])
|
|
7
|
+
*
|
|
8
|
+
* The proxy is initialized by Ignitor before preload files are imported.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Router } from '../router/Router.js'
|
|
12
|
+
|
|
13
|
+
let instance: Router | undefined
|
|
14
|
+
|
|
15
|
+
/** @internal Set the router instance (called by Ignitor). */
|
|
16
|
+
export function setRouter(router: Router): void {
|
|
17
|
+
instance = router
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @internal Get the router instance directly. */
|
|
21
|
+
export function getRouter(): Router | undefined {
|
|
22
|
+
return instance
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Router proxy — defers all property access to the underlying instance.
|
|
27
|
+
* Throws if accessed before Ignitor initializes it.
|
|
28
|
+
*/
|
|
29
|
+
const router: Router = new Proxy({} as Router, {
|
|
30
|
+
get(_target, prop) {
|
|
31
|
+
if (!instance) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Router accessed before initialization. ' +
|
|
34
|
+
'Ensure your route files are loaded as preloads in reamrc.ts, not at import time.',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
// Bind methods so private-field writes inside the class resolve
|
|
38
|
+
// against the real instance brand instead of the Proxy.
|
|
39
|
+
const value = Reflect.get(instance, prop, instance)
|
|
40
|
+
return typeof value === 'function' ? value.bind(instance) : value
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export default router
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server singleton service — proxy to the Ignitor's Server instance.
|
|
3
|
+
*
|
|
4
|
+
* Usage in start/kernel.ts:
|
|
5
|
+
* import server from '@c9up/ream/services/server'
|
|
6
|
+
* server.errorHandler(() => import('#exceptions/handler'))
|
|
7
|
+
* server.use([() => import('#middleware/logging')])
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Server } from '../server/Server.js'
|
|
11
|
+
|
|
12
|
+
let instance: Server | undefined
|
|
13
|
+
|
|
14
|
+
/** @internal Set the server instance (called by Ignitor). */
|
|
15
|
+
export function setServer(server: Server): void {
|
|
16
|
+
instance = server
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @internal Get the server instance directly. */
|
|
20
|
+
export function getServer(): Server | undefined {
|
|
21
|
+
return instance
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const server: Server = new Proxy({} as Server, {
|
|
25
|
+
get(_target, prop) {
|
|
26
|
+
if (!instance) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Server accessed before initialization. ' +
|
|
29
|
+
'Ensure your kernel files are loaded as preloads in reamrc.ts.',
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
// Bind methods to the real instance so private-field access inside
|
|
33
|
+
// them resolves against the underlying class brand — passing the
|
|
34
|
+
// Proxy as `this` breaks `#field` writes (Proxy isn't branded).
|
|
35
|
+
const value = Reflect.get(instance, prop, instance)
|
|
36
|
+
return typeof value === 'function' ? value.bind(instance) : value
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export default server
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session — server-side session management.
|
|
3
|
+
*
|
|
4
|
+
* Like AdonisJS Session:
|
|
5
|
+
* session.get('key')
|
|
6
|
+
* session.put('key', value)
|
|
7
|
+
* session.flash('success', 'Saved!')
|
|
8
|
+
* session.forget('key')
|
|
9
|
+
*
|
|
10
|
+
* Backed by pluggable drivers (cookie, memory, redis).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface SessionDriver {
|
|
14
|
+
read(sessionId: string): Promise<Record<string, unknown>>
|
|
15
|
+
write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void>
|
|
16
|
+
destroy(sessionId: string): Promise<void>
|
|
17
|
+
touch(sessionId: string, ttl: number): Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SessionConfig {
|
|
21
|
+
driver: string
|
|
22
|
+
cookieName?: string
|
|
23
|
+
maxAge?: number // seconds, default 7200 (2h)
|
|
24
|
+
clearWithBrowser?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* When true, re-emit Set-Cookie on every response so the cookie's `maxAge`
|
|
27
|
+
* window slides forward on each request. Active users won't be logged out
|
|
28
|
+
* mid-session; the trade-off is a `Vary: Cookie` side-channel on every
|
|
29
|
+
* response (defeats shared HTTP caches). Default `false` to preserve cache
|
|
30
|
+
* behaviour. Express-session and AdonisJS expose the same toggle.
|
|
31
|
+
*/
|
|
32
|
+
rolling?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a fresh session ID. Format mirrors AdonisJS / express-session —
|
|
37
|
+
* 32 bytes of base64url entropy (~256 bits). Defined here so `regenerate()`
|
|
38
|
+
* can pick a new id without bringing in a driver dependency.
|
|
39
|
+
*/
|
|
40
|
+
function generateSessionId(): string {
|
|
41
|
+
const bytes = new Uint8Array(32)
|
|
42
|
+
// `crypto.getRandomValues` is a Node ≥ 19 builtin AND the standard browser
|
|
43
|
+
// API; same call site works on both runtimes without a polyfill.
|
|
44
|
+
crypto.getRandomValues(bytes)
|
|
45
|
+
let str = ''
|
|
46
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
47
|
+
str += String.fromCharCode(bytes[i] ?? 0)
|
|
48
|
+
}
|
|
49
|
+
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Session {
|
|
53
|
+
#data: Record<string, unknown> = {}
|
|
54
|
+
#flashData: Record<string, unknown> = {}
|
|
55
|
+
#previousFlash: Record<string, unknown> = {}
|
|
56
|
+
#dirty = false
|
|
57
|
+
#sessionId: string
|
|
58
|
+
/**
|
|
59
|
+
* The session id active when the request arrived, captured at construction.
|
|
60
|
+
* The middleware uses this on the way out to migrate driver storage
|
|
61
|
+
* (`driver.write(newId)` + `driver.destroy(oldId)`) when the application
|
|
62
|
+
* called `regenerate()` mid-request.
|
|
63
|
+
*/
|
|
64
|
+
readonly #originalSessionId: string
|
|
65
|
+
#regenerated = false
|
|
66
|
+
|
|
67
|
+
constructor(sessionId: string, data: Record<string, unknown> = {}) {
|
|
68
|
+
this.#sessionId = sessionId
|
|
69
|
+
this.#originalSessionId = sessionId
|
|
70
|
+
this.#data = { ...data }
|
|
71
|
+
const flash = data.__flash
|
|
72
|
+
this.#previousFlash =
|
|
73
|
+
typeof flash === 'object' && flash !== null && !Array.isArray(flash)
|
|
74
|
+
? // biome-ignore lint/suspicious/noExplicitAny: flash narrowed to non-null non-array object; branded as Record for key access
|
|
75
|
+
(flash as any as Record<string, unknown>)
|
|
76
|
+
: {}
|
|
77
|
+
delete this.#data.__flash
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Current session id. Mutated by `regenerate()`. */
|
|
81
|
+
get sessionId(): string {
|
|
82
|
+
return this.#sessionId
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Rotate the session id while preserving the data. The standard mitigation
|
|
87
|
+
* for session-fixation (CWE-384): call this whenever a privilege boundary
|
|
88
|
+
* is crossed — login, role change, sensitive-action confirmation. Warden's
|
|
89
|
+
* `SessionStrategy.login()` calls it automatically before writing the
|
|
90
|
+
* authenticated user id.
|
|
91
|
+
*
|
|
92
|
+
* The new id takes effect inside this Session instance immediately so
|
|
93
|
+
* subsequent `session.get()` / `session.put()` see the same data under
|
|
94
|
+
* the new id. The SessionMiddleware sees `wasRegenerated() === true` on
|
|
95
|
+
* the way out and migrates the driver storage + emits a new Set-Cookie.
|
|
96
|
+
*/
|
|
97
|
+
regenerate(): void {
|
|
98
|
+
this.#sessionId = generateSessionId()
|
|
99
|
+
this.#regenerated = true
|
|
100
|
+
this.#dirty = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @internal — read by SessionMiddleware to detect a mid-request rotation. */
|
|
104
|
+
wasRegenerated(): boolean {
|
|
105
|
+
return this.#regenerated
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal — the id captured at request arrival; used to destroy the old driver entry post-regenerate. */
|
|
109
|
+
originalSessionId(): string {
|
|
110
|
+
return this.#originalSessionId
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Get a session value. */
|
|
114
|
+
get<T = unknown>(key: string, defaultValue?: T): T {
|
|
115
|
+
if (key in this.#data) {
|
|
116
|
+
// biome-ignore lint/suspicious/noExplicitAny: session data stored as unknown; caller brands the value type via T
|
|
117
|
+
return this.#data[key] as any as T
|
|
118
|
+
}
|
|
119
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic default — caller brands the value type via T
|
|
120
|
+
return defaultValue as any as T
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Set a session value. */
|
|
124
|
+
put(key: string, value: unknown): void {
|
|
125
|
+
this.#data[key] = value
|
|
126
|
+
this.#dirty = true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Check if a key exists. */
|
|
130
|
+
has(key: string): boolean {
|
|
131
|
+
return key in this.#data
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Get all session data. */
|
|
135
|
+
all(): Record<string, unknown> {
|
|
136
|
+
return { ...this.#data }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Remove a key. */
|
|
140
|
+
forget(key: string): void {
|
|
141
|
+
delete this.#data[key]
|
|
142
|
+
this.#dirty = true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Get and remove a key. */
|
|
146
|
+
pull<T = unknown>(key: string, defaultValue?: T): T {
|
|
147
|
+
const value = this.get<T>(key, defaultValue)
|
|
148
|
+
this.forget(key)
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Clear all session data. */
|
|
153
|
+
clear(): void {
|
|
154
|
+
this.#data = {}
|
|
155
|
+
this.#dirty = true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Increment a numeric value. */
|
|
159
|
+
increment(key: string, amount = 1): void {
|
|
160
|
+
const current = this.get<number>(key) ?? 0
|
|
161
|
+
this.put(key, current + amount)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Decrement a numeric value. */
|
|
165
|
+
decrement(key: string, amount = 1): void {
|
|
166
|
+
this.increment(key, -amount)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Flash data ───────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** Set flash data (available only on the next request). */
|
|
172
|
+
flash(key: string, value: unknown): void {
|
|
173
|
+
this.#flashData[key] = value
|
|
174
|
+
this.#dirty = true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Flash all current input. */
|
|
178
|
+
flashAll(input: Record<string, unknown>): void {
|
|
179
|
+
for (const [k, v] of Object.entries(input)) {
|
|
180
|
+
this.#flashData[k] = v
|
|
181
|
+
}
|
|
182
|
+
this.#dirty = true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Flash only specific keys. */
|
|
186
|
+
flashOnly(input: Record<string, unknown>, keys: string[]): void {
|
|
187
|
+
for (const k of keys) {
|
|
188
|
+
if (k in input) this.#flashData[k] = input[k]
|
|
189
|
+
}
|
|
190
|
+
this.#dirty = true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Flash all except specific keys. */
|
|
194
|
+
flashExcept(input: Record<string, unknown>, keys: string[]): void {
|
|
195
|
+
const keySet = new Set(keys)
|
|
196
|
+
for (const [k, v] of Object.entries(input)) {
|
|
197
|
+
if (!keySet.has(k)) this.#flashData[k] = v
|
|
198
|
+
}
|
|
199
|
+
this.#dirty = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Get a flashed value from the previous request. */
|
|
203
|
+
flashMessages(): Record<string, unknown> {
|
|
204
|
+
return { ...this.#previousFlash }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Get a specific flash message from the previous request. */
|
|
208
|
+
old<T = unknown>(key: string, defaultValue?: T): T {
|
|
209
|
+
if (key in this.#previousFlash) {
|
|
210
|
+
// biome-ignore lint/suspicious/noExplicitAny: flash data stored as unknown; caller brands the value type via T
|
|
211
|
+
return this.#previousFlash[key] as any as T
|
|
212
|
+
}
|
|
213
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic default — caller brands the value type via T
|
|
214
|
+
return defaultValue as any as T
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Serialization ────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/** Serialize for storage. Includes flash data for next request. */
|
|
220
|
+
toJSON(): Record<string, unknown> {
|
|
221
|
+
const result = { ...this.#data }
|
|
222
|
+
if (Object.keys(this.#flashData).length > 0) {
|
|
223
|
+
result.__flash = this.#flashData
|
|
224
|
+
}
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
isDirty(): boolean {
|
|
229
|
+
return this.#dirty
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session middleware — initializes session on each request.
|
|
3
|
+
*
|
|
4
|
+
* Reads session ID from cookie, loads data via driver, attaches to ctx,
|
|
5
|
+
* and writes back after the handler runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomBytes } from 'node:crypto'
|
|
9
|
+
import type { HttpContext } from '../http/HttpContext.js'
|
|
10
|
+
import { CookieDriver } from './drivers/CookieDriver.js'
|
|
11
|
+
import { MemoryDriver } from './drivers/MemoryDriver.js'
|
|
12
|
+
import type { SessionConfig, SessionDriver } from './Session.js'
|
|
13
|
+
import { Session } from './Session.js'
|
|
14
|
+
|
|
15
|
+
interface ResolvedSessionConfig {
|
|
16
|
+
driver: string
|
|
17
|
+
cookieName: string
|
|
18
|
+
maxAge: number
|
|
19
|
+
clearWithBrowser: boolean
|
|
20
|
+
rolling: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default class SessionMiddleware {
|
|
24
|
+
#driver: SessionDriver
|
|
25
|
+
#cookieDriver: CookieDriver | null = null
|
|
26
|
+
#config: ResolvedSessionConfig
|
|
27
|
+
|
|
28
|
+
constructor(config?: SessionConfig & { secret?: string }) {
|
|
29
|
+
this.#config = {
|
|
30
|
+
driver: config?.driver ?? 'memory',
|
|
31
|
+
cookieName: config?.cookieName ?? 'ream_session',
|
|
32
|
+
maxAge: config?.maxAge ?? 7200,
|
|
33
|
+
clearWithBrowser: config?.clearWithBrowser ?? false,
|
|
34
|
+
rolling: config?.rolling ?? false,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.#config.driver === 'cookie') {
|
|
38
|
+
if (!config?.secret) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Cookie session driver requires a secret. Set session.secret in your config.',
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
this.#cookieDriver = new CookieDriver(config.secret)
|
|
44
|
+
this.#driver = this.#cookieDriver
|
|
45
|
+
} else if (this.#config.driver === 'memory') {
|
|
46
|
+
this.#driver = new MemoryDriver()
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error(`Unknown session driver: ${this.#config.driver}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async handle(ctx: HttpContext, next: () => Promise<void>) {
|
|
53
|
+
const cookieName = this.#config.cookieName
|
|
54
|
+
const maxAge = this.#config.maxAge
|
|
55
|
+
const rolling = this.#config.rolling
|
|
56
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
57
|
+
|
|
58
|
+
// Read session ID from cookie — pre-parsed by the Rust HyperServer.
|
|
59
|
+
let sessionId = ctx.request.cookie(cookieName)
|
|
60
|
+
const hadIncomingCookie = sessionId !== null
|
|
61
|
+
|
|
62
|
+
// Cookie driver: the cookie value IS the session data (encrypted)
|
|
63
|
+
if (this.#cookieDriver) {
|
|
64
|
+
const data = await this.#driver.read(sessionId ?? '')
|
|
65
|
+
sessionId = sessionId ?? generateSessionId()
|
|
66
|
+
const session = new Session(sessionId, data)
|
|
67
|
+
ctx.store.set('session', session)
|
|
68
|
+
|
|
69
|
+
await next()
|
|
70
|
+
|
|
71
|
+
// Default: only emit Set-Cookie if the session was actually modified —
|
|
72
|
+
// sending a cookie on every response defeats HTTP caching, leaks state
|
|
73
|
+
// to CDNs, and creates a privacy side-channel (response varies even for
|
|
74
|
+
// read-only GETs).
|
|
75
|
+
// Rolling mode: re-emit on every response with an existing session so
|
|
76
|
+
// the cookie maxAge window slides forward (active users don't time out
|
|
77
|
+
// mid-session). Caller opts in via SessionConfig.rolling.
|
|
78
|
+
const shouldEmit = session.isDirty() || (rolling && hadIncomingCookie)
|
|
79
|
+
if (shouldEmit) {
|
|
80
|
+
const encoded = this.#cookieDriver.encode(session.toJSON())
|
|
81
|
+
ctx.response.cookie(cookieName, encoded, {
|
|
82
|
+
maxAge: this.#config.clearWithBrowser ? undefined : maxAge,
|
|
83
|
+
path: '/',
|
|
84
|
+
httpOnly: true,
|
|
85
|
+
sameSite: 'lax',
|
|
86
|
+
secure: isProduction,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Server-side drivers (memory, redis)
|
|
93
|
+
if (!sessionId) {
|
|
94
|
+
sessionId = generateSessionId()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await this.#driver.read(sessionId)
|
|
98
|
+
const session = new Session(sessionId, data)
|
|
99
|
+
ctx.store.set('session', session)
|
|
100
|
+
|
|
101
|
+
await next()
|
|
102
|
+
|
|
103
|
+
// Session-fixation mitigation: if the handler called `session.regenerate()`
|
|
104
|
+
// (typically via Warden's SessionStrategy.login), the session id has been
|
|
105
|
+
// rotated. Migrate the driver storage atomically — write under the new
|
|
106
|
+
// id first, then destroy the old entry so a crash between calls leaves
|
|
107
|
+
// the user with a valid session under one of the two ids rather than
|
|
108
|
+
// none. The cookie below picks up `session.sessionId` which already
|
|
109
|
+
// returns the new value.
|
|
110
|
+
const regenerated = session.wasRegenerated()
|
|
111
|
+
const isNew = !hadIncomingCookie
|
|
112
|
+
if (regenerated) {
|
|
113
|
+
const originalId = session.originalSessionId()
|
|
114
|
+
const currentId = session.sessionId
|
|
115
|
+
await this.#driver.write(currentId, session.toJSON(), maxAge)
|
|
116
|
+
// Best-effort: a destroy failure leaves a stale entry that TTL will
|
|
117
|
+
// sweep, but should not crash the request. The new session is already
|
|
118
|
+
// live under `currentId`.
|
|
119
|
+
try {
|
|
120
|
+
await this.#driver.destroy(originalId)
|
|
121
|
+
} catch {
|
|
122
|
+
// benign — TTL will reclaim it.
|
|
123
|
+
}
|
|
124
|
+
} else if (session.isDirty()) {
|
|
125
|
+
await this.#driver.write(sessionId, session.toJSON(), maxAge)
|
|
126
|
+
} else if (!isNew) {
|
|
127
|
+
await this.#driver.touch(sessionId, maxAge)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Emit Set-Cookie only when there is server-side state worth
|
|
131
|
+
// pinning to the browser: the session was modified (isDirty → we
|
|
132
|
+
// just wrote it), regenerated (rotated id post-login), or rolling
|
|
133
|
+
// mode is sliding an existing session's window. A brand-new but
|
|
134
|
+
// CLEAN anonymous request gets NO cookie — otherwise every first
|
|
135
|
+
// read-only GET hands out a persistent tracking id that points at
|
|
136
|
+
// no server-side row (touch() is a no-op without a backing entry),
|
|
137
|
+
// defeats HTTP caching, and leaks a Set-Cookie to CDNs. This mirrors
|
|
138
|
+
// the cookie-driver path above (which already omits `isNew`).
|
|
139
|
+
if (session.isDirty() || regenerated || (rolling && hadIncomingCookie)) {
|
|
140
|
+
ctx.response.cookie(cookieName, session.sessionId, {
|
|
141
|
+
maxAge: this.#config.clearWithBrowser ? undefined : maxAge,
|
|
142
|
+
path: '/',
|
|
143
|
+
httpOnly: true,
|
|
144
|
+
sameSite: 'lax',
|
|
145
|
+
secure: isProduction,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function generateSessionId(): string {
|
|
152
|
+
return randomBytes(24).toString('base64url')
|
|
153
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CookieSigner } from '../../security/CookieSigner.js'
|
|
2
|
+
import type { SessionDriver } from '../Session.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cookie session driver — stores session data in an encrypted cookie (AES-256-GCM).
|
|
6
|
+
* Stateless on the server. Limited by cookie size (~4KB).
|
|
7
|
+
* Requires a secret for encryption.
|
|
8
|
+
*/
|
|
9
|
+
export class CookieDriver implements SessionDriver {
|
|
10
|
+
private signer: CookieSigner
|
|
11
|
+
|
|
12
|
+
constructor(secret: string) {
|
|
13
|
+
if (!secret) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'CookieDriver requires a secret for session encryption. Set session.secret in your config.',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
this.signer = new CookieSigner(secret)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async read(sessionId: string): Promise<Record<string, unknown>> {
|
|
22
|
+
if (!sessionId) return {}
|
|
23
|
+
const decrypted = this.signer.decrypt(sessionId)
|
|
24
|
+
if (!decrypted) return {}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(decrypted)
|
|
27
|
+
} catch {
|
|
28
|
+
return {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async write(_sessionId: string, _data: Record<string, unknown>, _ttl: number): Promise<void> {
|
|
33
|
+
// Cookie driver writes via the response cookie — handled by middleware
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async destroy(_sessionId: string): Promise<void> {
|
|
37
|
+
// Cookie deletion handled by middleware setting maxAge=0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async touch(_sessionId: string, _ttl: number): Promise<void> {
|
|
41
|
+
// Cookie renewal handled by middleware
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Encode session data for cookie storage (encrypted). */
|
|
45
|
+
encode(data: Record<string, unknown>): string {
|
|
46
|
+
return this.signer.encrypt(JSON.stringify(data))
|
|
47
|
+
}
|
|
48
|
+
}
|