@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,335 @@
1
+ /**
2
+ * Turns a compiled route into a Bun request handler: it resolves model
3
+ * bindings, runs the middleware pipeline with the controller action as the
4
+ * final pipe, renders failures through the exception handler, and emits the
5
+ * framework request lifecycle events.
6
+ */
7
+ import { RequestContext } from "../context/RequestContext.ts";
8
+ import { HttpContext } from "../pipeline/HttpContext.ts";
9
+ import type { RequestIPProvider } from "../pipeline/HttpContext.ts";
10
+ import { Pipeline } from "../pipeline/Pipeline.ts";
11
+ import { ExceptionHandler } from "../application/ExceptionHandler.ts";
12
+ import { recordHttp, beginHttp, endHttp } from "../metrics/HttpMetrics.ts";
13
+ import { FrameworkEvents, RequestHandled, RequestFailed } from "../events/FrameworkEvents.ts";
14
+ import { injectRegistry } from "../container/inject.ts";
15
+ import type { Pipe, NextFn } from "../pipeline/types.ts";
16
+ import type { Container } from "../container/Container.ts";
17
+ import type { RouteDefinition, MiddlewareClass } from "./Route.ts";
18
+
19
+ /**
20
+ * Per-request provider hooks dispatched by Application on every HTTP request.
21
+ * Passed to createRouteHandler via Router.compile() so RouteHandler has no
22
+ * hard import dependency on Application (avoids a circular module graph).
23
+ */
24
+ export interface ProviderHooks {
25
+ onRequestReceived(ctx: HttpContext): Promise<void>;
26
+ onRequestProcessed(ctx: HttpContext): Promise<void>;
27
+ /** Register onResponseSent for all providers via ctx.afterResponse(). */
28
+ scheduleResponseSent(ctx: HttpContext): void;
29
+ }
30
+
31
+ /**
32
+ * Run a middleware chain against an HttpContext and report whether the
33
+ * request passed all the way through.
34
+ *
35
+ * "Passed" means every middleware called `next()` — a terminal probe pipe at
36
+ * the end of the chain executed. A middleware that short-circuits (returns
37
+ * without calling next, typically after setting `ctx.response`) yields
38
+ * `passed: false`; inspect `ctx.response` for the redirect/error it produced.
39
+ *
40
+ * Used by framework packages that need to re-run route middleware outside the
41
+ * normal HTTP pipeline (e.g. @zerotal/flow persistent middleware on WebSocket
42
+ * updates).
43
+ *
44
+ * @example
45
+ * const middleware = Router.middlewareFor('GET', '/dashboard');
46
+ * const passed = await runMiddleware(ctx, middleware, container);
47
+ * if (!passed) {
48
+ * // blocked — ctx.response holds the middleware's redirect/401/…
49
+ * }
50
+ */
51
+ export async function runMiddleware(
52
+ ctx: HttpContext,
53
+ middleware: MiddlewareClass[],
54
+ container?: Container,
55
+ ): Promise<boolean> {
56
+ if (middleware.length === 0) return true;
57
+
58
+ let passed = false;
59
+ const TerminalProbe = class implements Pipe<HttpContext> {
60
+ async handle(_context: HttpContext, next: NextFn): Promise<Response | void> {
61
+ passed = true;
62
+ return next();
63
+ }
64
+ };
65
+
66
+ const pipeline = Pipeline.send<HttpContext>(ctx).through([
67
+ ...middleware,
68
+ TerminalProbe as MiddlewareClass,
69
+ ]);
70
+ if (container) pipeline.via(container);
71
+ await pipeline.thenReturn();
72
+
73
+ return passed;
74
+ }
75
+
76
+ /**
77
+ * The parts of the request lifecycle that vary between the matched-route path
78
+ * (`createRouteHandler`) and the unmatched fallback (`Application.start()`'s
79
+ * fetch handler). Everything else — scoping, `HttpContext` creation, timing,
80
+ * the in-flight gauge, framework events, exception rendering, after-response
81
+ * hooks, and metrics — is shared in {@link dispatchRequest}.
82
+ */
83
+ export interface DispatchOptions {
84
+ container: Container;
85
+ exceptionHandler?: ExceptionHandler | undefined;
86
+ providerHooks?: ProviderHooks | undefined;
87
+ /** Configure the freshly created context (params, route info) before dispatch. */
88
+ configure?: (ctx: HttpContext) => void;
89
+ /**
90
+ * Path-specific work: run the pipeline and produce the response (or leave it
91
+ * on `ctx.response`). Errors thrown here are rendered by the exception handler.
92
+ */
93
+ execute: (ctx: HttpContext) => Promise<Response | undefined>;
94
+ /**
95
+ * Render an unhandled error to a Response. Defaults to reporting + rendering
96
+ * through `exceptionHandler` when present, else `ExceptionHandler.defaultRender`.
97
+ */
98
+ renderError?: (error: unknown, ctx: HttpContext) => Promise<Response>;
99
+ }
100
+
101
+ /**
102
+ * Run one HTTP request through the full framework lifecycle:
103
+ *
104
+ * runScoped → HttpContext → RequestContext → in-flight gauge → provider
105
+ * hooks → execute() → exception handling → RequestHandled/RequestFailed →
106
+ * after-response callbacks → metrics.
107
+ *
108
+ * Shared by `createRouteHandler` (matched routes) and `Application.start()`'s
109
+ * fetch fallback (unmatched requests) so instrumentation — notably the
110
+ * `beginHttp`/`endHttp` in-flight gauge — applies uniformly to both.
111
+ */
112
+ export async function dispatchRequest(
113
+ req: Request,
114
+ server: unknown,
115
+ options: DispatchOptions,
116
+ ): Promise<Response> {
117
+ const { container, exceptionHandler, providerHooks } = options;
118
+ return container.runScoped(async (scoped) => {
119
+ const ctx = new HttpContext(req, scoped);
120
+ ctx._server = server as RequestIPProvider | undefined;
121
+ options.configure?.(ctx);
122
+
123
+ return RequestContext.run(ctx, async (): Promise<Response> => {
124
+ const startedAtHighRes = performance.now();
125
+ const startedAtMs = Date.now();
126
+ beginHttp(); // in-flight gauge ++ (paired with endHttp in finally)
127
+ let response: Response;
128
+ let failure: unknown;
129
+
130
+ try {
131
+ if (providerHooks) await providerHooks.onRequestReceived(ctx);
132
+
133
+ const result = await options.execute(ctx);
134
+
135
+ if (providerHooks) await providerHooks.onRequestProcessed(ctx);
136
+
137
+ response = result ?? ctx.response ?? new Response("", { status: 204 });
138
+ } catch (error) {
139
+ failure = error;
140
+ if (options.renderError) {
141
+ response = await options.renderError(error, ctx);
142
+ } else if (exceptionHandler) {
143
+ await exceptionHandler.report(error, ctx);
144
+ response = await exceptionHandler.render(error, ctx);
145
+ } else {
146
+ response = await ExceptionHandler.defaultRender(error, ctx);
147
+ }
148
+ } finally {
149
+ endHttp(); // in-flight gauge -- (always, even if error rendering throws)
150
+ }
151
+
152
+ // Last chance to touch the response. Runs for a rendered error exactly as
153
+ // it does for a success, which is what lets session state set before a
154
+ // throw (flashed validation errors, old input) reach the client on the
155
+ // redirect that was supposed to carry it.
156
+ for (const finalize of ctx._responseFinalizers) {
157
+ try {
158
+ await finalize(response!);
159
+ } catch (error) {
160
+ console.error("[Zerotal] response finalizer failed:", error);
161
+ }
162
+ }
163
+
164
+ // Framework instrumentation: emit on every outcome so subscribers
165
+ // (devtools, logger, telemetry) can finalise the request trace.
166
+ const durationMs = Math.round(performance.now() - startedAtHighRes);
167
+ if (failure !== undefined) {
168
+ FrameworkEvents.emit(
169
+ new RequestFailed(
170
+ ctx,
171
+ startedAtMs,
172
+ durationMs,
173
+ failure instanceof Error ? failure.message : String(failure),
174
+ response!.status,
175
+ ),
176
+ );
177
+ } else {
178
+ FrameworkEvents.emit(new RequestHandled(ctx, startedAtMs, durationMs));
179
+ }
180
+
181
+ // scheduleResponseSent fires for every request — success and error alike.
182
+ // Providers observing onResponseSent see all outcomes via ctx.response.
183
+ if (providerHooks) providerHooks.scheduleResponseSent(ctx);
184
+
185
+ // runScoped's finally block calls scoped.flush() — no manual flush needed.
186
+ if (ctx._afterResponseCallbacks.length > 0) {
187
+ void Promise.allSettled(ctx._afterResponseCallbacks.map((callback) => callback()));
188
+ }
189
+
190
+ try {
191
+ recordHttp(response!.status, performance.now() - startedAtHighRes);
192
+ } catch {
193
+ // Metrics are best-effort and must never break a request.
194
+ }
195
+ return response!;
196
+ });
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Whether a controller class declares constructor dependencies — via
202
+ * `@inject()` metadata or declared constructor parameters. Used to decide whether zero-arg construction is a
203
+ * safe fallback when container resolution fails.
204
+ */
205
+ function _declaresConstructorDeps(ctor: RouteDefinition["controller"]): boolean {
206
+ const injected = injectRegistry.get(ctor);
207
+ if (injected && injected.length > 0) return true;
208
+ return ctor.length > 0;
209
+ }
210
+
211
+ /**
212
+ * Build a Bun-compatible request handler for a single route.
213
+ *
214
+ * The controller action is injected as the LAST pipe in the chain so that
215
+ * middleware finally-blocks (e.g. SessionMiddleware saving the cookie) execute
216
+ * after ctx.response has been set by the controller.
217
+ *
218
+ * Uses container.runScoped() so the scoped resolver is placed in AsyncLocalStorage,
219
+ * allowing container.make() for scoped bindings to work correctly inside matched routes.
220
+ */
221
+ export function createRouteHandler(
222
+ definition: RouteDefinition,
223
+ container: Container,
224
+ globalMiddleware: MiddlewareClass[] = [],
225
+ exceptionHandler?: ExceptionHandler,
226
+ providerHooks?: ProviderHooks,
227
+ ): (req: Request, server?: unknown) => Promise<Response> {
228
+ return (req: Request, server?: unknown): Promise<Response> =>
229
+ dispatchRequest(req, server, {
230
+ container,
231
+ exceptionHandler,
232
+ providerHooks,
233
+ configure(ctx) {
234
+ ctx.params = (req as { params?: Record<string, string> }).params ?? {};
235
+ ctx._routeDef = {
236
+ pattern: definition.path,
237
+ controller: definition.controller.name,
238
+ action: definition.action,
239
+ };
240
+ },
241
+ async execute(ctx) {
242
+ // Resolve the controller before building the pipe chain so failures
243
+ // are caught by dispatchRequest and return a proper error response.
244
+ // Zero-arg construction is only a fallback for dependency-FREE
245
+ // controllers; a controller that declares dependencies must fail
246
+ // loudly here rather than run with `undefined` injections that
247
+ // explode far from the cause.
248
+ let controller: Record<string, unknown>;
249
+ try {
250
+ controller = (await container.make(definition.controller)) as Record<string, unknown>;
251
+ } catch (error) {
252
+ if (_declaresConstructorDeps(definition.controller)) {
253
+ throw new Error(
254
+ `Failed to resolve controller ${definition.controller.name} from the container: ` +
255
+ `${error instanceof Error ? error.message : String(error)}`,
256
+ { cause: error },
257
+ );
258
+ }
259
+ controller = new definition.controller() as Record<string, unknown>;
260
+ }
261
+
262
+ const action = controller[definition.action];
263
+ if (typeof action !== "function") {
264
+ throw new Error(
265
+ `Controller action "${definition.action}" not found on ${definition.controller.name}.`,
266
+ );
267
+ }
268
+
269
+ // Build a single-use Pipe that invokes the controller action.
270
+ // Placing it LAST in the chain means middleware finally-blocks run
271
+ // after ctx.response is set — required for session cookie persistence.
272
+ const boundAction = (
273
+ action as (ctx: HttpContext) => Promise<Response | void> | Response | void
274
+ ).bind(controller);
275
+
276
+ const ControllerPipe = class implements Pipe<HttpContext> {
277
+ // Innermost pipe — runs the controller action and produces the response.
278
+ // It never calls next(); the pipeline terminal reads ctx.response.
279
+ // Route params and resolved model bindings are already on ctx.params.
280
+ async handle(context: HttpContext, _next: NextFn): Promise<Response | void> {
281
+ const result = await boundAction(context);
282
+ if (result instanceof Response) {
283
+ context.response = result;
284
+ }
285
+ return context.response;
286
+ }
287
+ };
288
+
289
+ // ── Route-model binding ────────────────────────────────────────────
290
+ // Second-innermost pipe: every middleware has run, the controller has
291
+ // not.
292
+ //
293
+ // This used to resolve before the pipeline, which put a database read
294
+ // and a 404 ahead of authentication: a protected route answered 404 for
295
+ // a missing id but 401 for one that existed, so anyone could enumerate
296
+ // which records exist without logging in. Middleware now decides
297
+ // whether the request may proceed at all before a binding is resolved.
298
+ //
299
+ // A throw here still renders correctly — `execute()` is wrapped by
300
+ // dispatchRequest's exception handler — and it now unwinds *through*
301
+ // the middleware, so their finally-blocks (e.g. SessionMiddleware
302
+ // persisting the cookie) run on a binding 404 as they do on any other
303
+ // failure.
304
+ const ModelBindingPipe = class implements Pipe<HttpContext> {
305
+ async handle(context: HttpContext, next: NextFn): Promise<Response | void> {
306
+ const params = context.params as Record<string, unknown>;
307
+ for (const [paramName, resolver] of definition.bindings) {
308
+ const paramValue = params[paramName] as string | undefined;
309
+ if (paramValue === undefined) continue;
310
+ const instance = await resolver(paramValue, context);
311
+ context._models.set(paramName, instance);
312
+ // Fold the resolved model onto params under its param name so handlers
313
+ // read it via `ctx.params.post` (raw string params stay as-is).
314
+ params[paramName] = instance;
315
+ }
316
+ return next();
317
+ }
318
+ };
319
+
320
+ const allPipes = [
321
+ ...globalMiddleware,
322
+ ...definition.middleware,
323
+ ...(definition.bindings.size > 0 ? [ModelBindingPipe as MiddlewareClass] : []),
324
+ ControllerPipe as MiddlewareClass,
325
+ ];
326
+
327
+ const finalContext = await Pipeline.send<HttpContext>(ctx)
328
+ .through(allPipes)
329
+ .via(container)
330
+ .thenReturn();
331
+
332
+ return finalContext.response ?? undefined;
333
+ },
334
+ });
335
+ }