@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
package/src/Codemods.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface Codemods {
|
|
5
|
+
addProvider(importPath: string): Promise<void>
|
|
6
|
+
addEnvVars(vars: Record<string, string>): Promise<void>
|
|
7
|
+
writeFile(filePath: string, content: string, options?: { force?: boolean }): Promise<void>
|
|
8
|
+
registerCommand(importPath: string): Promise<void>
|
|
9
|
+
registerMiddleware(importPath: string, options?: { tier?: 'server' | 'router' }): Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type MiddlewareTier = 'server' | 'router'
|
|
13
|
+
|
|
14
|
+
function isMiddlewareTier(value: unknown): value is MiddlewareTier {
|
|
15
|
+
return value === 'server' || value === 'router'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findTierBlock(
|
|
19
|
+
content: string,
|
|
20
|
+
tier: MiddlewareTier,
|
|
21
|
+
): { start: number; end: number } | null {
|
|
22
|
+
// Word-boundary prefix `(?:^|[^A-Za-z0-9_$.])` rejects `customServer.use` / `myRouter.use`
|
|
23
|
+
// when scanning for the canonical `server` / `router` tier. The `.` exclusion also rejects
|
|
24
|
+
// `obj.server.use` shapes. Match[1] captures the literal `<tier>.use([` so we can compute
|
|
25
|
+
// the post-`[` cursor without including the boundary char.
|
|
26
|
+
const marker = new RegExp(
|
|
27
|
+
`(?:^|[^A-Za-z0-9_$.])(${tier}\\.use\\s*\\(\\s*(?:\\/\\*[\\s\\S]*?\\*\\/\\s*)?\\[)`,
|
|
28
|
+
)
|
|
29
|
+
const match = marker.exec(content)
|
|
30
|
+
if (match === null || match[1] === undefined) return null
|
|
31
|
+
const start = match.index + match[0].length
|
|
32
|
+
// The array close pattern `\]\s*,?\s*\)` matches `])`, `],)`, `],\n)`, `]\n)` — every
|
|
33
|
+
// canonical kernel shape produced by `ream new` or hand-formatted by Prettier. Using
|
|
34
|
+
// this pattern (vs a bare `]`) avoids early-truncating the slice on nested `]` inside
|
|
35
|
+
// an array entry — important for the dedup window since we read the slice content for
|
|
36
|
+
// cross-tier collision detection AND per-tier idempotency.
|
|
37
|
+
const closeMatch = /\]\s*,?\s*\)/.exec(content.slice(start))
|
|
38
|
+
if (closeMatch === null) return null
|
|
39
|
+
return { start, end: start + closeMatch.index }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect line ending (CRLF on Windows checkouts, LF elsewhere) so codemods preserve the file's existing convention.
|
|
44
|
+
* Majority-based: a stray CRLF in an otherwise-LF file must not flip the whole insertion to CRLF.
|
|
45
|
+
*/
|
|
46
|
+
function detectLineEnding(content: string): '\r\n' | '\n' {
|
|
47
|
+
const crlfCount = (content.match(/\r\n/g) ?? []).length
|
|
48
|
+
const bareLfCount = (content.match(/(?<!\r)\n/g) ?? []).length
|
|
49
|
+
return crlfCount > bareLfCount ? '\r\n' : '\n'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Refuse to follow a symlink that escapes the project root. Mirrors the guard `writeFile` enforces
|
|
54
|
+
* (lines 137-155) so `registerCommand` and `registerMiddleware` cannot be tricked into writing
|
|
55
|
+
* outside `root` via a symlinked `reamrc.ts` or `start/kernel.ts`.
|
|
56
|
+
*/
|
|
57
|
+
function assertCanonicallyInside(root: string, filePath: string, displayName: string): void {
|
|
58
|
+
if (!fs.existsSync(filePath)) return
|
|
59
|
+
const canonRoot = fs.realpathSync(root)
|
|
60
|
+
const canonFile = fs.realpathSync(filePath)
|
|
61
|
+
if (!canonFile.startsWith(canonRoot + path.sep) && canonFile !== canonRoot) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[configure] Symlink escape detected: ${displayName} resolves outside project root`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createCodemods(options?: { force?: boolean; cwd?: string }): Codemods {
|
|
69
|
+
const force = options?.force ?? false
|
|
70
|
+
const root = path.resolve(options?.cwd ?? process.cwd())
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
/**
|
|
74
|
+
* Insert a provider import into `reamrc.ts` by string manipulation.
|
|
75
|
+
*
|
|
76
|
+
* KNOWN LIMITATION: this is a regex-based codemod, not a TS AST
|
|
77
|
+
* transform. It works for the canonical `providers: [ ... ]` shape
|
|
78
|
+
* emitted by `ream new` and tolerates comments/whitespace around
|
|
79
|
+
* the array literal. It does NOT handle unusual shapes such as
|
|
80
|
+
* providers declared via a spread, a ternary, a `const providers =
|
|
81
|
+
* [...]` extracted above the config object, or a `providers:`
|
|
82
|
+
* identifier that appears inside a string literal or block comment
|
|
83
|
+
* elsewhere in the file. For those cases, edit `reamrc.ts` by hand.
|
|
84
|
+
* A full AST-based rewrite is tracked separately.
|
|
85
|
+
*/
|
|
86
|
+
async addProvider(importPath: string): Promise<void> {
|
|
87
|
+
const rcPath = path.join(root, 'reamrc.ts')
|
|
88
|
+
if (!fs.existsSync(rcPath)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[configure] reamrc.ts not found — cannot register provider ${importPath}. Run 'ream new' first.`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
// Same symlink-containment guard registerCommand/registerMiddleware/
|
|
94
|
+
// writeFile apply — a symlinked reamrc.ts must not redirect the
|
|
95
|
+
// write outside the project root.
|
|
96
|
+
assertCanonicallyInside(root, rcPath, 'reamrc.ts')
|
|
97
|
+
|
|
98
|
+
let content = fs.readFileSync(rcPath, 'utf8')
|
|
99
|
+
// Dedup check — any mention of the exact import path is treated
|
|
100
|
+
// as an existing registration. This is a coarse heuristic but
|
|
101
|
+
// safe because `importPath` is typically a package path like
|
|
102
|
+
// `@c9up/atlas/providers`, which should not appear in comments
|
|
103
|
+
// or unrelated strings in a typical `reamrc.ts`.
|
|
104
|
+
if (content.includes(`'${importPath}'`) || content.includes(`"${importPath}"`)) return
|
|
105
|
+
|
|
106
|
+
const entry = ` () => import('${importPath}'),`
|
|
107
|
+
// Marker tolerates extra whitespace and optional trailing comments
|
|
108
|
+
// between `providers:` and `[`. Still brittle to string-literal
|
|
109
|
+
// false-positives (see doc above).
|
|
110
|
+
const marker = /providers\s*:\s*(?:\/\*[\s\S]*?\*\/\s*)?\[/
|
|
111
|
+
const match = marker.exec(content)
|
|
112
|
+
if (!match) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`[configure] Could not find 'providers: [' in reamrc.ts — provider ${importPath} not added. Check your reamrc.ts format.`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const insertAt = match.index + match[0].length
|
|
118
|
+
const eol = detectLineEnding(content)
|
|
119
|
+
content = `${content.slice(0, insertAt)}${eol}${entry}${content.slice(insertAt)}`
|
|
120
|
+
fs.writeFileSync(rcPath, content)
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async addEnvVars(vars: Record<string, string>): Promise<void> {
|
|
124
|
+
const envPath = path.join(root, '.env')
|
|
125
|
+
// A symlinked .env must not redirect the write outside the root.
|
|
126
|
+
assertCanonicallyInside(root, envPath, '.env')
|
|
127
|
+
let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''
|
|
128
|
+
let added = 0
|
|
129
|
+
|
|
130
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
131
|
+
const pattern = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=`, 'm')
|
|
132
|
+
if (!pattern.test(content)) {
|
|
133
|
+
content += `${key}=${value}\n`
|
|
134
|
+
added++
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (added > 0) fs.writeFileSync(envPath, content)
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async writeFile(filePath: string, content: string, opts?: { force?: boolean }): Promise<void> {
|
|
142
|
+
if (path.isAbsolute(filePath)) {
|
|
143
|
+
throw new Error(`[configure] Absolute paths not allowed: ${filePath}`)
|
|
144
|
+
}
|
|
145
|
+
const resolved = path.resolve(root, filePath)
|
|
146
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
147
|
+
throw new Error(`[configure] Refusing to write outside project root: ${filePath}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const shouldForce = opts?.force ?? force
|
|
151
|
+
if (fs.existsSync(resolved) && !shouldForce) return
|
|
152
|
+
|
|
153
|
+
const dir = path.dirname(resolved)
|
|
154
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
155
|
+
|
|
156
|
+
const canonRoot = fs.realpathSync(root)
|
|
157
|
+
|
|
158
|
+
if (fs.existsSync(dir)) {
|
|
159
|
+
const canonDir = fs.realpathSync(dir)
|
|
160
|
+
if (!canonDir.startsWith(canonRoot + path.sep) && canonDir !== canonRoot) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`[configure] Symlink escape detected: ${filePath} — directory resolves outside project root`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(resolved)) {
|
|
168
|
+
const canonFile = fs.realpathSync(resolved)
|
|
169
|
+
if (!canonFile.startsWith(canonRoot + path.sep)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`[configure] Symlink escape detected: ${filePath} — file resolves outside project root`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fs.writeFileSync(resolved, content)
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Insert a command import into `reamrc.ts` `commands: [ ... ]` array.
|
|
181
|
+
*
|
|
182
|
+
* Mirrors `addProvider`'s shape: regex-based codemod (NOT a TS AST
|
|
183
|
+
* transform), idempotent string-match dedup against the exact import
|
|
184
|
+
* path quoted, `[configure]` error prefix on failure.
|
|
185
|
+
*
|
|
186
|
+
* Bootstraps the `commands:` field when absent — inserts immediately
|
|
187
|
+
* after the existing `providers: [...]` block when present, otherwise
|
|
188
|
+
* before the closing `})` of `defineConfig({...})`. The field is
|
|
189
|
+
* optional in `ReamrcConfig` (Ignitor.ts:55), so adding it is
|
|
190
|
+
* non-breaking.
|
|
191
|
+
*
|
|
192
|
+
* Indent convention: inserted entries use 4-space indent, matching the
|
|
193
|
+
* canonical depth-2 nesting inside `defineConfig({ providers: [ ... ] })`.
|
|
194
|
+
* (registerMiddleware uses 2-space indent because its target is the
|
|
195
|
+
* depth-1 `<tier>.use([ ... ])` call in `start/kernel.ts`.)
|
|
196
|
+
*
|
|
197
|
+
* Marker anchoring: top-level fields of `defineConfig({...})` are
|
|
198
|
+
* anchored to start-of-line + ≤4 leading spaces to reject false matches
|
|
199
|
+
* inside nested objects (e.g. a `tests: { commands: [...] }` field
|
|
200
|
+
* higher in `reamrc.ts`).
|
|
201
|
+
*/
|
|
202
|
+
async registerCommand(importPath: string): Promise<void> {
|
|
203
|
+
const rcPath = path.join(root, 'reamrc.ts')
|
|
204
|
+
if (!fs.existsSync(rcPath)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`[configure] reamrc.ts not found — cannot register command ${importPath}. Run 'ream new' first.`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
assertCanonicallyInside(root, rcPath, 'reamrc.ts')
|
|
210
|
+
|
|
211
|
+
let content = fs.readFileSync(rcPath, 'utf8')
|
|
212
|
+
if (content.includes(`'${importPath}'`) || content.includes(`"${importPath}"`)) return
|
|
213
|
+
|
|
214
|
+
const eol = detectLineEnding(content)
|
|
215
|
+
const entry = ` () => import('${importPath}'),`
|
|
216
|
+
|
|
217
|
+
// Anchor `commands:` to start-of-line + ≤4 leading spaces (top-level field of
|
|
218
|
+
// `defineConfig({...})`). Rejects nested matches like `tests: { commands: [...] }`.
|
|
219
|
+
const arrayMarker = /^[ \t]{0,4}commands\s*:\s*(?:\/\*[\s\S]*?\*\/\s*)?\[/m
|
|
220
|
+
const arrayMatch = arrayMarker.exec(content)
|
|
221
|
+
if (arrayMatch !== null) {
|
|
222
|
+
const insertAt = arrayMatch.index + arrayMatch[0].length
|
|
223
|
+
content = `${content.slice(0, insertAt)}${eol}${entry}${content.slice(insertAt)}`
|
|
224
|
+
fs.writeFileSync(rcPath, content)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const block = ` commands: [${eol}${entry}${eol} ],${eol}`
|
|
229
|
+
|
|
230
|
+
// Same start-of-line anchor as the commands marker — rejects nested `providers:`.
|
|
231
|
+
// Note: the non-greedy `\[[\s\S]*?\]` still finds the FIRST `]` after `providers:`,
|
|
232
|
+
// so a top-level `providers: [...]` containing nested `]` (e.g. tuple-typed entries)
|
|
233
|
+
// can still mis-match. Rare in practice; if hit, the user's reamrc.ts is in a shape
|
|
234
|
+
// the regex codemod cannot safely manipulate — they should add `commands: []`
|
|
235
|
+
// manually and re-run, or fall through to the defineConfig-close fallback below.
|
|
236
|
+
const providersEndMarker = /^[ \t]{0,4}providers\s*:\s*\[[\s\S]*?\],?[ \t]*\r?\n/m
|
|
237
|
+
const providersEnd = providersEndMarker.exec(content)
|
|
238
|
+
if (providersEnd !== null) {
|
|
239
|
+
const insertAt = providersEnd.index + providersEnd[0].length
|
|
240
|
+
content = `${content.slice(0, insertAt)}${block}${content.slice(insertAt)}`
|
|
241
|
+
fs.writeFileSync(rcPath, content)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Use the LAST `})` in the file (conventionally the `defineConfig({...})` close in a
|
|
246
|
+
// canonical `export default defineConfig({...})` reamrc). `lastIndexOf` avoids the
|
|
247
|
+
// /m-anchored `exec` returning the FIRST line ending in `})` — which would mis-target
|
|
248
|
+
// a helper above defineConfig like `const helper = makeProviders({ eager: true })`.
|
|
249
|
+
const defineCloseIdx = content.lastIndexOf('})')
|
|
250
|
+
if (defineCloseIdx !== -1) {
|
|
251
|
+
content = `${content.slice(0, defineCloseIdx)}${block}${content.slice(defineCloseIdx)}`
|
|
252
|
+
fs.writeFileSync(rcPath, content)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new Error(
|
|
257
|
+
`[configure] Could not find 'defineConfig({ ... })' in reamrc.ts — command ${importPath} not added. Check your reamrc.ts format.`,
|
|
258
|
+
)
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Insert a middleware import into `start/kernel.ts` `<tier>.use([ ... ])`
|
|
263
|
+
* array. `tier` defaults to `'router'` (matched routes only) — the
|
|
264
|
+
* conservative choice that does not run on 404s. Set `tier: 'server'`
|
|
265
|
+
* for middleware that must run on every request including 404s
|
|
266
|
+
* (security headers, request-id, structured logging).
|
|
267
|
+
*
|
|
268
|
+
* Mirrors `addProvider`'s shape: regex-based codemod, idempotent
|
|
269
|
+
* per-tier string-match dedup, `[configure]` error prefix.
|
|
270
|
+
*
|
|
271
|
+
* KNOWN LIMITATION: does NOT bootstrap a missing `<tier>.use([])`
|
|
272
|
+
* block. Unlike `commands`, the `server.use` / `router.use` calls
|
|
273
|
+
* are user-authored idiom in `start/kernel.ts`, not a config-shape
|
|
274
|
+
* contract — synthesising them would require also synthesising the
|
|
275
|
+
* `import server from '@c9up/ream/services/server'` boilerplate, and
|
|
276
|
+
* is footgun-prone if the user has renamed identifiers. When the
|
|
277
|
+
* targeted block is absent, this method errors and asks the user
|
|
278
|
+
* to add the block manually.
|
|
279
|
+
*
|
|
280
|
+
* Cross-tier collision is rejected: registering the same import path
|
|
281
|
+
* in BOTH `server` and `router` tiers is almost certainly a mistake;
|
|
282
|
+
* the second call throws `[configure] middleware <importPath> is
|
|
283
|
+
* already registered in <other-tier> tier`.
|
|
284
|
+
*
|
|
285
|
+
* Indent convention: inserted entries use 2-space indent, matching
|
|
286
|
+
* the canonical depth-1 nesting inside `<tier>.use([ ... ])` at the
|
|
287
|
+
* top of `start/kernel.ts`. (registerCommand uses 4-space indent
|
|
288
|
+
* because its target is the depth-2 `defineConfig({ commands: [ ... ] })`
|
|
289
|
+
* block in `reamrc.ts`.)
|
|
290
|
+
*/
|
|
291
|
+
async registerMiddleware(
|
|
292
|
+
importPath: string,
|
|
293
|
+
options?: { tier?: MiddlewareTier },
|
|
294
|
+
): Promise<void> {
|
|
295
|
+
const requestedTier = options?.tier ?? 'router'
|
|
296
|
+
if (!isMiddlewareTier(requestedTier)) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`[configure] Invalid middleware tier: '${String(requestedTier)}'. Expected 'server' or 'router'.`,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tier: MiddlewareTier = requestedTier
|
|
303
|
+
const otherTier: MiddlewareTier = tier === 'server' ? 'router' : 'server'
|
|
304
|
+
|
|
305
|
+
const kernelPath = path.join(root, 'start', 'kernel.ts')
|
|
306
|
+
if (!fs.existsSync(kernelPath)) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`[configure] start/kernel.ts not found — cannot register middleware ${importPath}. Run 'ream new' first or create the file from the installation guide.`,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
assertCanonicallyInside(root, kernelPath, 'start/kernel.ts')
|
|
312
|
+
|
|
313
|
+
let content = fs.readFileSync(kernelPath, 'utf8')
|
|
314
|
+
|
|
315
|
+
const otherBlock = findTierBlock(content, otherTier)
|
|
316
|
+
if (otherBlock !== null) {
|
|
317
|
+
const otherSlice = content.slice(otherBlock.start, otherBlock.end)
|
|
318
|
+
if (otherSlice.includes(`'${importPath}'`) || otherSlice.includes(`"${importPath}"`)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`[configure] middleware ${importPath} is already registered in ${otherTier} tier — cannot also register in ${tier} tier. Choose one tier or remove the existing registration.`,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const targetBlock = findTierBlock(content, tier)
|
|
326
|
+
if (targetBlock === null) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`[configure] Could not find '${tier}.use([' in start/kernel.ts — middleware ${importPath} not added. Add the block manually or check your start/kernel.ts format.`,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const targetSlice = content.slice(targetBlock.start, targetBlock.end)
|
|
333
|
+
if (targetSlice.includes(`'${importPath}'`) || targetSlice.includes(`"${importPath}"`)) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const eol = detectLineEnding(content)
|
|
338
|
+
const entry = ` () => import('${importPath}'),`
|
|
339
|
+
content = `${content.slice(0, targetBlock.start)}${eol}${entry}${content.slice(targetBlock.start)}`
|
|
340
|
+
fs.writeFileSync(kernelPath, content)
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader — reads config/*.ts + .env files.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR18, FR19
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-memory config store. In production this is populated from config/*.ts
|
|
9
|
+
* files via `defineConfig()`. The class is its own contract — `AppContext.config`
|
|
10
|
+
* is typed against it directly (structural).
|
|
11
|
+
*/
|
|
12
|
+
export class ConfigStore {
|
|
13
|
+
private store: Map<string, unknown> = new Map()
|
|
14
|
+
|
|
15
|
+
get<T = unknown>(key: string): T | undefined {
|
|
16
|
+
return this.store.get(key) as T | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set(key: string, value: unknown): void {
|
|
20
|
+
this.store.set(key, value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
has(key: string): boolean {
|
|
24
|
+
return this.store.has(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Load config from an object (used for testing and initial setup). */
|
|
28
|
+
loadFromObject(config: Record<string, unknown>): void {
|
|
29
|
+
for (const [key, value] of Object.entries(config)) {
|
|
30
|
+
this.store.set(key, value)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read an environment variable with optional default.
|
|
37
|
+
* @implements FR19
|
|
38
|
+
*/
|
|
39
|
+
export function env(key: string, defaultValue?: string): string | undefined {
|
|
40
|
+
return process.env[key] ?? defaultValue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Define typed configuration for a module.
|
|
45
|
+
* Returns the config object as-is (type-checked by the module's defineConfig).
|
|
46
|
+
*/
|
|
47
|
+
export function defineModuleConfig<T>(config: T): T {
|
|
48
|
+
return config
|
|
49
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root Error Boundary — all errors flow through the bus.
|
|
3
|
+
*
|
|
4
|
+
* @implements Story 4.8
|
|
5
|
+
*
|
|
6
|
+
* Catches all uncaught errors and emits them as event-bus events:
|
|
7
|
+
* - service.error — handler/business logic failures
|
|
8
|
+
* - security.rejected — Blackhole rejections
|
|
9
|
+
* - system.error — infrastructure failures (NAPI, DB)
|
|
10
|
+
* - system.fatal — unrecoverable process-level errors
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type ErrorSeverity = 'info' | 'warning' | 'critical'
|
|
14
|
+
|
|
15
|
+
export interface ErrorEvent {
|
|
16
|
+
type: 'service.error' | 'security.rejected' | 'system.error' | 'system.fatal' | 'system.info'
|
|
17
|
+
source: string
|
|
18
|
+
message: string
|
|
19
|
+
severity: ErrorSeverity
|
|
20
|
+
correlationId?: string
|
|
21
|
+
originalError?: string
|
|
22
|
+
timestamp: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ErrorEmitter = (event: ErrorEvent) => void
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Root Error Boundary.
|
|
29
|
+
*
|
|
30
|
+
* Wraps the entire application lifecycle:
|
|
31
|
+
* - Catches unhandledRejection and uncaughtException
|
|
32
|
+
* - Emits structured error events via the provided emitter (event bus)
|
|
33
|
+
* - In dev mode, also logs to console
|
|
34
|
+
*/
|
|
35
|
+
export class ErrorBoundary {
|
|
36
|
+
private emitter: ErrorEmitter
|
|
37
|
+
private devMode: boolean
|
|
38
|
+
private installed = false
|
|
39
|
+
private rejectionHandler?: (reason: unknown) => void
|
|
40
|
+
private exceptionHandler?: (error: Error) => void
|
|
41
|
+
|
|
42
|
+
constructor(emitter: ErrorEmitter, devMode = false) {
|
|
43
|
+
this.emitter = emitter
|
|
44
|
+
this.devMode = devMode
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Install global error handlers. */
|
|
48
|
+
install(): void {
|
|
49
|
+
if (this.installed) return
|
|
50
|
+
|
|
51
|
+
// Skip global install under vitest — each test instantiates its own
|
|
52
|
+
// Ignitor and rarely calls `.stop()`, so per-test listeners pile up on
|
|
53
|
+
// the singleton `process` and trigger Node's MaxListenersExceeded
|
|
54
|
+
// warning. Production code-paths still get coverage via direct
|
|
55
|
+
// `handleError`/`serviceError` calls; only the Node-level signal
|
|
56
|
+
// bridging is suppressed.
|
|
57
|
+
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
|
|
58
|
+
this.installed = true
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.rejectionHandler = (reason) => {
|
|
63
|
+
this.handleError('system.fatal', 'UnhandledRejection', reason)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.exceptionHandler = (error) => {
|
|
67
|
+
this.handleError('system.fatal', 'UncaughtException', error)
|
|
68
|
+
// Node.js is in undefined state after uncaughtException — must exit
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.on('unhandledRejection', this.rejectionHandler)
|
|
73
|
+
process.on('uncaughtException', this.exceptionHandler)
|
|
74
|
+
|
|
75
|
+
this.installed = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Uninstall global error handlers (for testing). Only removes OUR handlers. */
|
|
79
|
+
uninstall(): void {
|
|
80
|
+
if (this.rejectionHandler) {
|
|
81
|
+
process.removeListener('unhandledRejection', this.rejectionHandler)
|
|
82
|
+
}
|
|
83
|
+
if (this.exceptionHandler) {
|
|
84
|
+
process.removeListener('uncaughtException', this.exceptionHandler)
|
|
85
|
+
}
|
|
86
|
+
this.rejectionHandler = undefined
|
|
87
|
+
this.exceptionHandler = undefined
|
|
88
|
+
this.installed = false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Emit a service error (handler failure). */
|
|
92
|
+
serviceError(source: string, error: unknown, correlationId?: string): void {
|
|
93
|
+
this.handleError('service.error', source, error, correlationId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Emit a security rejection. */
|
|
97
|
+
securityRejected(source: string, reason: string, correlationId?: string): void {
|
|
98
|
+
const event: ErrorEvent = {
|
|
99
|
+
type: 'security.rejected',
|
|
100
|
+
source,
|
|
101
|
+
message: reason,
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
correlationId,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
}
|
|
106
|
+
this.emit(event)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Emit a system error (infrastructure failure). */
|
|
110
|
+
systemError(source: string, error: unknown, correlationId?: string): void {
|
|
111
|
+
this.handleError('system.error', source, error, correlationId)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private handleError(
|
|
115
|
+
type: ErrorEvent['type'],
|
|
116
|
+
source: string,
|
|
117
|
+
error: unknown,
|
|
118
|
+
correlationId?: string,
|
|
119
|
+
): void {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
121
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
122
|
+
|
|
123
|
+
const event: ErrorEvent = {
|
|
124
|
+
type,
|
|
125
|
+
source,
|
|
126
|
+
message,
|
|
127
|
+
severity:
|
|
128
|
+
type === 'system.fatal' ? 'critical' : type === 'system.error' ? 'critical' : 'warning',
|
|
129
|
+
correlationId,
|
|
130
|
+
originalError: stack ?? message,
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.emit(event)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private emit(event: ErrorEvent): void {
|
|
138
|
+
try {
|
|
139
|
+
this.emitter(event)
|
|
140
|
+
} catch {
|
|
141
|
+
// If the emitter itself fails, log to stderr as last resort
|
|
142
|
+
process.stderr.write(`[ErrorBoundary] Failed to emit: ${JSON.stringify(event)}\n`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.devMode) {
|
|
146
|
+
const prefix =
|
|
147
|
+
event.type === 'system.fatal'
|
|
148
|
+
? '✗ FATAL'
|
|
149
|
+
: event.type === 'system.error'
|
|
150
|
+
? '✗ ERROR'
|
|
151
|
+
: event.type === 'security.rejected'
|
|
152
|
+
? '⚠ SECURITY'
|
|
153
|
+
: '✗ SERVICE'
|
|
154
|
+
process.stderr.write(`${prefix} [${event.source}] ${event.message}\n`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful Shutdown — handles SIGTERM/SIGINT with drain timeout.
|
|
3
|
+
*
|
|
4
|
+
* @implements FR82
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ShutdownOptions {
|
|
8
|
+
/** Maximum time to wait for drain in ms (default: 30000). */
|
|
9
|
+
drainTimeout?: number
|
|
10
|
+
/** Callback to execute during shutdown. */
|
|
11
|
+
onShutdown: () => Promise<void>
|
|
12
|
+
/** Optional logger. */
|
|
13
|
+
logger?: { info: (msg: string) => void; error: (msg: string) => void }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ShutdownHandle {
|
|
17
|
+
/** Manually trigger shutdown (for testing). */
|
|
18
|
+
trigger: () => Promise<void>
|
|
19
|
+
/** Remove signal listeners (for testing cleanup). */
|
|
20
|
+
cleanup: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Install graceful shutdown handlers for SIGTERM and SIGINT.
|
|
25
|
+
* Returns a handle to manually trigger shutdown or clean up listeners.
|
|
26
|
+
*/
|
|
27
|
+
export function installGracefulShutdown(options: ShutdownOptions): ShutdownHandle {
|
|
28
|
+
const drainTimeout = options.drainTimeout ?? 30_000
|
|
29
|
+
const logger = options.logger ?? { info: () => {}, error: () => {} }
|
|
30
|
+
let shutdownInProgress = false
|
|
31
|
+
|
|
32
|
+
const shutdown = async () => {
|
|
33
|
+
if (shutdownInProgress) return
|
|
34
|
+
shutdownInProgress = true
|
|
35
|
+
|
|
36
|
+
logger.info('Graceful shutdown initiated...')
|
|
37
|
+
|
|
38
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
39
|
+
|
|
40
|
+
const drainPromise = options.onShutdown().catch((err) => {
|
|
41
|
+
logger.error(`Shutdown error: ${err instanceof Error ? err.message : String(err)}`)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const timeoutPromise = new Promise<void>((resolve) => {
|
|
45
|
+
timeoutHandle = setTimeout(() => {
|
|
46
|
+
logger.error(`Drain timeout exceeded (${drainTimeout}ms) — forcing shutdown`)
|
|
47
|
+
resolve()
|
|
48
|
+
}, drainTimeout)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await Promise.race([drainPromise, timeoutPromise])
|
|
52
|
+
if (timeoutHandle) clearTimeout(timeoutHandle)
|
|
53
|
+
|
|
54
|
+
logger.info('Shutdown complete')
|
|
55
|
+
process.exit(0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const onSigterm = () => {
|
|
59
|
+
shutdown().catch((err) => {
|
|
60
|
+
logger.error(`Fatal: ${err}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
const onSigint = () => {
|
|
65
|
+
shutdown().catch((err) => {
|
|
66
|
+
logger.error(`Fatal: ${err}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.on('SIGTERM', onSigterm)
|
|
72
|
+
process.on('SIGINT', onSigint)
|
|
73
|
+
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
process.off('SIGTERM', onSigterm)
|
|
76
|
+
process.off('SIGINT', onSigint)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { trigger: shutdown, cleanup }
|
|
80
|
+
}
|