@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,275 @@
|
|
|
1
|
+
import type { TestResponse } from './TestClient.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fluent HTTP request builder with assertion methods.
|
|
5
|
+
*
|
|
6
|
+
* await client
|
|
7
|
+
* .fluent('GET', '/api/users/42')
|
|
8
|
+
* .withAuth(user)
|
|
9
|
+
* .expectStatus(200)
|
|
10
|
+
* .expectJson({ id: 42 })
|
|
11
|
+
*
|
|
12
|
+
* The builder chains: setters return `this`, assertion methods send the
|
|
13
|
+
* request on first call (memoised) and return a `Promise<this>` so further
|
|
14
|
+
* assertions can be awaited or chained via `await`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface AuthSubject {
|
|
18
|
+
/** String or numeric user id — used by `withAuth` / `asUser` to sign the session. */
|
|
19
|
+
id: string | number
|
|
20
|
+
/** Optional custom headers added to the request (e.g. tenant markers). */
|
|
21
|
+
extraHeaders?: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AuthStrategy {
|
|
25
|
+
/** Compute the headers Warden expects for this user (Bearer token / session cookie / ...). */
|
|
26
|
+
headersFor(subject: AuthSubject): Record<string, string> | Promise<Record<string, string>>
|
|
27
|
+
/** Compute cookies for session-based strategies. */
|
|
28
|
+
cookiesFor?(subject: AuthSubject): Record<string, string> | Promise<Record<string, string>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Assertion failure — plain Error so tests see a clean stack trace. */
|
|
32
|
+
class ExpectationError extends Error {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'ExpectationError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Internal low-level sender — matches what the TestClient exposes.
|
|
43
|
+
* Accepting it as an injected callable keeps RequestBuilder framework-agnostic.
|
|
44
|
+
*/
|
|
45
|
+
export type HttpSender = (
|
|
46
|
+
method: HttpMethod,
|
|
47
|
+
path: string,
|
|
48
|
+
init: {
|
|
49
|
+
headers: Record<string, string>
|
|
50
|
+
body: Buffer
|
|
51
|
+
},
|
|
52
|
+
) => Promise<TestResponse>
|
|
53
|
+
|
|
54
|
+
export class RequestBuilder {
|
|
55
|
+
#sender: HttpSender
|
|
56
|
+
#method: HttpMethod
|
|
57
|
+
#path: string
|
|
58
|
+
#headers: Record<string, string> = {}
|
|
59
|
+
#body: Buffer = Buffer.alloc(0)
|
|
60
|
+
#cookies: Record<string, string> = {}
|
|
61
|
+
#authStrategy: AuthStrategy | null
|
|
62
|
+
#pendingAuth: AuthSubject | null = null
|
|
63
|
+
#sent: Promise<TestResponse> | null = null
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
sender: HttpSender,
|
|
67
|
+
method: HttpMethod,
|
|
68
|
+
path: string,
|
|
69
|
+
authStrategy: AuthStrategy | null = null,
|
|
70
|
+
) {
|
|
71
|
+
this.#sender = sender
|
|
72
|
+
this.#method = method
|
|
73
|
+
this.#path = path
|
|
74
|
+
this.#authStrategy = authStrategy
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
headers(map: Record<string, string>): this {
|
|
78
|
+
for (const [k, v] of Object.entries(map)) {
|
|
79
|
+
this.#headers[k.toLowerCase()] = v
|
|
80
|
+
}
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
header(name: string, value: string): this {
|
|
85
|
+
this.#headers[name.toLowerCase()] = value
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
json(data: unknown): this {
|
|
90
|
+
this.#body = Buffer.from(JSON.stringify(data), 'utf8')
|
|
91
|
+
this.#headers['content-type'] = 'application/json'
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
body(data: Buffer | string, contentType?: string): this {
|
|
96
|
+
this.#body = typeof data === 'string' ? Buffer.from(data, 'utf8') : data
|
|
97
|
+
if (contentType) this.#headers['content-type'] = contentType
|
|
98
|
+
return this
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
form(data: Record<string, string>): this {
|
|
102
|
+
const pairs = Object.entries(data).map(
|
|
103
|
+
([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`,
|
|
104
|
+
)
|
|
105
|
+
this.#body = Buffer.from(pairs.join('&'), 'utf8')
|
|
106
|
+
this.#headers['content-type'] = 'application/x-www-form-urlencoded'
|
|
107
|
+
return this
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
cookies(map: Record<string, string>): this {
|
|
111
|
+
Object.assign(this.#cookies, map)
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
cookie(name: string, value: string): this {
|
|
116
|
+
this.#cookies[name] = value
|
|
117
|
+
return this
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attach auth for a user. Uses the strategy passed to `client`'s `auth`
|
|
122
|
+
* option — Warden's session/JWT/API-key strategy decides the header/cookie
|
|
123
|
+
* shape. Without a strategy, throws at send time.
|
|
124
|
+
*/
|
|
125
|
+
withAuth(subject: AuthSubject): this {
|
|
126
|
+
this.#pendingAuth = subject
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Shortcut for `withAuth({ id: userId })` — user is identified by id only. */
|
|
131
|
+
asUser(userId: string | number): this {
|
|
132
|
+
this.#pendingAuth = { id: userId }
|
|
133
|
+
return this
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Fire the request (once) and return the raw response. Idempotent. */
|
|
137
|
+
send(): Promise<TestResponse> {
|
|
138
|
+
if (this.#sent === null) this.#sent = this.#execute()
|
|
139
|
+
return this.#sent
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async expectStatus(code: number): Promise<this> {
|
|
143
|
+
const res = await this.send()
|
|
144
|
+
if (res.status !== code) {
|
|
145
|
+
throw new ExpectationError(
|
|
146
|
+
`Expected status ${code}, got ${res.status}. Body: ${capBody(res.body)}`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return this
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async expectHeader(name: string, value: string | RegExp): Promise<this> {
|
|
153
|
+
const res = await this.send()
|
|
154
|
+
const actual = res.headers[name.toLowerCase()]
|
|
155
|
+
if (actual === undefined) {
|
|
156
|
+
throw new ExpectationError(
|
|
157
|
+
`Expected header ${name}, not present in response headers: ${Object.keys(res.headers).join(', ')}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
if (value instanceof RegExp) {
|
|
161
|
+
if (!value.test(actual)) {
|
|
162
|
+
throw new ExpectationError(`Expected header ${name} to match ${value}, got "${actual}"`)
|
|
163
|
+
}
|
|
164
|
+
} else if (actual !== value) {
|
|
165
|
+
throw new ExpectationError(`Expected header ${name} = "${value}", got "${actual}"`)
|
|
166
|
+
}
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async expectCookie(name: string, value?: string | RegExp): Promise<this> {
|
|
171
|
+
const res = await this.send()
|
|
172
|
+
const setCookie = res.headers['set-cookie']
|
|
173
|
+
if (!setCookie) {
|
|
174
|
+
throw new ExpectationError(`Expected cookie ${name}, but no Set-Cookie header was returned`)
|
|
175
|
+
}
|
|
176
|
+
const cookies = setCookie.split(/,(?=\s*\w+=)/)
|
|
177
|
+
const match = cookies.find((c) => c.trimStart().startsWith(`${name}=`))
|
|
178
|
+
if (!match) {
|
|
179
|
+
throw new ExpectationError(`Expected cookie ${name}, not found in: ${setCookie}`)
|
|
180
|
+
}
|
|
181
|
+
if (value === undefined) return this
|
|
182
|
+
const rawVal = match.split(';')[0]?.split('=')[1] ?? ''
|
|
183
|
+
if (value instanceof RegExp) {
|
|
184
|
+
if (!value.test(rawVal)) {
|
|
185
|
+
throw new ExpectationError(`Expected cookie ${name} to match ${value}, got "${rawVal}"`)
|
|
186
|
+
}
|
|
187
|
+
} else if (rawVal !== value) {
|
|
188
|
+
throw new ExpectationError(`Expected cookie ${name} = "${value}", got "${rawVal}"`)
|
|
189
|
+
}
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async expectJson(expected: unknown): Promise<this> {
|
|
194
|
+
const res = await this.send()
|
|
195
|
+
let actual: unknown
|
|
196
|
+
try {
|
|
197
|
+
actual = res.json()
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new ExpectationError(
|
|
200
|
+
`Expected JSON body, got non-JSON. Body: ${capBody(res.body)} (parse error: ${err instanceof Error ? err.message : String(err)})`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
if (!partialMatch(actual, expected)) {
|
|
204
|
+
throw new ExpectationError(
|
|
205
|
+
`JSON partial match failed.\nExpected (partial): ${JSON.stringify(expected)}\nActual: ${capBody(JSON.stringify(actual))}`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
return this
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async #execute(): Promise<TestResponse> {
|
|
212
|
+
// Resolve auth before sending.
|
|
213
|
+
if (this.#pendingAuth !== null) {
|
|
214
|
+
if (!this.#authStrategy) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
'RequestBuilder: `withAuth()` / `asUser()` called but no AuthStrategy was provided via the client `auth` option.',
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
const authHeaders = await this.#authStrategy.headersFor(this.#pendingAuth)
|
|
220
|
+
for (const [k, v] of Object.entries(authHeaders)) {
|
|
221
|
+
this.#headers[k.toLowerCase()] = v
|
|
222
|
+
}
|
|
223
|
+
if (this.#authStrategy.cookiesFor) {
|
|
224
|
+
const authCookies = await this.#authStrategy.cookiesFor(this.#pendingAuth)
|
|
225
|
+
Object.assign(this.#cookies, authCookies)
|
|
226
|
+
}
|
|
227
|
+
if (this.#pendingAuth.extraHeaders) {
|
|
228
|
+
for (const [k, v] of Object.entries(this.#pendingAuth.extraHeaders)) {
|
|
229
|
+
this.#headers[k.toLowerCase()] = v
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Serialise cookies into a single `Cookie:` header.
|
|
235
|
+
const cookieEntries = Object.entries(this.#cookies)
|
|
236
|
+
if (cookieEntries.length > 0) {
|
|
237
|
+
this.#headers.cookie = cookieEntries.map(([k, v]) => `${k}=${v}`).join('; ')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return this.#sender(this.#method, this.#path, {
|
|
241
|
+
headers: this.#headers,
|
|
242
|
+
body: this.#body,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function capBody(s: string, max = 512): string {
|
|
248
|
+
return s.length <= max ? s : `${s.slice(0, max)}...[truncated]`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Narrowing guard — a non-null, non-array object usable as a string map. */
|
|
252
|
+
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
253
|
+
return typeof x === 'object' && x !== null && !Array.isArray(x)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Partial deep-match. For objects, every key in `expected` must match in
|
|
258
|
+
* `actual`. For arrays, every element in `expected` must match SOMEWHERE in
|
|
259
|
+
* `actual` (order-independent). Primitives compared by strict equality.
|
|
260
|
+
*/
|
|
261
|
+
export function partialMatch(actual: unknown, expected: unknown): boolean {
|
|
262
|
+
if (expected === null || expected === undefined) {
|
|
263
|
+
return actual === expected
|
|
264
|
+
}
|
|
265
|
+
if (typeof expected !== 'object') return actual === expected
|
|
266
|
+
if (Array.isArray(expected)) {
|
|
267
|
+
if (!Array.isArray(actual)) return false
|
|
268
|
+
return expected.every((want) => actual.some((have) => partialMatch(have, want)))
|
|
269
|
+
}
|
|
270
|
+
if (!isRecord(actual) || !isRecord(expected)) return false
|
|
271
|
+
for (const key of Object.keys(expected)) {
|
|
272
|
+
if (!partialMatch(actual[key], expected[key])) return false
|
|
273
|
+
}
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestClient — supertest-like HTTP client for integration testing.
|
|
3
|
+
*
|
|
4
|
+
* Boots a Ream application on an ephemeral port and provides a fluent API
|
|
5
|
+
* for sending requests and asserting responses.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const client = new TestClient(createApp)
|
|
9
|
+
* await client.boot()
|
|
10
|
+
* const res = await client.get('/api/health')
|
|
11
|
+
* expect(res.status).toBe(200)
|
|
12
|
+
* await client.close()
|
|
13
|
+
*
|
|
14
|
+
* Or with the helper:
|
|
15
|
+
* const client = await createTestClient(createApp)
|
|
16
|
+
* const res = await client.post('/api/users').json({ name: 'Alice' }).send()
|
|
17
|
+
* await client.close()
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Dict } from '../types/helpers.js'
|
|
21
|
+
import {
|
|
22
|
+
type AuthStrategy,
|
|
23
|
+
type HttpMethod,
|
|
24
|
+
type HttpSender,
|
|
25
|
+
RequestBuilder,
|
|
26
|
+
} from './RequestBuilder.js'
|
|
27
|
+
|
|
28
|
+
export interface TestResponse {
|
|
29
|
+
status: number
|
|
30
|
+
headers: Dict
|
|
31
|
+
body: string
|
|
32
|
+
json<T = unknown>(): T
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TestRequestBuilder {
|
|
36
|
+
/** Set a request header. */
|
|
37
|
+
header(name: string, value: string): TestRequestBuilder
|
|
38
|
+
/** Set the request body as JSON. */
|
|
39
|
+
json(data: unknown): TestRequestBuilder
|
|
40
|
+
/** Set a raw string body with optional content type. */
|
|
41
|
+
body(content: string, contentType?: string): TestRequestBuilder
|
|
42
|
+
/** Set a cookie. */
|
|
43
|
+
cookie(name: string, value: string): TestRequestBuilder
|
|
44
|
+
/** Send the request and return the response. */
|
|
45
|
+
send(): Promise<TestResponse>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TestClient {
|
|
49
|
+
#port = 0
|
|
50
|
+
#headers: Dict = {}
|
|
51
|
+
#bootFn: (port: number) => Promise<{ port: number; close: () => Promise<void> | void }>
|
|
52
|
+
#auth: AuthStrategy | null
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
bootFn: (port: number) => Promise<{ port: number; close: () => Promise<void> | void }>,
|
|
56
|
+
options: { auth?: AuthStrategy } = {},
|
|
57
|
+
) {
|
|
58
|
+
this.#bootFn = bootFn
|
|
59
|
+
this.#auth = options.auth ?? null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#server: { port: number; close: () => Promise<void> | void } | null = null
|
|
63
|
+
|
|
64
|
+
/** Boot the application on a random port. */
|
|
65
|
+
async boot(): Promise<void> {
|
|
66
|
+
this.#server = await this.#bootFn(0)
|
|
67
|
+
this.#port = this.#server.port
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Close the server. */
|
|
71
|
+
async close(): Promise<void> {
|
|
72
|
+
if (this.#server) {
|
|
73
|
+
await this.#server.close()
|
|
74
|
+
this.#server = null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ephemeral port the booted server bound to. Useful for long-lived
|
|
80
|
+
* connections (SSE / WebSocket) that the fluent buffered request
|
|
81
|
+
* surface can't model — open them with `fetch` against
|
|
82
|
+
* `http://127.0.0.1:${client.port}/...`. Returns 0 before `boot()`.
|
|
83
|
+
*/
|
|
84
|
+
get port(): number {
|
|
85
|
+
return this.#port
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Set a default header for all requests. */
|
|
89
|
+
withHeader(name: string, value: string): this {
|
|
90
|
+
this.#headers[name.toLowerCase()] = value
|
|
91
|
+
return this
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** GET request. */
|
|
95
|
+
get(path: string): Promise<TestResponse> {
|
|
96
|
+
return this.request('GET', path).send()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** POST request. */
|
|
100
|
+
post(path: string): TestRequestBuilder {
|
|
101
|
+
return this.request('POST', path)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** PUT request. */
|
|
105
|
+
put(path: string): TestRequestBuilder {
|
|
106
|
+
return this.request('PUT', path)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** PATCH request. */
|
|
110
|
+
patch(path: string): TestRequestBuilder {
|
|
111
|
+
return this.request('PATCH', path)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** DELETE request. */
|
|
115
|
+
delete(path: string): Promise<TestResponse> {
|
|
116
|
+
return this.request('DELETE', path).send()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a request with the rich fluent surface: chained assertions
|
|
121
|
+
* (`expectStatus` / `expectJson` / `expectHeader` / `expectCookie`), auth
|
|
122
|
+
* injection (`withAuth` / `asUser`, needs the `auth` client option), and
|
|
123
|
+
* `form()`. The lower-level `request()` (below) stays for raw control.
|
|
124
|
+
*/
|
|
125
|
+
fluent(method: HttpMethod, path: string): RequestBuilder {
|
|
126
|
+
const sender: HttpSender = (m, p, init) =>
|
|
127
|
+
sendRequest(
|
|
128
|
+
this.#port,
|
|
129
|
+
m,
|
|
130
|
+
p,
|
|
131
|
+
{ ...this.#headers, ...init.headers },
|
|
132
|
+
init.body.toString('utf8'),
|
|
133
|
+
)
|
|
134
|
+
return new RequestBuilder(sender, method, path, this.#auth)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build a request with the low-level fluent API. */
|
|
138
|
+
request(method: string, path: string): TestRequestBuilder {
|
|
139
|
+
const headers: Dict = { ...this.#headers }
|
|
140
|
+
let bodyContent = ''
|
|
141
|
+
const cookies: string[] = []
|
|
142
|
+
|
|
143
|
+
const builder: TestRequestBuilder = {
|
|
144
|
+
header(name: string, value: string) {
|
|
145
|
+
headers[name.toLowerCase()] = value
|
|
146
|
+
return builder
|
|
147
|
+
},
|
|
148
|
+
json(data: unknown) {
|
|
149
|
+
bodyContent = JSON.stringify(data)
|
|
150
|
+
headers['content-type'] = 'application/json'
|
|
151
|
+
return builder
|
|
152
|
+
},
|
|
153
|
+
body(content: string, contentType?: string) {
|
|
154
|
+
bodyContent = content
|
|
155
|
+
if (contentType) headers['content-type'] = contentType
|
|
156
|
+
return builder
|
|
157
|
+
},
|
|
158
|
+
cookie(name: string, value: string) {
|
|
159
|
+
cookies.push(`${name}=${value}`)
|
|
160
|
+
return builder
|
|
161
|
+
},
|
|
162
|
+
send: async () => {
|
|
163
|
+
if (cookies.length > 0) {
|
|
164
|
+
headers.cookie = cookies.join('; ')
|
|
165
|
+
}
|
|
166
|
+
return sendRequest(this.#port, method, path, headers, bodyContent)
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return builder
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Create and boot a test client in one call. */
|
|
175
|
+
export async function createTestClient(
|
|
176
|
+
bootFn: (port: number) => Promise<{ port: number; close: () => Promise<void> | void }>,
|
|
177
|
+
options?: { auth?: AuthStrategy },
|
|
178
|
+
): Promise<TestClient> {
|
|
179
|
+
const client = new TestClient(bootFn, options)
|
|
180
|
+
await client.boot()
|
|
181
|
+
return client
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
type AuthStrategy,
|
|
186
|
+
type AuthSubject,
|
|
187
|
+
type HttpMethod,
|
|
188
|
+
type HttpSender,
|
|
189
|
+
partialMatch,
|
|
190
|
+
RequestBuilder,
|
|
191
|
+
} from './RequestBuilder.js'
|
|
192
|
+
|
|
193
|
+
/** Send an HTTP request using raw TCP (no external dependencies). */
|
|
194
|
+
async function sendRequest(
|
|
195
|
+
port: number,
|
|
196
|
+
method: string,
|
|
197
|
+
path: string,
|
|
198
|
+
headers: Dict,
|
|
199
|
+
body: string,
|
|
200
|
+
): Promise<TestResponse> {
|
|
201
|
+
const { connect } = await import('node:net')
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const socket = connect({ host: '127.0.0.1', port }, () => {
|
|
205
|
+
// Build HTTP/1.1 request
|
|
206
|
+
const headerLines = Object.entries(headers)
|
|
207
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
208
|
+
.join('\r\n')
|
|
209
|
+
|
|
210
|
+
const contentLength = Buffer.byteLength(body, 'utf8')
|
|
211
|
+
|
|
212
|
+
let request = `${method} ${path} HTTP/1.1\r\n`
|
|
213
|
+
request += `Host: localhost:${port}\r\n`
|
|
214
|
+
request += `Connection: close\r\n`
|
|
215
|
+
if (contentLength > 0) {
|
|
216
|
+
request += `Content-Length: ${contentLength}\r\n`
|
|
217
|
+
}
|
|
218
|
+
if (headerLines) {
|
|
219
|
+
request += `${headerLines}\r\n`
|
|
220
|
+
}
|
|
221
|
+
request += '\r\n'
|
|
222
|
+
request += body
|
|
223
|
+
|
|
224
|
+
socket.write(request)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const chunks: Buffer[] = []
|
|
228
|
+
socket.on('data', (chunk) => chunks.push(chunk))
|
|
229
|
+
socket.on('end', () => {
|
|
230
|
+
// Keep the wire bytes as a Buffer through parseHttpResponse — chunked
|
|
231
|
+
// decoding has to count bytes (per RFC 7230), not UTF-16 code units.
|
|
232
|
+
// Converting to a string here used to truncate non-ASCII and binary
|
|
233
|
+
// chunked responses (é / emoji / images / PDF).
|
|
234
|
+
resolve(parseHttpResponse(Buffer.concat(chunks)))
|
|
235
|
+
})
|
|
236
|
+
socket.on('error', reject)
|
|
237
|
+
|
|
238
|
+
// Socket inactivity timeout. 5s was too tight for real e2e flows —
|
|
239
|
+
// signup hashes a password with argon2 (~50-200ms), then an INSERT
|
|
240
|
+
// crosses sqlite's WAL fsync, then JWT sign + serialize. Under
|
|
241
|
+
// cross-package CPU pressure (parallel helix workers, NAPI
|
|
242
|
+
// tokio runtimes) the cumulative latency exceeded 5s ~30% of runs
|
|
243
|
+
// and surfaced as flaky `bob accepts` / `non-member 403` failures
|
|
244
|
+
// in kitchen-sink's workspace.test.ts. 30s matches the helix
|
|
245
|
+
// `--timeout=60000` per-test budget without masking real hangs.
|
|
246
|
+
socket.setTimeout(30_000, () => {
|
|
247
|
+
socket.destroy()
|
|
248
|
+
reject(new Error('TestClient request timed out'))
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse a raw HTTP response. Accepts the wire bytes (Buffer) or a UTF-8
|
|
255
|
+
* string for back-compat with older test callers. Chunked decoding runs on
|
|
256
|
+
* the Buffer to keep byte-counting honest — HTTP counts bytes (RFC 7230),
|
|
257
|
+
* not UTF-16 code units, so doing the work on a `.toString('utf8')` string
|
|
258
|
+
* silently truncated non-ASCII and binary responses.
|
|
259
|
+
*/
|
|
260
|
+
export function parseHttpResponse(raw: string | Buffer): TestResponse {
|
|
261
|
+
const buf = typeof raw === 'string' ? Buffer.from(raw, 'utf8') : raw
|
|
262
|
+
const HEADER_TERM = Buffer.from('\r\n\r\n', 'ascii')
|
|
263
|
+
const headerEnd = buf.indexOf(HEADER_TERM)
|
|
264
|
+
if (headerEnd === -1) {
|
|
265
|
+
const body = buf.toString('utf8')
|
|
266
|
+
return { status: 0, headers: {}, body, json: () => JSON.parse(body) }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// HTTP headers are ASCII (RFC 7230 §3.2); decoding via 'ascii' is safe and
|
|
270
|
+
// makes any non-ASCII byte show as a question-mark, surfacing protocol bugs
|
|
271
|
+
// instead of silently smuggling them.
|
|
272
|
+
const headerSection = buf.subarray(0, headerEnd).toString('ascii')
|
|
273
|
+
let bodyBuffer = buf.subarray(headerEnd + HEADER_TERM.length)
|
|
274
|
+
|
|
275
|
+
const lines = headerSection.split('\r\n')
|
|
276
|
+
const statusLine = lines[0] ?? ''
|
|
277
|
+
const statusMatch = statusLine.match(/HTTP\/\d+\.\d+ (\d+)/)
|
|
278
|
+
const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : 0
|
|
279
|
+
|
|
280
|
+
const headers: Dict = {}
|
|
281
|
+
for (let i = 1; i < lines.length; i++) {
|
|
282
|
+
const colonIdx = lines[i].indexOf(':')
|
|
283
|
+
if (colonIdx === -1) continue
|
|
284
|
+
const key = lines[i].slice(0, colonIdx).trim().toLowerCase()
|
|
285
|
+
const value = lines[i].slice(colonIdx + 1).trim()
|
|
286
|
+
headers[key] = value
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// RFC 7230 §4.1: when Transfer-Encoding includes `chunked`, the message body
|
|
290
|
+
// is a sequence of `<size-hex>[;ext]\r\n<bytes>\r\n` chunks terminated by
|
|
291
|
+
// `0\r\n\r\n`. Decode on the Buffer so chunk sizes (bytes) match slice
|
|
292
|
+
// boundaries — character-counting corrupts é / emoji / binary payloads.
|
|
293
|
+
const transferEncoding = headers['transfer-encoding'] ?? ''
|
|
294
|
+
if (
|
|
295
|
+
transferEncoding
|
|
296
|
+
.toLowerCase()
|
|
297
|
+
.split(',')
|
|
298
|
+
.map((s) => s.trim())
|
|
299
|
+
.includes('chunked')
|
|
300
|
+
) {
|
|
301
|
+
bodyBuffer = decodeChunked(bodyBuffer)
|
|
302
|
+
delete headers['transfer-encoding']
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const body = bodyBuffer.toString('utf8')
|
|
306
|
+
return {
|
|
307
|
+
status,
|
|
308
|
+
headers,
|
|
309
|
+
body,
|
|
310
|
+
json<T = unknown>(): T {
|
|
311
|
+
return JSON.parse(body)
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Decode an HTTP/1.1 chunked-transfer body. Operates on a Buffer because
|
|
318
|
+
* chunk sizes are byte counts per RFC 7230 — character-counting silently
|
|
319
|
+
* corrupts non-ASCII / binary responses. Tolerant: stops at the first
|
|
320
|
+
* malformed chunk header rather than throwing — surfacing a partial body is
|
|
321
|
+
* more useful for a failing test assertion than swallowing the whole
|
|
322
|
+
* response.
|
|
323
|
+
*/
|
|
324
|
+
function decodeChunked(raw: Buffer): Buffer {
|
|
325
|
+
const CRLF = Buffer.from('\r\n', 'ascii')
|
|
326
|
+
const out: Buffer[] = []
|
|
327
|
+
let i = 0
|
|
328
|
+
while (i < raw.length) {
|
|
329
|
+
const lineEnd = raw.indexOf(CRLF, i)
|
|
330
|
+
if (lineEnd === -1) break
|
|
331
|
+
const sizeStr = raw.subarray(i, lineEnd).toString('ascii').split(';')[0]?.trim() ?? ''
|
|
332
|
+
const size = Number.parseInt(sizeStr, 16)
|
|
333
|
+
if (!Number.isFinite(size) || size < 0) break
|
|
334
|
+
i = lineEnd + CRLF.length
|
|
335
|
+
if (size === 0) break
|
|
336
|
+
if (i + size > raw.length) {
|
|
337
|
+
out.push(raw.subarray(i))
|
|
338
|
+
break
|
|
339
|
+
}
|
|
340
|
+
out.push(raw.subarray(i, i + size))
|
|
341
|
+
i += size + CRLF.length // chunk + trailing \r\n
|
|
342
|
+
}
|
|
343
|
+
return Buffer.concat(out)
|
|
344
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type helpers — utility types for the Ream framework.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Infer route parameters from a route pattern string.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* type Params = InferRouteParams<'/users/:id/posts/:postId'>
|
|
10
|
+
* // => { id: string; postId: string }
|
|
11
|
+
*/
|
|
12
|
+
export type InferRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
|
|
13
|
+
? { [K in Param | keyof InferRouteParams<`/${Rest}`>]: string }
|
|
14
|
+
: T extends `${string}:${infer Param}`
|
|
15
|
+
? { [K in Param]: string }
|
|
16
|
+
: Record<string, never>
|
|
17
|
+
|
|
18
|
+
/** Accept a single value or an array. */
|
|
19
|
+
export type OneOrMore<T> = T | T[]
|
|
20
|
+
|
|
21
|
+
/** Create a nominal/opaque type from a base type. */
|
|
22
|
+
export type Opaque<Base, Tag extends string> = Base & { readonly __tag: Tag }
|
|
23
|
+
|
|
24
|
+
/** Unwrap an opaque type back to its base. */
|
|
25
|
+
export type UnwrapOpaque<T> = T extends Opaque<infer Base, string> ? Base : T
|
|
26
|
+
|
|
27
|
+
/** Accept sync or async values. */
|
|
28
|
+
export type AsyncOrSync<T> = T | Promise<T>
|
|
29
|
+
|
|
30
|
+
/** Constructor type. */
|
|
31
|
+
export type Constructor<T = unknown, Args extends unknown[] = unknown[]> = new (...args: Args) => T
|
|
32
|
+
|
|
33
|
+
/** Abstract constructor type. */
|
|
34
|
+
export type AbstractConstructor<T = unknown> = abstract new (...args: unknown[]) => T
|
|
35
|
+
|
|
36
|
+
/** Extract only function properties from a type. */
|
|
37
|
+
export type ExtractFunctions<T, Ignore extends keyof T = never> = {
|
|
38
|
+
[K in Exclude<keyof T, Ignore>]: T[K] extends (...args: unknown[]) => unknown ? K : never
|
|
39
|
+
}[Exclude<keyof T, Ignore>]
|
|
40
|
+
|
|
41
|
+
/** Prettify a type intersection for better IDE display. */
|
|
42
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* String-keyed bag of values — the recurring HTTP-shaped objects:
|
|
46
|
+
* headers, cookies, query params, route params, env vars, form fields...
|
|
47
|
+
*
|
|
48
|
+
* Defaults to a string→string map (the most common case, and what
|
|
49
|
+
* `Record<string, string>` was being repeated for). Pass a value type
|
|
50
|
+
* explicitly when needed: `Dict<unknown>`, `Dict<number>`, ...
|
|
51
|
+
*/
|
|
52
|
+
export type Dict<V = string> = Record<string, V>
|
package/swcrc.app.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/swcrc",
|
|
3
|
+
"jsc": {
|
|
4
|
+
"parser": {
|
|
5
|
+
"syntax": "typescript",
|
|
6
|
+
"decorators": true
|
|
7
|
+
},
|
|
8
|
+
"transform": {
|
|
9
|
+
"legacyDecorator": true,
|
|
10
|
+
"decoratorMetadata": true
|
|
11
|
+
},
|
|
12
|
+
"target": "es2022"
|
|
13
|
+
},
|
|
14
|
+
"module": {
|
|
15
|
+
"type": "es6"
|
|
16
|
+
}
|
|
17
|
+
}
|