@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,509 @@
1
+ ---
2
+ title: Middleware
3
+ description: Inspect, transform, or short-circuit HTTP requests in a layered pipeline between the server and your controller.
4
+ ---
5
+
6
+ # Middleware
7
+
8
+ Middleware sits in the HTTP pipeline between the server and your controller.
9
+ Each piece of middleware receives the request context, can inspect or modify it,
10
+ then either passes control to the next layer or short-circuits with a response.
11
+
12
+ Middleware ships in `@zerotal/core`, so there is nothing to install or register
13
+ — import the types and write a class.
14
+
15
+ ## Getting Started
16
+
17
+ The middleware pipeline is built into `@zerotal/core` — nothing to install:
18
+
19
+ ```typescript
20
+ import type { HttpContext, NextFn } from "zerotal";
21
+ ```
22
+
23
+ ## Writing middleware
24
+
25
+ Implement the `Pipe<HttpContext>` interface — a single `handle` method that
26
+ receives the request `HttpContext` directly and reaches the request/response
27
+ helpers on it. A middleware does exactly one of three things: **continue** by
28
+ returning `next()`, **short-circuit** by returning a `Response`, or **wrap** by
29
+ awaiting `next()` (which resolves to the downstream `Response`) and returning a
30
+ transformed one. `next()` takes no arguments:
31
+
32
+ ```ts
33
+ // app/middleware/LogRequestMiddleware.ts
34
+ import type { Pipe, NextFn, HttpContext } from "zerotal";
35
+
36
+ export class LogRequestMiddleware implements Pipe<HttpContext> {
37
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
38
+ console.log(`→ ${ctx.request.method} ${ctx.path()}`);
39
+
40
+ const response = await next(); // ← inner middleware + controller run here
41
+
42
+ console.log(`← ${response?.status} (${ctx.took}ms)`);
43
+ return response;
44
+ }
45
+ }
46
+ ```
47
+
48
+ The `Pipe` and `NextFn` types come straight from the package:
49
+
50
+ ```ts
51
+ // the signatures, for reference
52
+ interface Pipe<T> {
53
+ handle(payload: T, next: NextFn): Promise<Response | void>;
54
+ }
55
+
56
+ type NextFn = () => Promise<Response | void>;
57
+ ```
58
+
59
+ ### Short-circuiting
60
+
61
+ Return a `Response` without calling `next` to stop the pipeline:
62
+
63
+ ```ts
64
+ // app/middleware/MaintenanceMiddleware.ts
65
+ import type { Pipe, NextFn, HttpContext } from "zerotal";
66
+
67
+ export class MaintenanceMiddleware implements Pipe<HttpContext> {
68
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
69
+ if (Bun.env.MAINTENANCE_MODE === "true") {
70
+ return new Response("Down for maintenance", { status: 503 }); // ← does NOT call next
71
+ }
72
+ return next();
73
+ }
74
+ }
75
+ ```
76
+
77
+ > **Note** — You can also set `ctx.response` and `return` (void) instead of returning the `Response`
78
+ > directly — both are equivalent. `ctx.response` is the canonical store; a `void` return
79
+ > leaves whatever it holds untouched, so it can never erase a response a deeper pipe set.
80
+
81
+ ### Wrapping
82
+
83
+ Code after `await next()` runs on the way out — after the controller has responded. `next()`
84
+ resolves to the downstream `Response` (or `undefined` if none was produced). Use this for
85
+ saving session data or appending headers. Because some responses (e.g. `Response.redirect()`)
86
+ have immutable headers, reconstruct rather than mutate — the `withHeaders` helper does this:
87
+
88
+ ```ts
89
+ // app/middleware/TimingHeaderMiddleware.ts
90
+ import type { Pipe, NextFn, HttpContext } from "zerotal";
91
+ import { withHeaders } from "zerotal";
92
+
93
+ export class TimingHeaderMiddleware implements Pipe<HttpContext> {
94
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
95
+ const response = await next();
96
+ if (!response) return;
97
+ return withHeaders(response, { "Server-Timing": `total;dur=${ctx.took}` });
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Attaching middleware to routes
103
+
104
+ There are five ways to attach middleware. Reach for the one that matches the scope you want:
105
+
106
+ - **Per-route** — one specific route needs the middleware.
107
+ - **Route groups** — a handful of related routes share it.
108
+ - **Named groups** — the same stack (`web`, `api`) is reused across many groups.
109
+ - **File-based** (`_middleware.ts`) — every route file under a directory inherits it.
110
+ - **Auto-discovered** — a class in `app/middleware/` referenced by its name as a string.
111
+
112
+ For middleware that must run on _every_ request, see [Global middleware](#global-middleware).
113
+
114
+ ### Per-route
115
+
116
+ Pass an array of middleware classes as the fourth argument to any route
117
+ registration method:
118
+
119
+ ```ts
120
+ // routes/index.ts
121
+ Router.get("/dashboard", DashboardController, "index", [AuthMiddleware]);
122
+ Router.post("/posts", PostController, "store", [AuthMiddleware, ThrottleMiddleware]);
123
+ ```
124
+
125
+ ### Route groups
126
+
127
+ ```ts
128
+ // routes/index.ts
129
+ Router.group({ middleware: AuthMiddleware }, () => {
130
+ Router.get("/dashboard", DashboardController, "index");
131
+ Router.resource("posts", PostController);
132
+ });
133
+ ```
134
+
135
+ ### Auto-discovered middleware
136
+
137
+ Middleware classes under `app/middleware/` are auto-registered at boot as a **named group under
138
+ their class name** — reference them by string in routes without importing:
139
+
140
+ ```ts
141
+ // app/middleware/EnsureSubscribed.ts → referenceable as "EnsureSubscribed"
142
+ Router.group({ middleware: ["EnsureSubscribed"] }, () => {
143
+ /* … */
144
+ });
145
+ ```
146
+
147
+ They are **not** global by default; set `static global = true` on the class to add it to the
148
+ global pipeline. See [Conventions](/docs/conventions#middleware-appmiddleware).
149
+
150
+ ### Named middleware groups
151
+
152
+ Define a group once, reference it by name everywhere:
153
+
154
+ ```ts
155
+ // in a ServiceProvider.onRegister()
156
+ Router.middlewareGroup("api", [ThrottleMiddleware, BearerTokenMiddleware]);
157
+ Router.middlewareGroup("web", [SessionMiddleware, CsrfMiddleware]);
158
+ ```
159
+
160
+ ```ts
161
+ // routes/index.ts
162
+ Router.group({ prefix: "/api/v1", middleware: "api" }, () => {
163
+ Router.resource("posts", PostController);
164
+ });
165
+
166
+ Router.group({ middleware: ["web", AuthMiddleware] }, () => {
167
+ Router.get("/dashboard", DashboardController, "index");
168
+ });
169
+ ```
170
+
171
+ ### File-based route middleware
172
+
173
+ Drop a `_middleware.ts` file into any directory under your file routes folder.
174
+ It applies to every route file in that directory and all subdirectories:
175
+
176
+ ```ts
177
+ // app/routes/admin/_middleware.ts
178
+ import { AuthMiddleware } from "@zerotal/auth";
179
+ import { AdminMiddleware } from "../../middleware/AdminMiddleware.ts";
180
+
181
+ export const middleware = [AuthMiddleware, AdminMiddleware];
182
+ ```
183
+
184
+ Middleware stacks from outer directories are prepended automatically —
185
+ you get `root/_middleware → admin/_middleware → route handler` in one pipeline.
186
+
187
+ ## Global middleware
188
+
189
+ Register middleware that runs on every request in `bootstrap/app.ts`:
190
+
191
+ ```ts
192
+ // bootstrap/app.ts
193
+ export default Application.create({ providers }).use([
194
+ DevtoolsInjectionMiddleware,
195
+ RequestIdMiddleware,
196
+ ]);
197
+ ```
198
+
199
+ Framework providers auto-register their own middleware (e.g. `SessionMiddleware`
200
+ from `@zerotal/session`, `AuthMiddleware` from `@zerotal/auth`) — you do not need to
201
+ add them manually.
202
+
203
+ ## Built-in middleware
204
+
205
+ The package ships several middleware you can drop straight into `app.use([...])`
206
+ or a route's middleware array. Each extends `BaseMiddleware`, so `.with({ … })`
207
+ bakes options into a zero-argument class.
208
+
209
+ ### CorsMiddleware
210
+
211
+ ```ts
212
+ // bootstrap/app.ts
213
+ import { CorsMiddleware } from "zerotal";
214
+
215
+ // Permissive (default — allow any origin)
216
+ app.use([CorsMiddleware]);
217
+
218
+ // Restrict to one origin
219
+ app.use([CorsMiddleware.with({ origin: "https://app.example.com", credentials: true })]);
220
+
221
+ // Dynamic origin check
222
+ app.use([CorsMiddleware.with({ origin: (o) => o.endsWith(".mycompany.com") })]);
223
+ ```
224
+
225
+ | Option | Default | Description |
226
+ | ---------------- | -------------- | ------------------------------------------------------------ |
227
+ | `origin` | `'*'` | Allowed origins — string, string[], or `(origin) => boolean` |
228
+ | `methods` | all verbs | Allowed HTTP methods |
229
+ | `allowedHeaders` | common headers | Allowed request headers |
230
+ | `exposedHeaders` | `[]` | Headers JS may read from the response |
231
+ | `credentials` | `false` | Allow cookies / auth in cross-origin requests |
232
+ | `maxAge` | `600` | Preflight cache duration (seconds) |
233
+
234
+ > **Danger** — Setting `origin: '*'` together with `credentials: true` is rejected by browsers
235
+ > and leaks cross-origin responses. Name an explicit origin (or use the function form) whenever
236
+ > you allow credentials.
237
+
238
+ ### ThrottleMiddleware
239
+
240
+ Rate-limits requests with an in-memory sliding window counter. Returns `429`
241
+ with `Retry-After` and `X-RateLimit-*` headers when the limit is exceeded.
242
+
243
+ ```ts
244
+ // bootstrap/app.ts (global) and routes/index.ts (per-route)
245
+ import { ThrottleMiddleware } from "zerotal";
246
+
247
+ // Global: 120 requests / minute
248
+ app.use([ThrottleMiddleware.with({ maxAttempts: 120, windowSeconds: 60 })]);
249
+
250
+ // Per-route: 5 login attempts / minute
251
+ Router.post("/login", AuthController, "login", [
252
+ ThrottleMiddleware.with({ maxAttempts: 5, windowSeconds: 60 }),
253
+ ]);
254
+
255
+ // By authenticated user ID instead of IP
256
+ ThrottleMiddleware.with({
257
+ maxAttempts: 1000,
258
+ windowSeconds: 3600,
259
+ keyResolver: (ctx) => String(ctx.user?.id ?? ctx.ip()),
260
+ });
261
+ ```
262
+
263
+ | Option | Default | Description |
264
+ | ---------------- | ----------- | ------------------------------------- |
265
+ | `maxAttempts` | (required) | Max requests in the window |
266
+ | `windowSeconds` | `60` | Window length in seconds |
267
+ | `keyResolver` | IP address | Function returning the rate-limit key |
268
+ | `trustedProxies` | `undefined` | Number of trusted upstream proxies |
269
+
270
+ ### RateLimiter — named limiters
271
+
272
+ For limits reused across routes (and queryable/resettable at runtime), define a
273
+ **named limiter** once at boot (e.g. in a `ServiceProvider`), then apply it by name.
274
+
275
+ ```ts
276
+ // in a ServiceProvider (boot time)
277
+ import { RateLimiter } from "zerotal";
278
+
279
+ // 1000 req/hour per authenticated user (falls back to IP when unauthenticated)
280
+ RateLimiter.for("api").limit(1000).every(3600).byUser().register();
281
+
282
+ // 5 login attempts per minute, per IP
283
+ RateLimiter.for("login").limit(5).every(60).byIp().register();
284
+
285
+ // 500 req/min keyed by an API-key header (unknown key -> per IP)
286
+ RateLimiter.for("partner").limit(500).every(60).byApiKey("x-api-key").register();
287
+
288
+ // Custom key
289
+ RateLimiter.for("upload")
290
+ .limit(10)
291
+ .every(3600)
292
+ .by((ctx) => `user:${ctx.user?.id ?? "anon"}`)
293
+ .register();
294
+ ```
295
+
296
+ Apply as route middleware with `RateLimiter.middleware(name)`:
297
+
298
+ ```ts
299
+ // routes/index.ts
300
+ Router.post("/login", AuthController, "login", [RateLimiter.middleware("login")]);
301
+
302
+ Router.group({ prefix: "/api", middleware: [RateLimiter.middleware("api")] }, () => {
303
+ Router.get("/users", UserController, "index");
304
+ });
305
+ ```
306
+
307
+ | Method | Keys on | Falls back to |
308
+ | -------------------- | --------------------------------------------- | ------------------------ |
309
+ | `.byUser()` | `ctx.user.id` | IP when unauthenticated |
310
+ | `.byApiKey(header?)` | `x-api-key` header (or custom) | IP when header is absent |
311
+ | `.byIp()` | Socket IP -> `X-Forwarded-For` -> `X-Real-IP` | `'unknown'` |
312
+ | `.by(fn)` | Return value of your function | - |
313
+
314
+ Check or reset a limiter manually — e.g. clear failed login attempts after a
315
+ successful sign-in:
316
+
317
+ ```ts
318
+ // in a controller action — `ctx` is the HttpContext the action receives
319
+ if (await RateLimiter.tooManyAttempts("login", ctx)) {
320
+ return ctx.json({ message: "Too Many Requests" }, 429);
321
+ }
322
+
323
+ RateLimiter.resetFor("login", ctx); // clear the counter for this actor
324
+ ```
325
+
326
+ > **Tip** — `ThrottleMiddleware.with({ … })` is inline and per-attachment; a named `RateLimiter`
327
+ > is defined once and can be reused, queried with `tooManyAttempts()`, and cleared with
328
+ > `resetFor()`. Use the inline form for one-off routes, the named form when the same limit
329
+ > appears in several places. See [Rate limiting](/docs/rate-limiting) for the full surface.
330
+
331
+ ### SecureHeadersMiddleware
332
+
333
+ Adds `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, and a
334
+ basic `Content-Security-Policy` to every response:
335
+
336
+ ```ts
337
+ // bootstrap/app.ts
338
+ import { SecureHeadersMiddleware } from "zerotal";
339
+ app.use([SecureHeadersMiddleware]);
340
+ ```
341
+
342
+ It is registered for you as kernel middleware, so an app gets these headers
343
+ without asking. Configure them under `app.secureHeaders` in `config/app.ts` — see
344
+ [Configuration](/docs/config-system).
345
+
346
+ **Static files get the same headers**, even though no middleware runs for them.
347
+ Files under `public/` are handed to Bun as pre-registered responses and served
348
+ without entering JavaScript, so the pipeline never sees them; the header set is
349
+ baked into those responses at registration time instead. A per-directory header
350
+ passed to `Router.static()` still wins, so a mount that is deliberately
351
+ embeddable stays that way.
352
+
353
+ > **Tip** — Run [`bun zt doctor --url=…`](/docs/deployment) after deploying. A
354
+ > header your app sets and your proxy also sets is invisible from inside the
355
+ > process, and browsers disagree about which copy applies.
356
+
357
+ ### WebhookMiddleware
358
+
359
+ Verifies HMAC-SHA256 signatures on incoming webhook requests:
360
+
361
+ ```ts
362
+ // routes/index.ts
363
+ import { WebhookMiddleware } from "zerotal";
364
+
365
+ Router.post("/webhooks/stripe", StripeController, "handle", [
366
+ WebhookMiddleware.with({
367
+ secret: Bun.env.STRIPE_WEBHOOK_SECRET!,
368
+ header: "stripe-signature",
369
+ algorithm: "sha256",
370
+ }),
371
+ ]);
372
+ ```
373
+
374
+ > **Danger** — Keep the webhook `secret` in an environment variable, never hard-coded. A leaked
375
+ > secret lets anyone forge valid signatures and call your webhook endpoint.
376
+
377
+ ## Middleware with constructor injection
378
+
379
+ Decorate the class with `@inject(...)`, listing its dependency tokens in
380
+ constructor order; the container resolves them and passes them in:
381
+
382
+ ```ts
383
+ // app/middleware/AuditMiddleware.ts
384
+ import { inject } from "zerotal";
385
+ import type { Pipe, NextFn, HttpContext } from "zerotal";
386
+
387
+ @inject(AuditLogger)
388
+ export class AuditMiddleware implements Pipe<HttpContext> {
389
+ constructor(private logger: AuditLogger) {}
390
+
391
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
392
+ const response = await next();
393
+ await this.logger.record(ctx.request.method, ctx.path(), ctx.user?.id);
394
+ return response;
395
+ }
396
+ }
397
+ ```
398
+
399
+ ## Execution order
400
+
401
+ Middleware nests like layers of an onion: outer layers run first on the way in,
402
+ and last on the way out (after `await next()`).
403
+
404
+ ```text
405
+ Global (app.use) ← outermost, first in / last out
406
+ Provider auto-middleware ← Session, Auth, etc.
407
+ Switch middleware ← CORS, Throttle, SecureHeaders
408
+ Group middleware ← Router.group({ middleware })
409
+ Route middleware ← Router.get(path, C, a, [M])
410
+ Controller action ← innermost
411
+ ← route mw unwind
412
+ ← group mw unwind
413
+ ← switch mw unwind
414
+ ← provider mw unwind ← session saved here
415
+ ← global mw unwind
416
+ ```
417
+
418
+ ## Testing
419
+
420
+ Set your suite up once as described in [Testing](/docs/testing). Middleware has
421
+ two behaviours worth proving, and they are easy to confuse: what it does when it
422
+ lets a request **through**, and what it does when it **stops** one.
423
+
424
+ **Test the stop first**, because it is the reason the middleware exists:
425
+
426
+ ```typescript
427
+ // tests/http/middleware.test.ts
428
+ import { test } from "bun:test";
429
+ import { createApp } from "../helpers.ts";
430
+
431
+ test("a guest is turned away from a protected route", async () => {
432
+ const app = await createApp();
433
+
434
+ const res = await app.get("/dashboard");
435
+
436
+ res.assertRedirect("/login");
437
+ await app.close();
438
+ });
439
+ ```
440
+
441
+ **Then prove it lets the right request through**, otherwise a middleware that
442
+ rejects everything passes the first test perfectly:
443
+
444
+ ```typescript
445
+ // tests/http/middleware.test.ts
446
+ const res = await app.actingAs(user).get("/dashboard");
447
+
448
+ res.assertOk();
449
+ ```
450
+
451
+ **A middleware that transforms rather than blocks** is tested through its effect.
452
+ Register a probe route in the `setup` callback — `createTestApp(bootstrap, setup)`
453
+ runs it before the server starts, so the route compiles into the router:
454
+
455
+ ```typescript
456
+ // tests/http/middleware.test.ts
457
+ const app = await createApp(() => {
458
+ Router.get("/probe", () => ({ locale: Context.get("locale") })).middleware([LocaleMiddleware]);
459
+ });
460
+
461
+ const res = await app.get("/probe", { "Accept-Language": "fr" });
462
+
463
+ res.assertJsonPath("locale", "fr");
464
+ ```
465
+
466
+ > **Warning** — Middleware ordering is behaviour, not configuration. If auth must
467
+ > run before a rate limiter (so anonymous floods are cheap) or after it (so
468
+ > logins are throttled), write the test that fails when the order flips —
469
+ > reordering the array is a one-line change nobody reviews closely.
470
+
471
+ ## References
472
+
473
+ Imported from `zerotal`.
474
+
475
+ | Member | Signature | Description |
476
+ | ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
477
+ | `Pipe<T>` | `interface Pipe<T> { handle(payload: T, next: NextFn): Promise<Response \| void> }` | The contract every middleware implements (`T` is `HttpContext`). |
478
+ | `NextFn` | `type NextFn = () => Promise<Response \| void>` | Passes control downstream; resolves to the downstream `Response`. |
479
+ | `HttpContext` | `class HttpContext<TParams>` | The request context passed to `handle` — read params from `ctx.params`. |
480
+ | `withHeaders` | `withHeaders(res: Response, headers: Record<string, string>): Response` | Returns a copy of `res` with headers added (safe on immutable responses). |
481
+ | `BaseMiddleware` | `class BaseMiddleware<O> { static with(options: Partial<O>): new () => … }` | Base class providing the `.with()` option-baking helper. |
482
+
483
+ Built-in middleware classes (`MiddlewareClass.with(options)` where noted):
484
+
485
+ | Class | Configure with | Purpose |
486
+ | ------------------------- | ----------------------------- | -------------------------------------------------- |
487
+ | `CorsMiddleware` | `.with(CorsOptions)` | Cross-origin resource sharing headers. |
488
+ | `ThrottleMiddleware` | `.with(ThrottleOptions)` | In-memory sliding-window rate limiting. |
489
+ | `SecureHeadersMiddleware` | `.with(SecureHeadersOptions)` | Security response headers (CSP, frame options, …). |
490
+ | `WebhookMiddleware` | `.with(WebhookOptions)` | HMAC signature verification for webhooks. |
491
+
492
+ Named rate limiters via `RateLimiter`:
493
+
494
+ | Method | Signature | Description |
495
+ | ---------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------- |
496
+ | `RateLimiter.for` | `for(name: string): LimiterDefinition` | Start defining a named limiter. |
497
+ | `RateLimiter.middleware` | `middleware(name: string): ThrottleMiddleware` | Get the middleware for a registered limiter. |
498
+ | `RateLimiter.tooManyAttempts` | `tooManyAttempts(name: string, ctx: HttpContext): Promise<boolean>` | Record a hit and report whether the limit is hit. |
499
+ | `RateLimiter.resetFor` | `resetFor(name: string, ctx: HttpContext): void` | Clear the counter for this actor's key. |
500
+ | `LimiterDefinition.limit`/`.every` | `limit(max): this` / `every(seconds): this` | Set the window size and length. |
501
+ | `LimiterDefinition.byUser`/`.byIp`/`.byApiKey`/`.by` | `byUser(): this` / `byApiKey(header?): this` / `by(fn): this` | Choose the key strategy. |
502
+ | `LimiterDefinition.register` | `register(): this` | Register the limiter with the global registry. |
503
+
504
+ ## Next steps
505
+
506
+ - [Routing](/docs/routing) — attach middleware to routes and groups.
507
+ - [Lifecycle](/docs/lifecycle) — where middleware sits in the request flow.
508
+ - [Controllers](/docs/controllers) — move route logic out of closures.
509
+ - [Rate limiting](/docs/rate-limiting) — named limiters in depth.