@shaferllc/keel 0.66.0 → 0.68.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 (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
@@ -0,0 +1,953 @@
1
+ # Request & Response
2
+
3
+ Beyond the terse `param()` / `json()` shortcuts, the `request` and `response`
4
+ accessors give you the full input/output surface — no context threading. They
5
+ resolve the active Hono context from async-context storage, which the HTTP
6
+ kernel enables for every request, so they only work **inside a request**. Call
7
+ one outside a handler and it throws (`json`/`text`/`html`/`redirect` are the
8
+ exception — see below).
9
+
10
+ ## Reading input
11
+
12
+ ```ts
13
+ import { request } from "@shaferllc/keel/core";
14
+
15
+ request.param("id"); // route parameter
16
+ request.query("q"); // query-string value
17
+ request.header("authorization");
18
+
19
+ // merged query + parsed body (async)
20
+ await request.all(); // { …query, …body }
21
+ await request.input("email"); // one value from query or body
22
+ await request.input("page", 1); // with a fallback
23
+ await request.only(["email", "name"]); // a subset
24
+ await request.except(["password"]); // everything but these
25
+ ```
26
+
27
+ `request.all()` reads the query string and, if the request carries a body,
28
+ merges the parsed body over it — JSON bodies via `req.json()`, form bodies
29
+ (`multipart/form-data` or `application/x-www-form-urlencoded`) via cached
30
+ `FormData`. File fields are dropped from `all()` (reach for `file()`/`files()`).
31
+ A missing or malformed body is swallowed — you just get the query values.
32
+ `input`/`only`/`except` all build on `all()`, so they're async too.
33
+
34
+ `request` also exposes `request.method`, `request.path`, `request.url`,
35
+ `request.status`, `request.ip()`, `request.ips()`, `request.hasBody()`,
36
+ `request.headers()`, and `request.raw` (the underlying web `Request`). For the
37
+ raw parsed JSON body without the query merge, use `request.json<T>()`.
38
+
39
+ For URL and connection introspection there's `request.protocol`,
40
+ `request.secure`, `request.host`, `request.hostname`, `request.origin`,
41
+ `request.fullUrl`, and `request.querystring`. These are **proxy-aware**:
42
+ `X-Forwarded-Proto` / `X-Forwarded-Host` win over the raw request URL, so an app
43
+ behind a TLS-terminating proxy or load balancer sees the client's real scheme
44
+ and host (handy for building absolute links or forcing HTTPS).
45
+
46
+ > `input`/`only`/`except`/`all` are object methods that lean on `this`. Call
47
+ > them off `request` (`request.input(…)`), not destructured (`const { input } =
48
+ > request`), or `this` is lost.
49
+
50
+ ### Other content types
51
+
52
+ `all()` understands JSON and form bodies. For anything else — XML, CSV, a binary
53
+ payload, a custom format — read the raw body and parse it yourself. There's no
54
+ content-type parser registry to configure (unlike Fastify): parsing is explicit,
55
+ so you call the accessor you want.
56
+
57
+ ```ts
58
+ await request.text(); // the body as a string (XML, CSV, …)
59
+ await request.arrayBuffer(); // the body as bytes (protobuf, msgpack, …)
60
+ await request.blob(); // the body as a Blob
61
+ ```
62
+
63
+ ```ts
64
+ r.post("/webhook", async () => {
65
+ const xml = await request.text();
66
+ return response.json(parseXml(xml));
67
+ });
68
+ ```
69
+
70
+ These read from the same underlying `Request` as `request.raw`, so a middleware
71
+ can equally parse a custom type once and stash it with `ctx().set("body", …)`.
72
+
73
+ ## Route info
74
+
75
+ The kernel stashes the matched route on the context, so you can branch on it:
76
+
77
+ ```ts
78
+ request.route; // { name, pattern, methods } | undefined
79
+ request.routeIs("users.show"); // true if the matched route is named that
80
+ request.subdomain("tenant"); // a param captured from a domain-bound route
81
+ ```
82
+
83
+ ## File uploads
84
+
85
+ Uploaded files come back as web-standard `File` objects (works on Node and the
86
+ edge — no temp directory, no streaming to disk):
87
+
88
+ ```ts
89
+ const avatar = await request.file("avatar"); // File | undefined
90
+ const docs = await request.files("docs"); // File[]
91
+ const all = await request.allFiles(); // { field: File | File[] }
92
+
93
+ if (avatar) {
94
+ avatar.name; // "photo.png"
95
+ avatar.size; // bytes
96
+ avatar.type; // "image/png" (client-supplied)
97
+ const bytes = await avatar.arrayBuffer(); // persist via R2/S3/fs yourself
98
+ }
99
+ ```
100
+
101
+ The `FormData` is parsed once per request and cached, so calling `file()`,
102
+ `files()`, `allFiles()`, and `all()` in one handler doesn't re-read the body.
103
+ `allFiles()` groups repeated field names into an array and single fields into a
104
+ lone `File`.
105
+
106
+ Validate a file with your schema (Keel stays schema-agnostic):
107
+
108
+ ```ts
109
+ const Upload = z.object({
110
+ avatar: z.instanceof(File).refine((f) => f.size < 2_000_000, "Too large"),
111
+ });
112
+ ```
113
+
114
+ ## Content negotiation
115
+
116
+ ```ts
117
+ request.accepts(["application/json", "text/html"]); // best match, or null
118
+ request.types(); // accepted types, ordered
119
+ request.language(["en", "fr"]); // best language, or null
120
+ request.languages();
121
+ request.encoding(["br", "gzip"]); // best content encoding
122
+ request.charset(["utf-8"]); // best charset
123
+ ```
124
+
125
+ `accepts`/`language`/`encoding`/`charset` parse the relevant `Accept*` header by
126
+ q-weight and return the highest-preference offered value, honoring `*/*` (or
127
+ `*`) as "anything" — which resolves to the first thing you offer. No match
128
+ returns `null`. Each has a plural list form (`types()`, `languages()`,
129
+ `encodings()`, `charsets()`) returning everything the client accepts, ordered by
130
+ preference.
131
+
132
+ ## Cookies
133
+
134
+ ```ts
135
+ request.cookie("session"); // one cookie, or undefined
136
+ request.cookie(); // all cookies as an object
137
+
138
+ response.cookie("session", token, { httpOnly: true, maxAge: 3600 });
139
+ response.clearCookie("session");
140
+ ```
141
+
142
+ ## Writing output
143
+
144
+ ```ts
145
+ import { response } from "@shaferllc/keel/core";
146
+
147
+ response.json({ ok: true });
148
+ response.text("hello");
149
+ response.html("<h1>Hi</h1>");
150
+ response.redirect("/login");
151
+ response.back("/"); // back to the Referer, else the fallback
152
+ response.send(anything); // objects → JSON, else text
153
+
154
+ response.status(201).json(created); // chainable
155
+ response.header("x-total", "42").json(rows);
156
+ response.type("text/csv").append("vary", "accept");
157
+ response.attachment("report.csv").text(csv); // Content-Disposition download
158
+ response.removeHeader("x-powered-by");
159
+ response.cookie("flash", "saved").redirect("/");
160
+ ```
161
+
162
+ Every mutator (`status`, `header`, `type`, `attachment`, `append`,
163
+ `removeHeader`, `cookie`, `clearCookie`) returns `response`, so they chain; the
164
+ terminal `json`/`text`/`html`/`redirect`/`back`/`send` produce the `Response`.
165
+ `send` inspects its argument — a non-null object becomes JSON, anything else is
166
+ stringified to text. `back` bounces to the `Referer` header (falling back to the
167
+ argument, default `"/"`); `redirect("back")` is the same shortcut.
168
+
169
+ ## Aborting with guards
170
+
171
+ ```ts
172
+ response.abort("Not found", 404); // always
173
+ response.abortIf(!user, "Not found", 404); // if truthy
174
+ response.abortUnless(user.isAdmin, "Forbidden", 403);
175
+ ```
176
+
177
+ `abort()` throws an `HttpException` (default status `400`), which the kernel
178
+ renders (see [Errors](./errors.md)). `abortIf`/`abortUnless` throw the same,
179
+ conditionally — handy as one-line guards at the top of a handler.
180
+
181
+ ## Standalone shortcuts
182
+
183
+ Every reader/writer also exists as a flat helper for terse handlers:
184
+ `json()`, `text()`, `html()`, `redirect()`, `param()`, `query()`, `header()`,
185
+ `body()`. Use whichever reads best — they resolve the same request.
186
+
187
+ The response builders (`json`/`text`/`html`/`redirect`) are special: they work
188
+ **both** inside a handler and standalone. Inside a request they build on the
189
+ context (so status and queued headers/cookies apply); outside one they fall
190
+ back to a plain web `Response`. That's what lets you hand one straight to the
191
+ router as a static route value — `router.get("/ping", json({ ok: true }))` —
192
+ and have it cloned per request.
193
+
194
+ ---
195
+
196
+ ## API reference
197
+
198
+ ### `ctx()`
199
+
200
+ `ctx(): Context`
201
+
202
+ Returns the current Hono `Context` from async-context storage — the escape
203
+ hatch when you need something the accessors don't wrap.
204
+
205
+ ```ts
206
+ import { ctx } from "@shaferllc/keel/core";
207
+
208
+ ctx().req.raw; // the web Request
209
+ ctx().executionCtx; // waitUntil, passThroughOnException, …
210
+ ```
211
+
212
+ **Notes:** throws if called outside a request (nothing has set up the context).
213
+ Everything else in this module is built on it.
214
+
215
+ ### `json(data, status?)`
216
+
217
+ `json(data: unknown, status?: number): Response`
218
+
219
+ Serializes `data` to a JSON `Response`.
220
+
221
+ ```ts
222
+ import { json } from "@shaferllc/keel/core";
223
+
224
+ json({ ok: true });
225
+ json({ error: "nope" }, 422);
226
+ ```
227
+
228
+ **Notes:** works inside a handler (builds on the context, applying any queued
229
+ status/headers) and standalone (a plain `Response.json`). Safe as a static
230
+ route value.
231
+
232
+ ### `text(body, status?)`
233
+
234
+ `text(body: string, status?: number): Response`
235
+
236
+ Returns a `text/plain; charset=UTF-8` response.
237
+
238
+ ```ts
239
+ import { text } from "@shaferllc/keel/core";
240
+
241
+ text("pong");
242
+ text("rate limited", 429);
243
+ ```
244
+
245
+ **Notes:** dual-mode like `json`. Standalone, it sets the content-type header
246
+ itself.
247
+
248
+ ### `html(body, status?)`
249
+
250
+ `html(body: string, status?: number): Response`
251
+
252
+ Returns a `text/html; charset=UTF-8` response.
253
+
254
+ ```ts
255
+ import { html } from "@shaferllc/keel/core";
256
+
257
+ html("<h1>Hi</h1>");
258
+ ```
259
+
260
+ **Notes:** does not escape `body` — you're responsible for the markup.
261
+
262
+ ### `redirect(location, status?)`
263
+
264
+ `redirect(location: string, status?: number): Response`
265
+
266
+ Returns a redirect to `location`.
267
+
268
+ ```ts
269
+ import { redirect } from "@shaferllc/keel/core";
270
+
271
+ redirect("/login");
272
+ redirect("/", 301);
273
+ ```
274
+
275
+ **Notes:** default status is `302` in standalone mode. Sets the `Location`
276
+ header.
277
+
278
+ ### `request`
279
+
280
+ The flat request accessor. You import it as-is (it's a singleton object, not a
281
+ class) and read off it — every access resolves the current context, so it's
282
+ always about the in-flight request.
283
+
284
+ #### `request.method`
285
+
286
+ `get method(): string`
287
+
288
+ The HTTP method (`"GET"`, `"POST"`, …).
289
+
290
+ ```ts
291
+ if (request.method === "POST") { /* … */ }
292
+ ```
293
+
294
+ #### `request.path`
295
+
296
+ `get path(): string`
297
+
298
+ The request path, without query string.
299
+
300
+ ```ts
301
+ request.path; // "/users/1"
302
+ ```
303
+
304
+ #### `request.url`
305
+
306
+ `get url(): string`
307
+
308
+ The full request URL, including query string.
309
+
310
+ ```ts
311
+ request.url; // "https://api.example.com/users/1?tab=posts"
312
+ ```
313
+
314
+ #### `request.protocol` · `request.secure` · `request.host` · `request.hostname` · `request.origin` · `request.fullUrl` · `request.querystring`
315
+
316
+ Proxy-aware URL and connection accessors — `X-Forwarded-Proto` and
317
+ `X-Forwarded-Host` take precedence over the raw request URL, so an app behind a
318
+ TLS-terminating proxy sees the client's real scheme and host.
319
+
320
+ ```ts
321
+ request.protocol; // "https" (get protocol(): string)
322
+ request.secure; // true (get secure(): boolean — protocol === "https")
323
+ request.host; // "example.com:443" (host with port)
324
+ request.hostname; // "example.com" (host without port)
325
+ request.origin; // "https://example.com"
326
+ request.fullUrl; // "https://example.com/users/1?tab=posts"
327
+ request.querystring; // "tab=posts" (no leading "?", "" when none)
328
+ ```
329
+
330
+ **Notes:** `fullUrl` is rebuilt from the (forwarded) origin plus the path and
331
+ query, so it reflects the client-facing URL rather than the internal one the
332
+ proxy dialed. Use `origin` to build absolute links and `secure` to gate or
333
+ redirect insecure requests.
334
+
335
+ #### `request.status`
336
+
337
+ `get status(): number`
338
+
339
+ The current response status — useful in middleware after `await next()`.
340
+
341
+ ```ts
342
+ await next();
343
+ if (request.status >= 500) log(request.path);
344
+ ```
345
+
346
+ #### `request.header(name)`
347
+
348
+ `header(name: string): string | undefined`
349
+
350
+ A single request header (case-insensitive), or `undefined`.
351
+
352
+ ```ts
353
+ request.header("authorization");
354
+ ```
355
+
356
+ #### `request.param(name?)`
357
+
358
+ `param(name?: string): string | Record<string, string>`
359
+
360
+ One route parameter by name, or all of them as an object when called with no
361
+ argument.
362
+
363
+ ```ts
364
+ request.param("id"); // "42"
365
+ request.param(); // { id: "42" }
366
+ ```
367
+
368
+ **Notes:** the return type is a union — narrow it, or prefer the overloaded
369
+ standalone `param()` helper when you want a precise `string`.
370
+
371
+ #### `request.query(name?)`
372
+
373
+ `query(name?: string): string | undefined | Record<string, string>`
374
+
375
+ One query-string value, or the whole query object with no argument.
376
+
377
+ ```ts
378
+ request.query("q"); // "keel" | undefined
379
+ request.query(); // { q: "keel", page: "2" }
380
+ ```
381
+
382
+ #### `request.json<T>()`
383
+
384
+ `json<T = unknown>(): Promise<T>`
385
+
386
+ The parsed JSON body, typed as `T`.
387
+
388
+ ```ts
389
+ const body = await request.json<{ email: string }>();
390
+ ```
391
+
392
+ **Notes:** rejects if the body isn't valid JSON. For a query+body merge instead,
393
+ use `request.all()`.
394
+
395
+ #### `request.text()` · `request.arrayBuffer()` · `request.blob()`
396
+
397
+ `text(): Promise<string>` · `arrayBuffer(): Promise<ArrayBuffer>` · `blob(): Promise<Blob>`
398
+
399
+ The raw request body, for content types `json()`/`all()` don't handle — parse it
400
+ yourself.
401
+
402
+ ```ts
403
+ const xml = await request.text(); // XML, CSV, plain text
404
+ const bytes = await request.arrayBuffer(); // protobuf, msgpack, binary
405
+ ```
406
+
407
+ **Notes:** thin passes to the underlying Hono request, which caches the body, so
408
+ these compose with each other (the body isn't re-read).
409
+
410
+ #### `request.raw`
411
+
412
+ `get raw(): Request`
413
+
414
+ The underlying web `Request`.
415
+
416
+ ```ts
417
+ request.raw.signal; // AbortSignal, streaming body, etc.
418
+ ```
419
+
420
+ #### `request.route`
421
+
422
+ `get route(): { name?: string; pattern?: string; methods?: string[] } | undefined`
423
+
424
+ The matched route descriptor the kernel stashed on the context.
425
+
426
+ ```ts
427
+ request.route?.name; // "users.show"
428
+ ```
429
+
430
+ **Notes:** `undefined` if no named route matched (or the kernel didn't set it).
431
+
432
+ #### `request.routeIs(name)`
433
+
434
+ `routeIs(name: string): boolean`
435
+
436
+ Whether the matched route has the given name.
437
+
438
+ ```ts
439
+ if (request.routeIs("users.show")) highlightNav();
440
+ ```
441
+
442
+ #### `request.subdomain(name)`
443
+
444
+ `subdomain(name: string): string | undefined`
445
+
446
+ A subdomain parameter captured from a domain-bound route.
447
+
448
+ ```ts
449
+ request.subdomain("tenant"); // "acme" for acme.example.com
450
+ ```
451
+
452
+ #### `request.cookie(name?)`
453
+
454
+ `cookie(name?: string): string | undefined | Record<string, string>`
455
+
456
+ One request cookie by name, or all cookies with no argument.
457
+
458
+ ```ts
459
+ request.cookie("session"); // "abc123" | undefined
460
+ request.cookie(); // { session: "abc123" }
461
+ ```
462
+
463
+ #### `request.ip()`
464
+
465
+ `ip(): string | undefined`
466
+
467
+ The client IP, from `X-Forwarded-For` (first hop) then `X-Real-IP`.
468
+
469
+ ```ts
470
+ request.ip(); // "203.0.113.7"
471
+ ```
472
+
473
+ **Notes:** trusts proxy headers — only reliable behind a proxy you control.
474
+
475
+ #### `request.ips()`
476
+
477
+ `ips(): string[]`
478
+
479
+ The full `X-Forwarded-For` chain, client first.
480
+
481
+ ```ts
482
+ request.ips(); // ["203.0.113.7", "10.0.0.1"]
483
+ ```
484
+
485
+ **Notes:** empty array when there's no `X-Forwarded-For`.
486
+
487
+ #### `request.hasBody()`
488
+
489
+ `hasBody(): boolean`
490
+
491
+ True if the request declares a body (has `Content-Length` or
492
+ `Transfer-Encoding`).
493
+
494
+ ```ts
495
+ if (request.hasBody()) await request.all();
496
+ ```
497
+
498
+ #### `request.headers()`
499
+
500
+ `headers(): Record<string, string>`
501
+
502
+ All request headers as a plain object (names lower-cased by the runtime).
503
+
504
+ ```ts
505
+ request.headers(); // { "content-type": "application/json", … }
506
+ ```
507
+
508
+ #### `request.all()`
509
+
510
+ `all(): Promise<Record<string, unknown>>`
511
+
512
+ The query string merged with the parsed body (body wins on key collisions).
513
+
514
+ ```ts
515
+ const input = await request.all(); // { …query, …body }
516
+ ```
517
+
518
+ **Notes:** async. Handles JSON and form bodies; drops file fields; swallows a
519
+ missing/invalid body. Backs `input`/`only`/`except`.
520
+
521
+ #### `request.input(key, fallback?)`
522
+
523
+ `input<T = unknown>(key: string, fallback?: T): Promise<T>`
524
+
525
+ A single value from `all()`, with an optional fallback when the key is absent.
526
+
527
+ ```ts
528
+ const email = await request.input("email");
529
+ const page = await request.input("page", 1); // T inferred as number
530
+ ```
531
+
532
+ **Notes:** the fallback only applies when the key is missing entirely — a
533
+ present-but-empty value is returned as-is.
534
+
535
+ #### `request.only(keys)`
536
+
537
+ `only(keys: string[]): Promise<Record<string, unknown>>`
538
+
539
+ Just the named inputs from `all()`.
540
+
541
+ ```ts
542
+ await request.only(["email", "name"]);
543
+ ```
544
+
545
+ **Notes:** keys not present are omitted (not set to `undefined`).
546
+
547
+ #### `request.except(keys)`
548
+
549
+ `except(keys: string[]): Promise<Record<string, unknown>>`
550
+
551
+ Every input except the named ones.
552
+
553
+ ```ts
554
+ await request.except(["password", "_csrf"]);
555
+ ```
556
+
557
+ #### `request.file(name)`
558
+
559
+ `file(name: string): Promise<File | undefined>`
560
+
561
+ One uploaded file by field name, as a web `File`.
562
+
563
+ ```ts
564
+ const avatar = await request.file("avatar");
565
+ if (avatar) await store(await avatar.arrayBuffer());
566
+ ```
567
+
568
+ **Notes:** `undefined` if the field is absent or wasn't a file.
569
+
570
+ #### `request.files(name)`
571
+
572
+ `files(name: string): Promise<File[]>`
573
+
574
+ All uploaded files for a repeated field name.
575
+
576
+ ```ts
577
+ const docs = await request.files("docs"); // File[]
578
+ ```
579
+
580
+ **Notes:** empty array when there are none; non-file values are filtered out.
581
+
582
+ #### `request.allFiles()`
583
+
584
+ `allFiles(): Promise<Record<string, File | File[]>>`
585
+
586
+ Every uploaded file, grouped by field name.
587
+
588
+ ```ts
589
+ const files = await request.allFiles(); // { avatar: File, docs: File[] }
590
+ ```
591
+
592
+ **Notes:** a field with one file maps to a lone `File`; repeated fields map to
593
+ `File[]`.
594
+
595
+ #### `request.accepts(types)`
596
+
597
+ `accepts(types: string[]): string | null`
598
+
599
+ The best of the offered content types per the `Accept` header, or `null`.
600
+
601
+ ```ts
602
+ switch (request.accepts(["application/json", "text/html"])) {
603
+ case "application/json": return json(data);
604
+ case "text/html": return html(page);
605
+ default: return response.abort("Not acceptable", 406);
606
+ }
607
+ ```
608
+
609
+ **Notes:** honors `*/*`/`*` (returns the first offered). `null` when nothing
610
+ matches.
611
+
612
+ #### `request.types()`
613
+
614
+ `types(): string[]`
615
+
616
+ Accepted content types, ordered by q-weight preference.
617
+
618
+ ```ts
619
+ request.types(); // ["text/html", "application/json"]
620
+ ```
621
+
622
+ #### `request.language(languages)`
623
+
624
+ `language(languages: string[]): string | null`
625
+
626
+ The best of the offered languages per `Accept-Language`, or `null`.
627
+
628
+ ```ts
629
+ request.language(["en", "fr"]); // "fr"
630
+ ```
631
+
632
+ #### `request.languages()`
633
+
634
+ `languages(): string[]`
635
+
636
+ Accepted languages, ordered by preference.
637
+
638
+ ```ts
639
+ request.languages(); // ["fr", "en"]
640
+ ```
641
+
642
+ #### `request.encoding(encodings)` · `request.encodings()`
643
+
644
+ `encoding(encodings: string[]): string | null` · `encodings(): string[]`
645
+
646
+ Negotiate the response's content encoding against `Accept-Encoding` — same
647
+ q-weight and `*` rules as `accepts`.
648
+
649
+ ```ts
650
+ request.encoding(["br", "gzip"]); // "br"
651
+ request.encodings(); // ["br", "gzip", "identity"]
652
+ ```
653
+
654
+ #### `request.charset(charsets)` · `request.charsets()`
655
+
656
+ `charset(charsets: string[]): string | null` · `charsets(): string[]`
657
+
658
+ Negotiate the response charset against `Accept-Charset`.
659
+
660
+ ```ts
661
+ request.charset(["utf-8", "iso-8859-1"]); // "utf-8"
662
+ request.charsets(); // ["utf-8"]
663
+ ```
664
+
665
+ ### `response`
666
+
667
+ The flat response accessor — a singleton object mirroring `request`. Mutators
668
+ return `response` (chainable); terminals return a `Response`.
669
+
670
+ #### `response.json(data, status?)`
671
+
672
+ `json(data: unknown, status?: number): Response`
673
+
674
+ Same as the standalone `json()`, but reads nicely after chained mutators.
675
+
676
+ ```ts
677
+ response.status(201).json(created);
678
+ ```
679
+
680
+ #### `response.text(body, status?)`
681
+
682
+ `text(body: string, status?: number): Response`
683
+
684
+ A plain-text response.
685
+
686
+ ```ts
687
+ response.text("pong");
688
+ ```
689
+
690
+ #### `response.html(body, status?)`
691
+
692
+ `html(body: string, status?: number): Response`
693
+
694
+ An HTML response.
695
+
696
+ ```ts
697
+ response.html("<h1>Hi</h1>");
698
+ ```
699
+
700
+ #### `response.redirect(location, status?)`
701
+
702
+ `redirect(location: string, status?: number): Response`
703
+
704
+ A redirect response.
705
+
706
+ ```ts
707
+ response.cookie("flash", "saved").redirect("/");
708
+ ```
709
+
710
+ Passing `"back"` as the location bounces to the `Referer` header, or `"/"` when
711
+ there isn't one — see `response.back` for a version with a custom fallback.
712
+
713
+ #### `response.back(fallback?, status?)`
714
+
715
+ `back(fallback = "/", status?: number): Response`
716
+
717
+ Redirects to the request's `Referer` header, falling back to `fallback` (default
718
+ `"/"`) when the header is absent. Handy for "return to where you came from" flows
719
+ after a form post.
720
+
721
+ ```ts
722
+ response.abortUnless(ok, "Nope");
723
+ return response.back("/dashboard");
724
+ ```
725
+
726
+ #### `response.send(data, status?)`
727
+
728
+ `send(data: unknown, status?: number): Response`
729
+
730
+ Sends a value — a non-null object becomes JSON, everything else becomes text.
731
+
732
+ ```ts
733
+ response.send({ ok: true }); // JSON
734
+ response.send("pong"); // text
735
+ ```
736
+
737
+ **Notes:** `null` is treated as non-object, so it's stringified to text
738
+ (`"null"`); wrap it in an object if you want JSON `null`.
739
+
740
+ #### `response.status(code)`
741
+
742
+ `status(code: number): ResponseHelper`
743
+
744
+ Sets the response status. Chainable.
745
+
746
+ ```ts
747
+ response.status(202).json({ queued: true });
748
+ ```
749
+
750
+ #### `response.header(name, value)`
751
+
752
+ `header(name: string, value: string): ResponseHelper`
753
+
754
+ Sets a response header. Chainable.
755
+
756
+ ```ts
757
+ response.header("x-total", "42").json(rows);
758
+ ```
759
+
760
+ #### `response.headers(map)`
761
+
762
+ `headers(map: Record<string, string>): ResponseHelper`
763
+
764
+ Sets several response headers at once. Chainable.
765
+
766
+ ```ts
767
+ response.headers({ "x-total": "42", "cache-control": "no-store" });
768
+ ```
769
+
770
+ #### `response.getHeader(name)` / `response.hasHeader(name)`
771
+
772
+ `getHeader(name: string): string | null`
773
+ `hasHeader(name: string): boolean`
774
+
775
+ Read a response header set so far — useful in middleware after `await next()`, to
776
+ inspect what a handler set.
777
+
778
+ ```ts
779
+ kernel.use(async (c, next) => {
780
+ await next();
781
+ if (!response.hasHeader("cache-control")) response.header("cache-control", "no-store");
782
+ });
783
+ ```
784
+
785
+ #### `response.type(mime)`
786
+
787
+ `type(mime: string): ResponseHelper`
788
+
789
+ Sets the `Content-Type`. Chainable.
790
+
791
+ ```ts
792
+ response.type("text/csv").send(csv);
793
+ ```
794
+
795
+ #### `response.attachment(filename?)`
796
+
797
+ `attachment(filename?: string): ResponseHelper`
798
+
799
+ Marks the response as a downloadable attachment via `Content-Disposition`.
800
+ Chainable. With no argument it sets a bare `attachment`; with a filename it adds
801
+ both a quoted ASCII `filename` and an RFC 5987 `filename*` so non-ASCII names
802
+ survive. Pair it with `type()` to set the download's content type.
803
+
804
+ ```ts
805
+ response.attachment("report.csv").type("text/csv").send(csv);
806
+ // Content-Disposition: attachment; filename="report.csv"; filename*=UTF-8''report.csv
807
+ ```
808
+
809
+ #### `response.append(name, value)`
810
+
811
+ `append(name: string, value: string): ResponseHelper`
812
+
813
+ Appends to a (possibly multi-value) header rather than replacing it. Chainable.
814
+
815
+ ```ts
816
+ response.append("vary", "accept").append("vary", "accept-language");
817
+ ```
818
+
819
+ #### `response.removeHeader(name)`
820
+
821
+ `removeHeader(name: string): ResponseHelper`
822
+
823
+ Removes a response header. Chainable.
824
+
825
+ ```ts
826
+ response.removeHeader("x-powered-by");
827
+ ```
828
+
829
+ #### `response.cookie(name, value, options?)`
830
+
831
+ `cookie(name: string, value: string, options?: CookieOptions): ResponseHelper`
832
+
833
+ Queues a `Set-Cookie`. Chainable.
834
+
835
+ ```ts
836
+ response.cookie("session", token, { httpOnly: true, maxAge: 3600 });
837
+ ```
838
+
839
+ **Notes:** `options` is Hono's cookie option bag (`httpOnly`, `secure`,
840
+ `sameSite`, `maxAge`, `path`, `domain`, …).
841
+
842
+ #### `response.clearCookie(name, options?)`
843
+
844
+ `clearCookie(name: string, options?: CookieOptions): ResponseHelper`
845
+
846
+ Clears a cookie (queues an expired `Set-Cookie`). Chainable.
847
+
848
+ ```ts
849
+ response.clearCookie("session");
850
+ ```
851
+
852
+ **Notes:** pass the same `path`/`domain` you set the cookie with, or the browser
853
+ won't match it.
854
+
855
+ #### `response.abort(message, status?)`
856
+
857
+ `abort(message: string, status?: number): never`
858
+
859
+ Throws an `HttpException` to end the request.
860
+
861
+ ```ts
862
+ response.abort("Not found", 404);
863
+ ```
864
+
865
+ **Notes:** default status `400`. Return type is `never`, so TypeScript treats
866
+ everything after it as unreachable. Rendered by the kernel (see
867
+ [Errors](./errors.md)).
868
+
869
+ #### `response.abortIf(condition, message, status?)`
870
+
871
+ `abortIf(condition: unknown, message: string, status?: number): void`
872
+
873
+ Aborts only if `condition` is truthy.
874
+
875
+ ```ts
876
+ response.abortIf(!user, "Not found", 404);
877
+ ```
878
+
879
+ **Notes:** default status `400`. Doesn't narrow types (return is `void`, not a
880
+ type guard).
881
+
882
+ #### `response.abortUnless(condition, message, status?)`
883
+
884
+ `abortUnless(condition: unknown, message: string, status?: number): void`
885
+
886
+ Aborts unless `condition` is truthy.
887
+
888
+ ```ts
889
+ response.abortUnless(user?.isAdmin, "Forbidden", 403);
890
+ ```
891
+
892
+ **Notes:** default status `400`.
893
+
894
+ ### Standalone shortcuts
895
+
896
+ Flat helpers for terse handlers — they resolve the same request as `request`.
897
+
898
+ #### `param(name?)`
899
+
900
+ `param(): Record<string, string>`
901
+ `param(name: string): string`
902
+
903
+ One route parameter (typed `string`) or all of them.
904
+
905
+ ```ts
906
+ import { param } from "@shaferllc/keel/core";
907
+
908
+ param("id"); // string
909
+ param(); // Record<string, string>
910
+ ```
911
+
912
+ **Notes:** overloaded, so `param("id")` is precisely `string` — unlike
913
+ `request.param`, which returns the union.
914
+
915
+ #### `query(name?)`
916
+
917
+ `query(): Record<string, string>`
918
+ `query(name: string): string | undefined`
919
+
920
+ One query value or the whole query object.
921
+
922
+ ```ts
923
+ import { query } from "@shaferllc/keel/core";
924
+
925
+ query("q"); // string | undefined
926
+ query(); // Record<string, string>
927
+ ```
928
+
929
+ #### `header(name)`
930
+
931
+ `header(name: string): string | undefined`
932
+
933
+ A single request header.
934
+
935
+ ```ts
936
+ import { header } from "@shaferllc/keel/core";
937
+
938
+ header("authorization");
939
+ ```
940
+
941
+ #### `body<T>()`
942
+
943
+ `body<T = unknown>(): Promise<T>`
944
+
945
+ The parsed JSON body — the standalone twin of `request.json<T>()`.
946
+
947
+ ```ts
948
+ import { body } from "@shaferllc/keel/core";
949
+
950
+ const data = await body<{ email: string }>();
951
+ ```
952
+
953
+ **Notes:** rejects on invalid JSON.