@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A generic middleware pipeline: sends a payload through an ordered list of
3
+ * pipes and hands the final payload to a destination. Pipes are resolved from
4
+ * the container when one is supplied, otherwise instantiated directly.
5
+ */
6
+ import type { Container } from "../container/Container.ts";
7
+ import { injectRegistry } from "../container/inject.ts";
8
+ import type { Pipe, NextFn } from "./types.ts";
9
+
10
+ type PipeClass<T> = new (...args: unknown[]) => Pipe<T>;
11
+
12
+ /**
13
+ * Generic middleware pipeline: sends a payload through an ordered list of pipe
14
+ * classes and hands the final payload to a destination.
15
+ *
16
+ * Pipes are resolved from the container when `.via()` is called; otherwise each
17
+ * is instantiated directly with `new PipeClass()`.
18
+ *
19
+ * @typeParam T - The payload type carried through the pipes (typically {@link HttpContext}).
20
+ *
21
+ * @example
22
+ * const result = await Pipeline.send(ctx)
23
+ * .through([AuthMiddleware, ThrottleMiddleware])
24
+ * .via(container) // optional — resolves pipes from container
25
+ * .then((ctx) => ctx.response);
26
+ *
27
+ * @example
28
+ * // Run the chain and read the mutated payload back:
29
+ * const finished = await Pipeline.send(ctx).through(pipes).via(container).thenReturn();
30
+ * return finished.response;
31
+ */
32
+ export class Pipeline<T> {
33
+ private _payload!: T;
34
+ private _pipes: PipeClass<T>[] = [];
35
+ private _container: Container | undefined = undefined;
36
+
37
+ private constructor() {}
38
+
39
+ // ── Fluent builder ────────────────────────────────────────────────────
40
+
41
+ /** Begin a pipeline carrying `payload` through the pipes added by `through()`. */
42
+ static send<T>(payload: T): Pipeline<T> {
43
+ const pipeline = new Pipeline<T>();
44
+ pipeline._payload = payload;
45
+ return pipeline;
46
+ }
47
+
48
+ /** Set the ordered list of pipe classes the payload travels through. */
49
+ through(pipes: PipeClass<T>[]): this {
50
+ this._pipes = pipes;
51
+ return this;
52
+ }
53
+
54
+ /** Inject a container so pipes are resolved from it (supports `@inject`). */
55
+ via(container: Container): this {
56
+ this._container = container;
57
+ return this;
58
+ }
59
+
60
+ // ── Execution ─────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Execute the pipeline and pass the final payload to `destination`.
64
+ * Returns whatever `destination` returns.
65
+ */
66
+ async then<R>(destination: (payload: T) => Promise<R> | R): Promise<R> {
67
+ await this._run();
68
+ return destination(this._payload);
69
+ }
70
+
71
+ /**
72
+ * Execute the pipeline and return the final payload unchanged.
73
+ *
74
+ * The pipeline mutates the payload in-place: every pipe shares the one
75
+ * `payload` (the request `HttpContext`), and the canonical `Response` is
76
+ * written to `payload.response`. Read it back from there.
77
+ */
78
+ async thenReturn(): Promise<T> {
79
+ await this._run();
80
+ return this._payload;
81
+ }
82
+
83
+ // ── Internal recursive runner ─────────────────────────────────────────
84
+
85
+ /**
86
+ * Run the pipes as a recursive onion. Each pipe either calls `next()` to
87
+ * descend, short-circuits, or wraps the downstream response. A pipe that
88
+ * returns a `Response` has it mirrored onto `payload.response`; a `void`
89
+ * return leaves that canonical slot untouched. `next()` resolves to whatever
90
+ * `Response` (or `undefined`) the deeper pipes left there.
91
+ */
92
+ private async _run(): Promise<Response | undefined> {
93
+ const pipes = this._pipes;
94
+ // The canonical response store. All real pipelines carry the request
95
+ // HttpContext; duck-typed so Pipeline stays payload-agnostic.
96
+ const http = this._payload as { response?: Response } | undefined;
97
+
98
+ const execute = async (index: number): Promise<Response | undefined> => {
99
+ if (index === pipes.length) {
100
+ // End of chain — surface whatever response was set on the way down.
101
+ return http?.response;
102
+ }
103
+
104
+ const PipeClass = pipes[index]!;
105
+
106
+ // Resolve from the container when it can actually satisfy this class (supports @inject
107
+ // singleton middleware); otherwise instantiate directly. The previous
108
+ // implementation used `try { makeSync() } catch { new PipeClass() }`, but middleware is
109
+ // never registered in the container, so `makeSync` threw BindingNotFoundError for *every*
110
+ // pipe on *every* request. `Error` captures a stack trace, which made exception
111
+ // construction — not the middleware itself — a measurable share of per-request CPU
112
+ // (~3.2 us per pipe). `_isContainerResolvable` is a couple of Map lookups.
113
+ let pipeInstance: Pipe<T>;
114
+ if (this._container && _isContainerResolvable(this._container, PipeClass)) {
115
+ pipeInstance = this._container.makeSync(PipeClass as never) as Pipe<T>;
116
+ } else {
117
+ pipeInstance = new PipeClass();
118
+ }
119
+
120
+ const next: NextFn = () => execute(index + 1);
121
+
122
+ const result = await pipeInstance.handle(this._payload, next);
123
+ // Returning a Response is sugar for "set ctx.response"; a void return
124
+ // leaves the canonical slot as the pipe (or a deeper one) left it.
125
+ if (result instanceof Response && http) http.response = result;
126
+ return http?.response;
127
+ };
128
+
129
+ return execute(0);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * True when `container.makeSync(PipeClass)` would succeed — i.e. the class is registered, is
135
+ * deferred behind a provider, or is auto-wirable via `@inject()`.
136
+ *
137
+ * This mirrors the resolution branches in {@link Container.makeSync} so the pipeline can pick
138
+ * the right path without provoking (and swallowing) a `BindingNotFoundError` per pipe per
139
+ * request. Kept deliberately conservative: anything it is unsure about falls through to
140
+ * `new PipeClass()`, which is what the old catch-all did anyway.
141
+ */
142
+ function _isContainerResolvable<T>(
143
+ container: Container,
144
+ PipeClass: new (...args: unknown[]) => Pipe<T>,
145
+ ): boolean {
146
+ if (container.bound(PipeClass)) return true;
147
+ // Auto-wiring: @inject() records the class in injectRegistry, making it
148
+ // resolvable without an explicit binding.
149
+ return injectRegistry.has(PipeClass);
150
+ }
@@ -0,0 +1,46 @@
1
+ import { HttpContext } from "./HttpContext.ts";
2
+
3
+ /**
4
+ * Resolves the page a paginator should return when the caller doesn't name one.
5
+ *
6
+ * @param pageName - The paginator's name, so one request can drive several independently.
7
+ * @returns The 1-based page, or `undefined` to fall back to the query string.
8
+ */
9
+ export type CurrentPageResolver = (pageName: string) => number | undefined;
10
+
11
+ /**
12
+ * Override where `paginate()` reads the current page for the rest of this request.
13
+ *
14
+ * The default reads the query string, which is what a plain HTTP request wants. A
15
+ * server-driven view whose page number lives in component state rather than the URL
16
+ * registers its own instead — Flow's `Pagination` mixin does exactly this, so
17
+ * `Post.paginate(10)` inside a component follows the component's page.
18
+ *
19
+ * Scoped to the request: the resolver goes on the active {@link HttpContext}, reached through
20
+ * request-scoped storage, so it lasts exactly as long as the request does and never reaches
21
+ * another one. Keep it that way — a module-level slot would not be request-scoped.
22
+ *
23
+ * @param resolver - Called with the paginator name; return `undefined` to defer to the query string.
24
+ */
25
+ export function setCurrentPageResolver(resolver: CurrentPageResolver): void {
26
+ const ctx = HttpContext.tryGet();
27
+ if (ctx) ctx._pageResolver = resolver;
28
+ }
29
+
30
+ /**
31
+ * The page a paginator should return, for the request in flight.
32
+ *
33
+ * Order: a resolver registered for this request, then the query string, then `1`. Outside a
34
+ * request — a queue worker, a CLI command, a test — there is nothing to read, so it is `1`.
35
+ *
36
+ * @param pageName - The paginator's name. Defaults to `"page"`.
37
+ * @returns A 1-based page number, never below 1.
38
+ */
39
+ export function currentPage(pageName = "page"): number {
40
+ const ctx = HttpContext.tryGet();
41
+ if (!ctx) return 1;
42
+
43
+ const resolved = ctx._pageResolver?.(pageName);
44
+ const page = resolved ?? ctx.integer(pageName, 1);
45
+ return Math.max(1, Math.trunc(page ?? 1));
46
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared types for the middleware pipeline: the `next` callback, the controller
3
+ * handler return type, the `Pipe` contract, and the response-carrying payload.
4
+ */
5
+
6
+ /**
7
+ * Passes control to the rest of the pipeline and resolves with the `Response`
8
+ * produced downstream (or `undefined` when nothing deeper has set one yet —
9
+ * the controller may not have run, or a deeper pipe short-circuited with void).
10
+ *
11
+ * Always guard the result before touching it:
12
+ *
13
+ * const res = await next();
14
+ * if (res) res.headers.get('…'); // res is Response | undefined
15
+ */
16
+ export type NextFn = () => Promise<Response | void>;
17
+
18
+ /**
19
+ * Return type for controller handler methods.
20
+ *
21
+ * Equivalent to `Promise<void>` but communicates intent — a handler either
22
+ * returns nothing (side-effect already set on ctx) or a ResponseBuilder from
23
+ * a redirect helper (redirect(), redirect().back(), etc.). Because ResponseBuilder
24
+ * implements PromiseLike<void>, TypeScript unwraps it to void automatically,
25
+ * so all IFRS helpers work without changing this type.
26
+ *
27
+ * @example
28
+ * async store(ctx: HttpContext): HttpResponse {
29
+ * const data = await StorePostRequest.validate();
30
+ * const post = await Post.create(data);
31
+ * return redirect(`/posts/${post.id}`, 303).withSuccess('Post created.');
32
+ * }
33
+ *
34
+ * async showLogin(ctx: HttpContext): HttpResponse {
35
+ * return inertia('Auth/Login');
36
+ * }
37
+ */
38
+ export type HttpResponse = Promise<void>;
39
+
40
+ /**
41
+ * A single step in a middleware pipeline.
42
+ *
43
+ * ## Contract
44
+ * A pipe does exactly one of three things:
45
+ * - **Continue:** `return next()` to run the rest of the chain.
46
+ * - **Short-circuit:** produce a `Response` and stop. Either `return` it
47
+ * directly, or set `ctx.response` and `return` (void). Do NOT call `next()`.
48
+ * - **Wrap:** `const res = await next()`, inspect/transform `res`, then return
49
+ * the (possibly new) `Response` — or just mutate `ctx.response` and return void.
50
+ *
51
+ * `ctx.response` is the canonical store: returning a `Response` is sugar the
52
+ * pipeline mirrors onto it, and a `void` return leaves it untouched (so it can
53
+ * never erase a response a deeper pipe already set).
54
+ *
55
+ * @example
56
+ * class RequireAuth implements Pipe<HttpContext> {
57
+ * async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
58
+ * if (!ctx.user) {
59
+ * return Response.json({ message: 'Unauthenticated.' }, { status: 401 });
60
+ * }
61
+ * return next();
62
+ * }
63
+ * }
64
+ */
65
+ export interface Pipe<T> {
66
+ handle(payload: T, next: NextFn): Promise<Response | void>;
67
+ }
68
+
69
+ /**
70
+ * HttpContext carries the Response when a controller sets it.
71
+ * Pipes that produce a response set ctx.response and return ctx.
72
+ * The pipeline terminal in Application.ts reads ctx.response.
73
+ */
74
+ /**
75
+ * A payload that can carry a `Response`. Pipes set `response` and return the
76
+ * payload to short-circuit; the pipeline terminal reads it back.
77
+ */
78
+ export interface HasResponse {
79
+ response: Response | undefined;
80
+ }
@@ -0,0 +1,64 @@
1
+ import { ServiceProvider } from "./ServiceProvider.ts";
2
+ import type { AppEnvironment } from "./ServiceProvider.ts";
3
+ import type { ConfigManager } from "../config/ConfigManager.ts";
4
+ import { LockManager } from "../lock/LockManager.ts";
5
+ import { LockConfig } from "../lock/config.ts";
6
+ import type { LockConfigShape } from "../lock/config.ts";
7
+ import { MemoryLockDriver } from "../lock/drivers/MemoryLockDriver.ts";
8
+ import { SqliteLockDriver } from "../lock/drivers/SqliteLockDriver.ts";
9
+ import { RedisLockDriver } from "../lock/drivers/RedisLockDriver.ts";
10
+ import type { LockDriver } from "../lock/drivers/LockDriver.ts";
11
+
12
+ // The `lock` binding is registered directly on core's ContainerBindings
13
+ // (see src/container/types.ts) since lock ships as part of core.
14
+
15
+ /**
16
+ * Service provider that registers the `lock` binding.
17
+ *
18
+ * Reads the `lock` config, instantiates the matching {@link LockDriver}
19
+ * ({@link MemoryLockDriver}, {@link SqliteLockDriver}, or
20
+ * {@link RedisLockDriver}), and binds a {@link LockManager} singleton. Pre-
21
+ * resolves it on boot so the synchronous {@link Lock} facade works afterward,
22
+ * and disposes the manager (closing driver resources) on stop.
23
+ *
24
+ * @category Configuration
25
+ */
26
+ export class LockProvider extends ServiceProvider {
27
+ static override provides = ["lock"] as const;
28
+ static override environments: AppEnvironment[] = ["web", "worker", "console", "test"];
29
+
30
+ override onRegister(): void {
31
+ this.app.container.singleton("lock", () => {
32
+ const configManager = this.app.container.tryMake("config") as ConfigManager | null;
33
+ const raw = configManager?.get<Partial<LockConfigShape>>("lock") ?? {};
34
+ const cfg = LockConfig(raw);
35
+
36
+ let driver: LockDriver;
37
+
38
+ switch (cfg.driver) {
39
+ case "redis":
40
+ driver = new RedisLockDriver(cfg.prefix);
41
+ break;
42
+ case "sqlite":
43
+ driver = new SqliteLockDriver(cfg.sqlite.path);
44
+ break;
45
+ case "memory":
46
+ default:
47
+ driver = new MemoryLockDriver();
48
+ break;
49
+ }
50
+
51
+ return new LockManager(driver);
52
+ });
53
+ }
54
+
55
+ override async onBooted(): Promise<void> {
56
+ // Pre-resolve so the Lock facade (synchronous callers) works after boot.
57
+ await this.app.container.make("lock");
58
+ }
59
+
60
+ override async onStopping(): Promise<void> {
61
+ const manager = this.app.container.tryMake("lock") as LockManager | null;
62
+ manager?.dispose();
63
+ }
64
+ }
@@ -0,0 +1,137 @@
1
+ import { ServiceProvider } from "./ServiceProvider.ts";
2
+ import type { AppEnvironment } from "./ServiceProvider.ts";
3
+ import { FrameworkEvents } from "../events/FrameworkEvents.ts";
4
+ import type {
5
+ // Application lifecycle
6
+ AppBooted,
7
+ // HTTP
8
+ RequestHandled,
9
+ RequestFailed,
10
+ } from "../events/FrameworkEvents.ts";
11
+
12
+ import type { LoggingConfigShape } from "../logger/types.ts";
13
+ import { LoggingConfig } from "../logger/config.ts";
14
+ import { LogManager } from "../logger/LogManager.ts";
15
+ import { LoggerMiddleware } from "../logger/LoggerMiddleware.ts";
16
+
17
+ /**
18
+ * What an app without a `config/logging.ts` gets. Deliberately the same as
19
+ * {@link LoggingConfig}'s own defaults rather than a second, thinner set — an
20
+ * app that never publishes the config file still gets the file trail, which is
21
+ * the whole point of the trail being on by default.
22
+ */
23
+ function _defaultConfig(): LoggingConfigShape {
24
+ return LoggingConfig();
25
+ }
26
+
27
+ /** Casts the opaque ctx object to extract HTTP-readable fields. */
28
+ function _ctx(raw: object) {
29
+ return raw as {
30
+ response?: { status?: number };
31
+ request?: { method?: string };
32
+ url?: { pathname?: string };
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Service provider that wires logging into the application.
38
+ *
39
+ * Registers the `log` binding (a {@link LogManager} built from the `logging`
40
+ * config, enriched with the app name and environment), installs the
41
+ * {@link LoggerMiddleware} for per-request access logs (unless
42
+ * `logging.requests` is `false`), and subscribes to the core lifecycle events
43
+ * (`AppBooted`, and 4xx/5xx `RequestHandled` / `RequestFailed`) to route them
44
+ * through the logger. Subscriptions are torn down on stop.
45
+ *
46
+ * Feature packages log their own activity (slow queries, job/mail/scheduler
47
+ * failures, the auth audit trail) through their own bridge, which resolves the
48
+ * `log` binding when logging is installed — so the logger knows nothing about them.
49
+ *
50
+ * @category Logging
51
+ */
52
+ export class LogProvider extends ServiceProvider {
53
+ static override provides = ["log"] as const;
54
+ static override environments: AppEnvironment[] = ["web", "console", "worker", "test", "repl"];
55
+
56
+ private _unsubs: Array<() => void> = [];
57
+
58
+ override onRegister(): void {
59
+ this.app.container.singleton("log", async (c) => {
60
+ const cfg = (await c.make("config")) as { get<T>(path: string): T | undefined };
61
+ const logging = cfg.get<LoggingConfigShape>("logging") ?? _defaultConfig();
62
+ const appName = cfg.get<string>("app.name");
63
+ const appEnv = cfg.get<string>("app.env") ?? Bun.env["APP_ENV"];
64
+
65
+ return new LogManager(logging, { app: appName, env: appEnv });
66
+ });
67
+ }
68
+
69
+ override async onBooting(): Promise<void> {
70
+ const log = (await this.app.container.make("log")) as LogManager;
71
+ LoggerMiddleware.setManager(log);
72
+
73
+ // Per-request access logging is on by default; `logging.requests: false` opts out.
74
+ const cfg = (await this.app.container.make("config")) as {
75
+ get<T>(path: string): T | undefined;
76
+ };
77
+ const requests = cfg.get<LoggingConfigShape>("logging")?.requests ?? true;
78
+ if (requests) this.app.useOnce(LoggerMiddleware as never);
79
+ }
80
+
81
+ override async onBooted(): Promise<void> {
82
+ const manager = (await this.app.container.make("log")) as LogManager;
83
+ // Framework events carry a scope so a boot log reads as columns rather than
84
+ // a wall of prose: [APP] for lifecycle, [HTTP] for the request pipeline.
85
+ const appLog = manager.scope("app");
86
+ const log = manager.scope("http");
87
+
88
+ this._unsubs.push(
89
+ // ── Application lifecycle ──────────────────────────────────────────────────
90
+
91
+ FrameworkEvents.on<AppBooted>("AppBooted", (e) => {
92
+ appLog.info("Application booted", {
93
+ durationMs: Math.round(e.durationMs),
94
+ environment: e.environment,
95
+ providers: e.providerCount,
96
+ });
97
+ }),
98
+
99
+ // ── HTTP ─────────────────────────────────────────────────────────────────
100
+
101
+ FrameworkEvents.on<RequestHandled>("RequestHandled", (e) => {
102
+ const c = _ctx(e.ctx);
103
+ const path = c.url?.pathname ?? "?";
104
+ if (
105
+ path.startsWith("/__zerotal/") ||
106
+ path.startsWith("/__flow/") ||
107
+ path.startsWith("/__dev/")
108
+ )
109
+ return;
110
+ const status = c.response?.status ?? 0;
111
+ const ctx = { method: c.request?.method ?? "?", path, status, durationMs: e.durationMs };
112
+ if (status >= 500) log.error("Request error", ctx);
113
+ else if (status >= 400) log.warn("Request warning", ctx);
114
+ }),
115
+
116
+ FrameworkEvents.on<RequestFailed>("RequestFailed", (e) => {
117
+ const c = _ctx(e.ctx);
118
+ log.error(
119
+ "Request pipeline error",
120
+ {
121
+ method: c.request?.method ?? "?",
122
+ path: c.url?.pathname ?? "?",
123
+ status: e.status,
124
+ durationMs: e.durationMs,
125
+ },
126
+ new Error(e.error),
127
+ );
128
+ }),
129
+ );
130
+ }
131
+
132
+ override onStopping(): Promise<void> {
133
+ for (const unsub of this._unsubs) unsub();
134
+ this._unsubs = [];
135
+ return Promise.resolve();
136
+ }
137
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The base class application authors extend to register bindings and hook into
3
+ * the application lifecycle (boot, start, stop, and per-request phases).
4
+ */
5
+ import type { Application } from "../application/Application.ts";
6
+ import type { ContainerBindings } from "../container/types.ts";
7
+ import type { HttpContext } from "../pipeline/HttpContext.ts";
8
+
9
+ /** The runtime modes a provider can declare it participates in. */
10
+ export type AppEnvironment = "web" | "console" | "worker" | "test" | "repl";
11
+
12
+ /** Base class for service providers: register bindings and observe the app lifecycle. */
13
+ export abstract class ServiceProvider {
14
+ /** Environments in which this provider is active. Defaults to all of them. */
15
+ static environments: AppEnvironment[] = ["web", "console", "worker", "test", "repl"];
16
+
17
+ /**
18
+ * Container tokens this provider registers.
19
+ * Required when using the array form of app.defer([Provider, ...]).
20
+ *
21
+ * @example
22
+ * static override provides = ['cache'] as const;
23
+ */
24
+ static provides?: readonly (keyof ContainerBindings)[];
25
+
26
+ /**
27
+ * Other providers this one needs. They are pulled into the application
28
+ * automatically (transitively, de-duplicated) and guaranteed to boot **before**
29
+ * this provider — so you only have to register the feature you want, not its
30
+ * plumbing. Reference the provider classes directly:
31
+ *
32
+ * @example
33
+ * static override dependsOn = [FlowProvider];
34
+ */
35
+ static dependsOn?: Array<new (app: Application) => ServiceProvider>;
36
+
37
+ /**
38
+ * Boot-order tiebreak among providers with no `dependsOn` relationship.
39
+ * Lower boots earlier; defaults to `0`. Handy for framework-core providers
40
+ * (e.g. `static override priority = -100`).
41
+ */
42
+ static priority?: number;
43
+
44
+ constructor(protected app: Application) {}
45
+
46
+ /** Register container bindings. Runs before any provider boots; do not resolve services here. */
47
+ onRegister(): void {}
48
+ /** Resolve and prepare services before the app is considered booted. */
49
+ async onBooting(): Promise<void> {}
50
+ /** Run once all providers have booted and every binding is available. */
51
+ async onBooted(): Promise<void> {}
52
+ /** Run just before the app starts accepting work (e.g. the HTTP server). */
53
+ async onStarting(): Promise<void> {}
54
+ /** Run once the app has started. */
55
+ async onStarted(): Promise<void> {}
56
+ /** Run when the app begins a graceful shutdown; release resources here. */
57
+ async onStopping(): Promise<void> {}
58
+ /** Run once shutdown is complete. */
59
+ async onStopped(): Promise<void> {}
60
+
61
+ // ── Per-request hooks ─────────────────────────────────────────────────
62
+ // Called by the framework on every HTTP request. Override to observe or
63
+ // mutate the lifecycle without registering middleware.
64
+
65
+ /** Called before the middleware pipeline runs. */
66
+ async onRequestReceived(_ctx: HttpContext): Promise<void> {}
67
+
68
+ /** Called after the pipeline completes and ctx.response is set. */
69
+ async onRequestProcessed(_ctx: HttpContext): Promise<void> {}
70
+
71
+ /** Called after the response has been sent to the client. */
72
+ async onResponseSent(_ctx: HttpContext): Promise<void> {}
73
+
74
+ /**
75
+ * Variables to expose in `bun zt repl`.
76
+ * Override in any provider to make its facades available by name in the REPL session.
77
+ *
78
+ * @example
79
+ * override replContext() { return { DB, Mail }; }
80
+ */
81
+ replContext(): Record<string, unknown> {
82
+ return {};
83
+ }
84
+ }
@@ -0,0 +1,45 @@
1
+ import { ServiceProvider } from "./ServiceProvider.ts";
2
+ import type { AppEnvironment } from "./ServiceProvider.ts";
3
+ import type { ConfigManager } from "../config/ConfigManager.ts";
4
+ import { StorageManager } from "../storage/StorageManager.ts";
5
+ import { StorageConfig } from "../storage/config.ts";
6
+ import { StorageFilesMiddleware, mountsFrom } from "../storage/StorageFilesMiddleware.ts";
7
+
8
+ declare module "@zerotal/core" {
9
+ interface ContainerBindings {
10
+ storage: StorageManager;
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Registers the `storage` binding and, for any disk that declares `serve`, the
16
+ * middleware that exposes it over HTTP.
17
+ *
18
+ * A disk without a `serve` block has no URL at all — the default `local` disk
19
+ * is unreachable and only `public` is served. Exposure is opt-in per disk
20
+ * rather than something you have to remember to switch off.
21
+ */
22
+ export class StorageProvider extends ServiceProvider {
23
+ static override provides = ["storage"] as const;
24
+ static override environments: AppEnvironment[] = ["web", "console", "worker", "test"];
25
+
26
+ override onRegister(): void {
27
+ this.app.container.singleton("storage", () => {
28
+ const config = this.app.container.makeSync("config") as ConfigManager;
29
+ const storageCfg = config.get<ReturnType<typeof StorageConfig>>("storage", StorageConfig());
30
+ return new StorageManager(storageCfg);
31
+ });
32
+ }
33
+
34
+ override async onBooting(): Promise<void> {
35
+ const storage = await this.app.container.make("storage");
36
+
37
+ const config = this.app.container.makeSync("config") as ConfigManager;
38
+ const storageCfg = config.get<ReturnType<typeof StorageConfig>>("storage", StorageConfig());
39
+ const mounts = mountsFrom(storageCfg);
40
+ // No servable disk means no middleware — nothing to match, nothing to get wrong.
41
+ if (mounts.length === 0) return;
42
+
43
+ this.app.useOnce(StorageFilesMiddleware.with({ mounts, storage }));
44
+ }
45
+ }