@zerotal/core 1.0.0
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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Zerotal kernel — the IoC container, application lifecycle, HTTP router and
|
|
3
|
+
* pipeline, service providers, events, facades, and the common request helpers
|
|
4
|
+
* that every Zerotal app builds on.
|
|
5
|
+
*
|
|
6
|
+
* `@zerotal/core` is the one package every app depends on. This root entry
|
|
7
|
+
* carries only the hot, cheap kernel used across the framework — `Application`,
|
|
8
|
+
* the {@link Container}, {@link HttpContext}, the {@link Router}, middleware,
|
|
9
|
+
* providers, {@link Emitter | events}, facades, and helpers. Heavier or cohesive
|
|
10
|
+
* subsystems live behind explicit subpaths so importing the kernel never drags
|
|
11
|
+
* them in:
|
|
12
|
+
*
|
|
13
|
+
* | Subpath | What it provides |
|
|
14
|
+
* | --- | --- |
|
|
15
|
+
* | `@zerotal/core/contracts` | Interface-only seams packages implement and the kernel consumes (session, transaction, authenticatable user) |
|
|
16
|
+
* | `@zerotal/core/carbon` | Date/time via {@link Carbon} (pulls the Temporal polyfill) |
|
|
17
|
+
* | `@zerotal/core/http` | Outbound HTTP client, URL building, uploads, API resources, content negotiation |
|
|
18
|
+
* | `@zerotal/core/view` | Server-side JSX runtime + authoring helpers |
|
|
19
|
+
* | `@zerotal/core/env` | Typed environment-variable schema |
|
|
20
|
+
* | `@zerotal/core/config` | Config manager/loader + app config shapes |
|
|
21
|
+
* | `@zerotal/core/security` | `Crypt` + `Hash` |
|
|
22
|
+
* | `@zerotal/core/logger` | Structured logging (`Log`, channels) |
|
|
23
|
+
* | `@zerotal/core/lock` | Distributed locks |
|
|
24
|
+
* | `@zerotal/core/commands` | Built-in CLI commands |
|
|
25
|
+
* | `@zerotal/core/dev` | Dev-only build/reload tooling (owns `Bun.build`) |
|
|
26
|
+
* | `@zerotal/core/assets` | Asset URL helper + versioning |
|
|
27
|
+
* | `@zerotal/core/health` | Health checks |
|
|
28
|
+
* | `@zerotal/core/metrics` | HTTP request metrics |
|
|
29
|
+
*
|
|
30
|
+
* @example Bootstrap an application
|
|
31
|
+
* ```ts
|
|
32
|
+
* // bootstrap/app.ts
|
|
33
|
+
* import { Application } from "@zerotal/core";
|
|
34
|
+
* import providers from "./providers.ts";
|
|
35
|
+
*
|
|
36
|
+
* const app = Application.create({ providers, env: "web" });
|
|
37
|
+
* await app.start();
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example Define routes and a controller
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { Router } from "@zerotal/core";
|
|
43
|
+
*
|
|
44
|
+
* Router.get("/", (ctx) => ctx.html("<h1>Hello</h1>"));
|
|
45
|
+
* Router.get("/users/{id}", (ctx) => ctx.json({ id: ctx.params.id }));
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* Zerotal runs on **Bun ≥ 1.1** — Node.js is not supported. The framework uses
|
|
50
|
+
* `Bun.sql`, `Bun.CryptoHasher`, `Bun.build`, and other Bun-native APIs
|
|
51
|
+
* throughout.
|
|
52
|
+
*
|
|
53
|
+
* @packageDocumentation
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
// Application
|
|
57
|
+
|
|
58
|
+
// Application
|
|
59
|
+
export { Application, registerAppScope } from "./application/Application.ts";
|
|
60
|
+
export { currentApp, tryCurrentApp, withApp } from "./application/currentApp.ts";
|
|
61
|
+
export type { AuthenticatedUser } from "./auth/AuthenticatedUser.ts";
|
|
62
|
+
export type {
|
|
63
|
+
WebSocketHandlers,
|
|
64
|
+
RoutingEntry,
|
|
65
|
+
RoutingConfig,
|
|
66
|
+
FileRoutingEntry,
|
|
67
|
+
FileRoutingConfig,
|
|
68
|
+
AppScopeInstaller,
|
|
69
|
+
} from "./application/Application.ts";
|
|
70
|
+
export { ExceptionHandler } from "./application/ExceptionHandler.ts";
|
|
71
|
+
|
|
72
|
+
// Container
|
|
73
|
+
export { Container } from "./container/Container.ts";
|
|
74
|
+
export { ScopedResolver } from "./container/ScopedResolver.ts";
|
|
75
|
+
export { inject } from "./container/inject.ts";
|
|
76
|
+
export type { ContainerBindings, BindingToken, Factory } from "./container/types.ts";
|
|
77
|
+
|
|
78
|
+
// Context
|
|
79
|
+
export { RequestContext } from "./context/RequestContext.ts";
|
|
80
|
+
|
|
81
|
+
// Pipeline
|
|
82
|
+
export { HttpContext, safeRedirectPath } from "./pipeline/HttpContext.ts";
|
|
83
|
+
export { currentPage, setCurrentPageResolver } from "./pipeline/currentPage.ts";
|
|
84
|
+
export { pageElements } from "./helpers/pageElements.ts";
|
|
85
|
+
export type { CurrentPageResolver } from "./pipeline/currentPage.ts";
|
|
86
|
+
export type { ContextRegistry, ContextKey, ContextValue } from "./pipeline/ContextRegistry.ts";
|
|
87
|
+
export type { CompiledDomain } from "./router/domain.ts";
|
|
88
|
+
export type { RequestIPProvider } from "./pipeline/HttpContext.ts";
|
|
89
|
+
export { Pipeline } from "./pipeline/Pipeline.ts";
|
|
90
|
+
export type { Pipe, NextFn, HasResponse, HttpResponse } from "./pipeline/types.ts";
|
|
91
|
+
|
|
92
|
+
// Provider
|
|
93
|
+
export { ServiceProvider } from "./provider/ServiceProvider.ts";
|
|
94
|
+
export type { AppEnvironment } from "./provider/ServiceProvider.ts";
|
|
95
|
+
export type { ConcernDescriptor, ConcernContext } from "./conventions/ConventionLoader.ts";
|
|
96
|
+
|
|
97
|
+
// Errors
|
|
98
|
+
export {
|
|
99
|
+
ZerotalError,
|
|
100
|
+
HttpError,
|
|
101
|
+
BadRequestError,
|
|
102
|
+
NotFoundError,
|
|
103
|
+
UnauthorizedError,
|
|
104
|
+
ForbiddenError,
|
|
105
|
+
MethodNotAllowedError,
|
|
106
|
+
ConflictError,
|
|
107
|
+
GoneError,
|
|
108
|
+
UnprocessableEntityError,
|
|
109
|
+
TooManyRequestsError,
|
|
110
|
+
ServiceUnavailableError,
|
|
111
|
+
ValidationError,
|
|
112
|
+
ConfigError,
|
|
113
|
+
BindingNotFoundError,
|
|
114
|
+
ContainerLockedError,
|
|
115
|
+
} from "./errors/index.ts";
|
|
116
|
+
|
|
117
|
+
// Events
|
|
118
|
+
export { Emitter } from "./events/Emitter.ts";
|
|
119
|
+
export { EventFake } from "./events/EventFake.ts";
|
|
120
|
+
export {
|
|
121
|
+
FrameworkEvents,
|
|
122
|
+
AppBooted,
|
|
123
|
+
RequestHandled,
|
|
124
|
+
RequestFailed,
|
|
125
|
+
OutgoingRequestCompleted,
|
|
126
|
+
MiddlewareSkipped,
|
|
127
|
+
CommandRan,
|
|
128
|
+
} from "./events/FrameworkEvents.ts";
|
|
129
|
+
export { CallQueuedListener } from "./events/CallQueuedListener.ts";
|
|
130
|
+
export type { QueuedListener } from "./events/Emitter.ts";
|
|
131
|
+
|
|
132
|
+
// Helpers
|
|
133
|
+
export {
|
|
134
|
+
env,
|
|
135
|
+
requireEnv,
|
|
136
|
+
setAppEnv,
|
|
137
|
+
basePath,
|
|
138
|
+
Str,
|
|
139
|
+
tap,
|
|
140
|
+
tapAsync,
|
|
141
|
+
pipe,
|
|
142
|
+
pipeAsync,
|
|
143
|
+
rescue,
|
|
144
|
+
rescueSync,
|
|
145
|
+
data_get,
|
|
146
|
+
} from "./helpers/index.ts";
|
|
147
|
+
export { config } from "./helpers/config.ts";
|
|
148
|
+
export { pluralize, singularize, snakeCase, camelCase, tableNameFor } from "./support/str.ts";
|
|
149
|
+
export { deepMerge } from "./support/deepMerge.ts";
|
|
150
|
+
export { safeEqual, sha256Hex, hmacHex } from "./support/crypto.ts";
|
|
151
|
+
export {
|
|
152
|
+
buildCookie,
|
|
153
|
+
readCookie,
|
|
154
|
+
parseCookieHeader,
|
|
155
|
+
type CookieOptions,
|
|
156
|
+
type SameSite,
|
|
157
|
+
} from "./support/cookie.ts";
|
|
158
|
+
export { isDevSurfaceAllowed, devSurfacesEnabled } from "./support/env.ts";
|
|
159
|
+
export { fluent, Fluent } from "./helpers/fluent.ts";
|
|
160
|
+
export { collect, Collection } from "./helpers/Collection.ts";
|
|
161
|
+
export {
|
|
162
|
+
ResponseBuilder,
|
|
163
|
+
RedirectBuilder,
|
|
164
|
+
MarkdownBuilder,
|
|
165
|
+
redirect,
|
|
166
|
+
redirectTo,
|
|
167
|
+
json,
|
|
168
|
+
view,
|
|
169
|
+
html,
|
|
170
|
+
markdown,
|
|
171
|
+
file,
|
|
172
|
+
abort,
|
|
173
|
+
} from "./helpers/response.ts";
|
|
174
|
+
export type { ControllerResponse } from "./helpers/response.ts";
|
|
175
|
+
export { request } from "./helpers/request.ts";
|
|
176
|
+
export { make, app } from "./helpers/make.ts";
|
|
177
|
+
|
|
178
|
+
// Router
|
|
179
|
+
export {
|
|
180
|
+
RouterState,
|
|
181
|
+
route,
|
|
182
|
+
ResourceRouteBuilder,
|
|
183
|
+
setImplicitModelResolver,
|
|
184
|
+
} from "./router/Router.ts";
|
|
185
|
+
export type {
|
|
186
|
+
RouteRegistration,
|
|
187
|
+
ViewRegistration,
|
|
188
|
+
GroupOptions,
|
|
189
|
+
RouterMacros,
|
|
190
|
+
} from "./router/Router.ts";
|
|
191
|
+
export type {
|
|
192
|
+
RouteDefinition,
|
|
193
|
+
HttpMethod,
|
|
194
|
+
MiddlewareClass,
|
|
195
|
+
ModelClass,
|
|
196
|
+
ModelBindingResolver,
|
|
197
|
+
ViewComponent,
|
|
198
|
+
ViewLayout,
|
|
199
|
+
} from "./router/Route.ts";
|
|
200
|
+
export { scanFileRoutes, registerFileRouteResolver } from "./router/FileRouter.ts";
|
|
201
|
+
export type { FileRouteResolver, FileRouteContext } from "./router/FileRouter.ts";
|
|
202
|
+
export type {
|
|
203
|
+
FileHandler,
|
|
204
|
+
RouteModule,
|
|
205
|
+
RouteFileMeta,
|
|
206
|
+
RouteMiddleware,
|
|
207
|
+
RouteMethodMiddleware,
|
|
208
|
+
MiddlewareModule,
|
|
209
|
+
} from "./router/FileRouter.ts";
|
|
210
|
+
export type { FileHandler as FileRouteHandler } from "./router/FileRouter.ts";
|
|
211
|
+
|
|
212
|
+
// Command
|
|
213
|
+
export { Command } from "./command/Command.ts";
|
|
214
|
+
export type { ArgDef, FlagDef } from "./command/Command.ts";
|
|
215
|
+
export { CommandRunner } from "./command/CommandRunner.ts";
|
|
216
|
+
export { startZerotal } from "./command/startZerotal.ts";
|
|
217
|
+
export type { StartZerotalOptions } from "./command/startZerotal.ts";
|
|
218
|
+
|
|
219
|
+
// OutputWriter
|
|
220
|
+
export { TerminalWriter, BufferWriter } from "./command/OutputWriter.ts";
|
|
221
|
+
export type { OutputWriter } from "./command/OutputWriter.ts";
|
|
222
|
+
|
|
223
|
+
// Facades
|
|
224
|
+
export { createFacade } from "./facade/Facade.ts";
|
|
225
|
+
export { App, Config, Events, Artisan } from "./facade/facades/index.ts";
|
|
226
|
+
export type { ArtisanResult } from "./facade/facades/index.ts";
|
|
227
|
+
|
|
228
|
+
// Middleware
|
|
229
|
+
export { BaseMiddleware } from "./middleware/BaseMiddleware.ts";
|
|
230
|
+
export { ThrottleMiddleware } from "./middleware/ThrottleMiddleware.ts";
|
|
231
|
+
export type { ThrottleOptions } from "./middleware/ThrottleMiddleware.ts";
|
|
232
|
+
export { CorsMiddleware } from "./middleware/CorsMiddleware.ts";
|
|
233
|
+
export type { CorsOptions } from "./middleware/CorsMiddleware.ts";
|
|
234
|
+
export { RateLimiter, LimiterDefinition } from "./middleware/RateLimiter.ts";
|
|
235
|
+
export { SecureHeadersMiddleware } from "./middleware/SecureHeadersMiddleware.ts";
|
|
236
|
+
export type { SecureHeadersOptions } from "./middleware/SecureHeadersMiddleware.ts";
|
|
237
|
+
export { WebhookMiddleware } from "./middleware/WebhookMiddleware.ts";
|
|
238
|
+
export type { WebhookOptions } from "./middleware/WebhookMiddleware.ts";
|
|
239
|
+
|
|
240
|
+
// Response header helper — reconstructs (never mutates) so immutable responses are safe
|
|
241
|
+
export { withHeaders } from "./http/withHeaders.ts";
|
|
242
|
+
|
|
243
|
+
import { Router as _Router } from "./router/Router.ts";
|
|
244
|
+
import type { RouterMacros as _RouterMacros } from "./router/Router.ts";
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The route registry, macro-aware: methods packages add via `Router.macro()`
|
|
248
|
+
* (`Router.flow`, `Router.inertia`, …) are visible to TypeScript without a cast.
|
|
249
|
+
*/
|
|
250
|
+
export const Router: typeof _Router & _RouterMacros = _Router as typeof _Router & _RouterMacros;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { LockDriver } from "./drivers/LockDriver.ts";
|
|
2
|
+
import { LockNotAcquiredError } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options controlling how {@link LockManager.block} and {@link Lock.block} wait
|
|
6
|
+
* for a busy lock.
|
|
7
|
+
*
|
|
8
|
+
* @category Acquiring
|
|
9
|
+
*/
|
|
10
|
+
export interface BlockOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Maximum seconds to wait for the lock before throwing.
|
|
13
|
+
* Defaults to the lock TTL.
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Milliseconds between polling attempts while waiting.
|
|
19
|
+
* Default: 100 ms.
|
|
20
|
+
*/
|
|
21
|
+
retryDelay?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single named lock instance.
|
|
26
|
+
*
|
|
27
|
+
* Each instance holds a unique `owner` token so that release is always
|
|
28
|
+
* owner-guarded — a process that held a lock which expired cannot accidentally
|
|
29
|
+
* release the lock of a new holder that acquired it during the gap.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const lock = manager.lock('invoice:123', 10);
|
|
34
|
+
* await lock.acquire(); // try once → returns boolean
|
|
35
|
+
* await lock.block(30); // wait up to 30s → throws on timeout
|
|
36
|
+
* await lock.release(); // release (no-op if not acquired or expired)
|
|
37
|
+
* await lock.forceRelease(); // unconditionally remove
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @category Acquiring
|
|
41
|
+
*/
|
|
42
|
+
export class ManagedLock {
|
|
43
|
+
private readonly _owner: string;
|
|
44
|
+
private _acquired = false;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly _key: string,
|
|
48
|
+
private readonly _ttl: number,
|
|
49
|
+
private readonly _driver: LockDriver,
|
|
50
|
+
) {
|
|
51
|
+
this._owner = crypto.randomUUID();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Try to acquire exactly once. Returns `true` on success. Re-acquiring while
|
|
56
|
+
* this same instance already holds the key refreshes it (returns `true`).
|
|
57
|
+
*
|
|
58
|
+
* @category Acquiring
|
|
59
|
+
*/
|
|
60
|
+
async acquire(): Promise<boolean> {
|
|
61
|
+
this._acquired = await this._driver.acquire(this._key, this._owner, this._ttl);
|
|
62
|
+
return this._acquired;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Block until the lock can be acquired or `timeoutSeconds` elapses, polling
|
|
67
|
+
* every `retryDelayMs`.
|
|
68
|
+
*
|
|
69
|
+
* @param timeoutSeconds - Maximum seconds to wait before giving up.
|
|
70
|
+
* @param retryDelayMs - Milliseconds between acquire attempts (default 100).
|
|
71
|
+
* @throws {LockNotAcquiredError} On timeout.
|
|
72
|
+
* @category Acquiring
|
|
73
|
+
*/
|
|
74
|
+
async block(timeoutSeconds: number, retryDelayMs = 100): Promise<void> {
|
|
75
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
76
|
+
|
|
77
|
+
while (true) {
|
|
78
|
+
if (await this.acquire()) return;
|
|
79
|
+
if (Date.now() >= deadline) break;
|
|
80
|
+
await Bun.sleep(retryDelayMs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new LockNotAcquiredError(this._key);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Release the lock. Owner-guarded at the driver level, so it only removes the
|
|
88
|
+
* lock if this instance still holds it. No-op when this instance never
|
|
89
|
+
* acquired it or it already expired.
|
|
90
|
+
*
|
|
91
|
+
* @category Releasing
|
|
92
|
+
*/
|
|
93
|
+
async release(): Promise<void> {
|
|
94
|
+
if (!this._acquired) return;
|
|
95
|
+
this._acquired = false;
|
|
96
|
+
await this._driver.release(this._key, this._owner);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unconditionally remove the lock regardless of who holds it. Use with care —
|
|
101
|
+
* it can release a lock owned by another holder.
|
|
102
|
+
*
|
|
103
|
+
* @category Releasing
|
|
104
|
+
*/
|
|
105
|
+
async forceRelease(): Promise<void> {
|
|
106
|
+
this._acquired = false;
|
|
107
|
+
await this._driver.forceRelease(this._key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The logical lock name. */
|
|
111
|
+
get key(): string {
|
|
112
|
+
return this._key;
|
|
113
|
+
}
|
|
114
|
+
/** Whether this instance currently believes it holds the lock. */
|
|
115
|
+
get isAcquired(): boolean {
|
|
116
|
+
return this._acquired;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* High-level entry point for distributed locking, backed by a pluggable
|
|
122
|
+
* {@link LockDriver} (memory, SQLite, or Redis).
|
|
123
|
+
*
|
|
124
|
+
* Offers three usage styles: {@link LockManager.try | try} (fail fast),
|
|
125
|
+
* {@link LockManager.block | block} (wait for the lock), and
|
|
126
|
+
* {@link LockManager.lock | lock} (a manual {@link ManagedLock} handle). Both
|
|
127
|
+
* `try` and `block` always release the lock, even when the callback throws.
|
|
128
|
+
* Normally resolved via the {@link Lock} facade rather than constructed
|
|
129
|
+
* directly.
|
|
130
|
+
*
|
|
131
|
+
* @category Acquiring
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* // Fail fast: throw immediately if the lock is busy.
|
|
136
|
+
* await manager.try("invoice:123", 10, async () => {
|
|
137
|
+
* await processInvoice(123);
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* // Blocking: wait up to 30s for the lock to free.
|
|
141
|
+
* await manager.block("invoice:123", 10, async () => {
|
|
142
|
+
* await processInvoice(123);
|
|
143
|
+
* }, { timeout: 30 });
|
|
144
|
+
*
|
|
145
|
+
* // Manual, for flows that span multiple steps.
|
|
146
|
+
* const lock = manager.lock("invoice:123", 10);
|
|
147
|
+
* if (await lock.acquire()) {
|
|
148
|
+
* try { await processInvoice(123); } finally { await lock.release(); }
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export class LockManager {
|
|
153
|
+
/** @param _driver - Storage backend that performs the atomic acquire/release operations. */
|
|
154
|
+
constructor(private readonly _driver: LockDriver) {}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build a named {@link ManagedLock} handle for manual acquire/release flows.
|
|
158
|
+
* Does not acquire the lock.
|
|
159
|
+
*
|
|
160
|
+
* @param key - Logical lock name.
|
|
161
|
+
* @param ttlSeconds - Time-to-live, in seconds, after which the lock auto-expires.
|
|
162
|
+
* @category Acquiring
|
|
163
|
+
*/
|
|
164
|
+
lock(key: string, ttlSeconds: number): ManagedLock {
|
|
165
|
+
return new ManagedLock(key, ttlSeconds, this._driver);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Try to acquire the lock exactly once, execute `callback`, then release.
|
|
170
|
+
* The lock is always released — even when `callback` throws.
|
|
171
|
+
*
|
|
172
|
+
* @param key - Logical lock name.
|
|
173
|
+
* @param ttlSeconds - Lock time-to-live in seconds.
|
|
174
|
+
* @param callback - Critical section to run while the lock is held.
|
|
175
|
+
* @returns The value returned by `callback`.
|
|
176
|
+
* @throws {LockNotAcquiredError} Immediately, if the lock is already held.
|
|
177
|
+
* @category Acquiring
|
|
178
|
+
*/
|
|
179
|
+
async try<T>(key: string, ttlSeconds: number, callback: () => Promise<T> | T): Promise<T> {
|
|
180
|
+
const lock = this.lock(key, ttlSeconds);
|
|
181
|
+
const acquired = await lock.acquire();
|
|
182
|
+
if (!acquired) throw new LockNotAcquiredError(key);
|
|
183
|
+
try {
|
|
184
|
+
return await callback();
|
|
185
|
+
} finally {
|
|
186
|
+
await lock.release();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Block until the lock can be acquired (up to `options.timeout` seconds,
|
|
192
|
+
* defaulting to `ttlSeconds`), then execute `callback` and release.
|
|
193
|
+
* The lock is always released — even when `callback` throws.
|
|
194
|
+
*
|
|
195
|
+
* @param key - Logical lock name.
|
|
196
|
+
* @param ttlSeconds - Lock time-to-live in seconds.
|
|
197
|
+
* @param callback - Critical section to run while the lock is held.
|
|
198
|
+
* @param options - Wait {@link BlockOptions.timeout | timeout} and
|
|
199
|
+
* {@link BlockOptions.retryDelay | poll interval}.
|
|
200
|
+
* @returns The value returned by `callback`.
|
|
201
|
+
* @throws {LockNotAcquiredError} If the lock cannot be acquired before the timeout elapses.
|
|
202
|
+
* @category Acquiring
|
|
203
|
+
*/
|
|
204
|
+
async block<T>(
|
|
205
|
+
key: string,
|
|
206
|
+
ttlSeconds: number,
|
|
207
|
+
callback: () => Promise<T> | T,
|
|
208
|
+
options: BlockOptions = {},
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
const lock = this.lock(key, ttlSeconds);
|
|
211
|
+
await lock.block(options.timeout ?? ttlSeconds, options.retryDelay);
|
|
212
|
+
try {
|
|
213
|
+
return await callback();
|
|
214
|
+
} finally {
|
|
215
|
+
await lock.release();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Release any background resources held by the underlying driver (timers, DB
|
|
221
|
+
* connections). Called by {@link LockProvider} when the application stops.
|
|
222
|
+
*
|
|
223
|
+
* @category Configuration
|
|
224
|
+
*/
|
|
225
|
+
dispose(): void {
|
|
226
|
+
this._driver.dispose?.();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { deepMerge } from "../support/deepMerge.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Full shape of the `lock` config namespace, as produced by {@link LockConfig}.
|
|
5
|
+
* Selects the storage backend and driver-specific options.
|
|
6
|
+
*
|
|
7
|
+
* @category Configuration
|
|
8
|
+
*/
|
|
9
|
+
export interface LockConfigShape {
|
|
10
|
+
/** Storage backend. Default: `'memory'` */
|
|
11
|
+
driver: "memory" | "sqlite" | "redis";
|
|
12
|
+
|
|
13
|
+
/** Key prefix prepended to every lock key. Default: `'zerotal_lock:'` */
|
|
14
|
+
prefix: string;
|
|
15
|
+
|
|
16
|
+
/** SQLite-specific options. */
|
|
17
|
+
sqlite: {
|
|
18
|
+
/** Path to the SQLite file. Use `':memory:'` for in-process storage. */
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaults: LockConfigShape = {
|
|
24
|
+
driver: "memory",
|
|
25
|
+
prefix: "zerotal_lock:",
|
|
26
|
+
sqlite: { path: ":memory:" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a typed {@link LockConfigShape} with defaults applied (memory driver,
|
|
31
|
+
* `zerotal_lock:` prefix, in-memory SQLite path).
|
|
32
|
+
*
|
|
33
|
+
* @param options - Partial overrides deep-merged over the defaults.
|
|
34
|
+
* @returns The resolved lock config.
|
|
35
|
+
* @category Configuration
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // config/lock.ts
|
|
40
|
+
* import { LockConfig } from '@zerotal/core/lock';
|
|
41
|
+
* export default LockConfig({ driver: 'redis' });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function LockConfig(options: Partial<LockConfigShape> = {}): LockConfigShape {
|
|
45
|
+
return deepMerge(defaults, options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The `lock` config namespace is registered directly on core's ConfigRegistry
|
|
49
|
+
// (see src/config/registry.ts) since lock ships as part of core.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract for lock storage backends.
|
|
3
|
+
*
|
|
4
|
+
* Every method must be atomic at the driver level. The `owner` parameter
|
|
5
|
+
* prevents a process from releasing a lock it no longer holds (e.g. if the
|
|
6
|
+
* TTL expired and another process re-acquired).
|
|
7
|
+
*
|
|
8
|
+
* @category Configuration
|
|
9
|
+
*/
|
|
10
|
+
export interface LockDriver {
|
|
11
|
+
/**
|
|
12
|
+
* Try to acquire the lock. Returns `true` when acquired, `false` if already
|
|
13
|
+
* held by another owner and not yet expired.
|
|
14
|
+
*/
|
|
15
|
+
acquire(key: string, owner: string, ttlSeconds: number): Promise<boolean>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Release the lock only if the caller is still the owner.
|
|
19
|
+
* Returns `true` when released, `false` if the lock was held by someone else
|
|
20
|
+
* (e.g. TTL expired and another process re-acquired).
|
|
21
|
+
*/
|
|
22
|
+
release(key: string, owner: string): Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
/** Unconditionally delete the lock regardless of owner. */
|
|
25
|
+
forceRelease(key: string): Promise<void>;
|
|
26
|
+
|
|
27
|
+
/** Returns `true` if the lock is currently held (not expired). */
|
|
28
|
+
exists(key: string): Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
/** Release background resources (timers, DB connections). */
|
|
31
|
+
dispose?(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LockDriver } from "./LockDriver.ts";
|
|
2
|
+
|
|
3
|
+
interface LockRecord {
|
|
4
|
+
owner: string;
|
|
5
|
+
expiresAt: number; // Date.now() ms
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-process lock driver backed by a plain Map.
|
|
10
|
+
*
|
|
11
|
+
* Suitable for single-instance servers and tests. Not safe across
|
|
12
|
+
* processes or server restarts — use the Redis or SQLite driver for
|
|
13
|
+
* distributed / persistent locks.
|
|
14
|
+
*
|
|
15
|
+
* @category Configuration
|
|
16
|
+
*/
|
|
17
|
+
export class MemoryLockDriver implements LockDriver {
|
|
18
|
+
private readonly _store = new Map<string, LockRecord>();
|
|
19
|
+
|
|
20
|
+
async acquire(key: string, owner: string, ttlSeconds: number): Promise<boolean> {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const existing = this._store.get(key);
|
|
23
|
+
|
|
24
|
+
if (existing && now < existing.expiresAt) {
|
|
25
|
+
return existing.owner === owner; // re-entrant for same owner
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this._store.set(key, { owner, expiresAt: now + ttlSeconds * 1000 });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async release(key: string, owner: string): Promise<boolean> {
|
|
33
|
+
const existing = this._store.get(key);
|
|
34
|
+
if (!existing || existing.owner !== owner) return false;
|
|
35
|
+
this._store.delete(key);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async forceRelease(key: string): Promise<void> {
|
|
40
|
+
this._store.delete(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async exists(key: string): Promise<boolean> {
|
|
44
|
+
const existing = this._store.get(key);
|
|
45
|
+
return !!existing && Date.now() < existing.expiresAt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Flush all locks — useful in tests. */
|
|
49
|
+
flush(): void {
|
|
50
|
+
this._store.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { redis } from "bun";
|
|
2
|
+
import type { LockDriver } from "./LockDriver.ts";
|
|
3
|
+
|
|
4
|
+
// Lua script: delete the key only if the caller is still the owner.
|
|
5
|
+
// Evaluated atomically by Redis — no race between GET and DEL.
|
|
6
|
+
const RELEASE_SCRIPT = `
|
|
7
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
8
|
+
return redis.call('del', KEYS[1])
|
|
9
|
+
else
|
|
10
|
+
return 0
|
|
11
|
+
end
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Redis-backed distributed lock driver.
|
|
16
|
+
*
|
|
17
|
+
* Uses `SET key owner NX EX ttl` for atomic acquisition and a Lua script
|
|
18
|
+
* for owner-guarded release. Safe across multiple server instances.
|
|
19
|
+
*
|
|
20
|
+
* Requires Bun's built-in Redis client (available when the REDIS_URL
|
|
21
|
+
* environment variable is set or via the global `redis` export from `bun`).
|
|
22
|
+
*
|
|
23
|
+
* @category Configuration
|
|
24
|
+
*/
|
|
25
|
+
export class RedisLockDriver implements LockDriver {
|
|
26
|
+
constructor(private readonly _prefix: string = "zerotal_lock:") {}
|
|
27
|
+
|
|
28
|
+
private _key(key: string): string {
|
|
29
|
+
return this._prefix + key;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async acquire(key: string, owner: string, ttlSeconds: number): Promise<boolean> {
|
|
33
|
+
const result = await redis.set(this._key(key), owner, "NX", "EX", String(ttlSeconds));
|
|
34
|
+
if (result !== null) return true;
|
|
35
|
+
|
|
36
|
+
// Re-entrant: allow the same owner to refresh its own lock
|
|
37
|
+
const current = await redis.get(this._key(key));
|
|
38
|
+
if (current === owner) {
|
|
39
|
+
await redis.expire(this._key(key), ttlSeconds);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async release(key: string, owner: string): Promise<boolean> {
|
|
47
|
+
const deleted = await redis.send("EVAL", [RELEASE_SCRIPT, "1", this._key(key), owner]);
|
|
48
|
+
return deleted === 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async forceRelease(key: string): Promise<void> {
|
|
52
|
+
await redis.del(this._key(key));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async exists(key: string): Promise<boolean> {
|
|
56
|
+
return redis.exists(this._key(key));
|
|
57
|
+
}
|
|
58
|
+
}
|