@shaferllc/keel 0.66.0 → 0.74.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 (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,129 @@
1
+ # Debugging
2
+
3
+ Two helpers for the moments you'd otherwise reach for `console.log`. Both are
4
+ edge-safe — `dump()` is a plain `console.log`, and `dd()` throws a
5
+ self-rendering exception, so neither needs any runtime-specific support.
6
+
7
+ ## dump
8
+
9
+ `dump(...values)` prints to the console and **returns its first argument**, so
10
+ you can drop it inline without restructuring code:
11
+
12
+ ```ts
13
+ import { dump } from "@shaferllc/keel/core";
14
+
15
+ dump(user, order); // logs both, execution continues
16
+
17
+ const total = dump(computeTotal()); // logs the total AND uses it
18
+ ```
19
+
20
+ Every log is prefixed with `⚓ dump →` so your probes are easy to spot (and
21
+ easy to grep out later). Because it returns the first value unchanged, you can
22
+ wrap it around any expression — an argument, a return value, a link in a chain —
23
+ without changing what the code does:
24
+
25
+ ```ts
26
+ return dump(await user.save()); // inspect the saved model, still return it
27
+ ```
28
+
29
+ `dump()` hands your values straight to `console.log`, so the runtime's own
30
+ formatter renders them — objects stay inspectable, not flattened to a string.
31
+ (The safe JSON rendering below is `dd()`'s job, not `dump()`'s.)
32
+
33
+ > `dump()` requires at least one argument — its signature is
34
+ > `(...values: [T, ...unknown[]])`. Calling `dump()` with no arguments is a type
35
+ > error, which stops you from leaving a probe that prints nothing.
36
+
37
+ ## dd — dump and die
38
+
39
+ `dd(...values)` dumps to the **browser** and halts the request — a readable HTML
40
+ page with each value pretty-printed. Perfect for inspecting state mid-request:
41
+
42
+ ```ts
43
+ import { dd } from "@shaferllc/keel/core";
44
+
45
+ store() {
46
+ dd(await request.all(), request.headers());
47
+ // never reached
48
+ }
49
+ ```
50
+
51
+ Its return type is `never`: `dd()` throws, so nothing after it runs and
52
+ TypeScript knows the following code is unreachable. Each value is serialized
53
+ with a **safe** JSON stringifier before it hits the page, so the usual
54
+ `JSON.stringify` hazards don't crash the dump:
55
+
56
+ - **circular references** render as `[Circular]` instead of throwing;
57
+ - **functions** render as `[Function: name]` (or `[Function: anonymous]`);
58
+ - **bigints** render as `123n` instead of throwing;
59
+ - **`undefined`** renders as `[undefined]` instead of vanishing.
60
+
61
+ Values are HTML-escaped before rendering, so dumping a string full of `<`, `>`,
62
+ or `&` shows the literal text rather than injecting markup.
63
+
64
+ Under the hood `dd()` throws a self-rendering exception (see
65
+ [Errors](./errors.md)), so it works the same on Node and the edge — no special
66
+ runtime support needed. The exception carries a **200** status, so the dump page
67
+ returns `200 OK`, not an error status — this is a deliberate inspection tool,
68
+ not an error path.
69
+
70
+ > **Shared references, not just cycles.** The safe stringifier tracks every
71
+ > object it has seen and never forgets one, so the *same* object appearing twice
72
+ > in unrelated places (siblings, not an actual cycle) renders as `[Circular]` on
73
+ > its second appearance. If a dump shows an unexpected `[Circular]`, that's why —
74
+ > the value isn't necessarily cyclic, just repeated.
75
+
76
+ ## Turning on framework debug output
77
+
78
+ Set `APP_DEBUG=true` (i.e. `config('app.debug')`) to get full error pages with
79
+ stack traces from the kernel. Turn it off in production so internals stay hidden.
80
+ See [Errors](./errors.md) for how responses change between debug and production.
81
+
82
+ ---
83
+
84
+ ## API reference
85
+
86
+ Both functions are top-level exports — there are no classes to construct or
87
+ interfaces to implement. The `DumpException` that `dd()` throws is internal; you
88
+ never reference it directly.
89
+
90
+ ### `dump(...values)`
91
+
92
+ `dump<T>(...values: [T, ...unknown[]]): T`
93
+
94
+ Logs all values to the console (prefixed `⚓ dump →`) and returns the first one,
95
+ so it can be dropped inline.
96
+
97
+ ```ts
98
+ import { dump } from "@shaferllc/keel/core";
99
+
100
+ const total = dump(computeTotal()); // logs, then flows the value onward
101
+ dump(user, order, request); // logs all three, returns `user`
102
+ ```
103
+
104
+ **Notes:** requires at least one argument (the tuple type `[T, ...unknown[]]`
105
+ enforces it). Returns `values[0]` unchanged — never a copy — so it's safe to wrap
106
+ around any expression. Uses `console.log` directly, so formatting is the
107
+ runtime's, not the safe stringifier; it does not halt execution.
108
+
109
+ ### `dd(...values)`
110
+
111
+ `dd(...values: unknown[]): never`
112
+
113
+ Dumps every value to a self-rendering HTML page and halts the request — "dump
114
+ and die".
115
+
116
+ ```ts
117
+ import { dd } from "@shaferllc/keel/core";
118
+
119
+ dd(await request.all(), request.headers());
120
+ // unreachable — dd() throws
121
+ ```
122
+
123
+ **Notes:** returns `never` — it throws an internal `DumpException` (a
124
+ self-handling `HttpException`) rather than returning, so any code after it is
125
+ unreachable. The rendered page returns status **200**, not an error code. Values
126
+ are serialized with the safe stringifier (circular refs → `[Circular]`,
127
+ functions → `[Function: …]`, bigints → `123n`, `undefined` → `[undefined]`) and
128
+ HTML-escaped before rendering. Accepts zero or more arguments; `dd()` with no
129
+ arguments still halts the request and renders an empty page.
@@ -0,0 +1,127 @@
1
+ # Request Decorators
2
+
3
+ Attach named, computed values to the current request — `request.user`,
4
+ `request.tenant`, `request.locale` — resolved **lazily** and **memoized for the
5
+ life of the request**. Inspired by [Fastify's decorators](https://fastify.dev/docs/latest/Reference/Decorators/),
6
+ but without the footguns: you register a resolver once, and Keel computes it on
7
+ first access and caches it per request. No null-placeholder declaration, no
8
+ `onRequest` hook to remember, no shared-state leak between requests.
9
+
10
+ > Decorating the **application** is already the [service container's](./container.md)
11
+ > job — `bind` / `singleton` / `instance` / `make`, with `bound()` as
12
+ > `hasDecorator`. Decorators here are the per-*request* counterpart.
13
+
14
+ ## Registering
15
+
16
+ Register decorators once at boot (typically in a service provider). A resolver
17
+ receives the request context and returns a value — sync or async:
18
+
19
+ ```ts
20
+ import { decorateRequest } from "@shaferllc/keel/core";
21
+
22
+ decorateRequest("locale", (c) => c.req.header("accept-language") ?? "en");
23
+ decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
24
+ ```
25
+
26
+ ## Accessing
27
+
28
+ Read a decorator anywhere in the request with `decorated()`. It runs the resolver
29
+ on first access and caches the result for the rest of that request:
30
+
31
+ ```ts
32
+ import { decorated } from "@shaferllc/keel/core";
33
+
34
+ const locale = await decorated<string>("locale");
35
+ const user = await decorated<User | null>("user"); // computed once, then cached
36
+ ```
37
+
38
+ `decorated()` always returns a promise (resolvers may be async). A second request
39
+ starts with a fresh cache — nothing leaks between requests.
40
+
41
+ ## Setting a value directly
42
+
43
+ When something upstream already resolved a value — say an auth middleware — set it
44
+ imperatively so downstream `decorated()` calls skip the resolver:
45
+
46
+ ```ts
47
+ import { setRequestValue, decorated } from "@shaferllc/keel/core";
48
+
49
+ // in middleware, after verifying the session:
50
+ setRequestValue("user", theAuthenticatedUser);
51
+
52
+ // later, in a controller:
53
+ const user = await decorated("user"); // returns the value set above, no re-lookup
54
+ ```
55
+
56
+ This is the clean version of Fastify's "declare a placeholder, set it in an
57
+ `onRequest` hook" pattern.
58
+
59
+ ## Why lazy + memoized
60
+
61
+ Resolving the current user (or tenant, or locale, or a feature-flag set) is the
62
+ kind of thing every handler needs but nothing should compute twice. Registering a
63
+ resolver once and letting the framework memoize it per request means:
64
+
65
+ - handlers that never touch `request.user` never pay for the lookup;
66
+ - handlers that touch it repeatedly pay exactly once;
67
+ - there's no per-request wiring to forget.
68
+
69
+ ## API reference
70
+
71
+ ### `decorateRequest(name, resolver)`
72
+
73
+ `decorateRequest<T>(name: string, resolver: (c: Context) => T | Promise<T>): void`
74
+
75
+ Registers a request decorator. The resolver is called at most once per request,
76
+ on first access.
77
+
78
+ ```ts
79
+ decorateRequest("tenant", (c) => c.req.header("x-tenant") ?? "public");
80
+ ```
81
+
82
+ **Notes:** throws if `name` is already registered (a collision guard). Register at
83
+ boot, not per request.
84
+
85
+ ### `decorated(name)`
86
+
87
+ `decorated<T>(name: string): Promise<T>`
88
+
89
+ The memoized value of a decorator for the current request.
90
+
91
+ ```ts
92
+ const tenant = await decorated<string>("tenant");
93
+ ```
94
+
95
+ **Notes:** computes via the resolver on first access, caches for the rest of the
96
+ request. Throws if `name` was never registered. Must run inside a request (it
97
+ reads the current context).
98
+
99
+ ### `setRequestValue(name, value)`
100
+
101
+ `setRequestValue<T>(name: string, value: T): void`
102
+
103
+ Sets a decorator's value for the current request, overriding the resolver.
104
+
105
+ **Notes:** later `decorated(name)` calls return this value without invoking the
106
+ resolver. Useful from middleware.
107
+
108
+ ### `hasRequestDecorator(name)`
109
+
110
+ `hasRequestDecorator(name: string): boolean`
111
+
112
+ Whether a decorator has been registered.
113
+
114
+ ### `clearRequestDecorators()`
115
+
116
+ `clearRequestDecorators(): void`
117
+
118
+ Unregisters all decorators — a test helper.
119
+
120
+ ### Interfaces & types
121
+
122
+ #### `RequestResolver`
123
+
124
+ `type RequestResolver<T> = (c: Context) => T | Promise<T>`
125
+
126
+ The function registered with `decorateRequest`; receives the Hono `Context` and
127
+ returns the value (sync or async).
package/docs/errors.md ADDED
@@ -0,0 +1,395 @@
1
+ # Errors & Exceptions
2
+
3
+ Throw an exception anywhere — a handler, middleware, or a service deep in the
4
+ container — and Keel's HTTP kernel turns it into the right response. No
5
+ try/catch in every controller.
6
+
7
+ ## HTTP exceptions
8
+
9
+ `HttpException` carries a status code and message. Throw it (or one of its
10
+ subclasses) to short-circuit a request with a specific status:
11
+
12
+ ```ts
13
+ import {
14
+ HttpException,
15
+ NotFoundException,
16
+ UnauthorizedException,
17
+ ForbiddenException,
18
+ } from "@shaferllc/keel/core";
19
+
20
+ throw new NotFoundException("Widget not found"); // 404
21
+ throw new UnauthorizedException(); // 401
22
+ throw new ForbiddenException(); // 403
23
+ throw new HttpException(429, "Slow down"); // any status
24
+ throw new HttpException(503, "Down for maintenance", { "Retry-After": "120" });
25
+ ```
26
+
27
+ ### The full HTTP error family
28
+
29
+ Every common HTTP status has a named subclass with a fixed `status` and a stable
30
+ machine `code`. Each takes an optional message and an optional `data` bag (see
31
+ [below](#attaching-structured-data)):
32
+
33
+ | Class | Status | `code` |
34
+ |-------|--------|--------|
35
+ | `BadRequestException` | 400 | `E_BAD_REQUEST` |
36
+ | `UnauthorizedException` | 401 | `E_UNAUTHORIZED` |
37
+ | `PaymentRequiredException` | 402 | `E_PAYMENT_REQUIRED` |
38
+ | `ForbiddenException` | 403 | `E_FORBIDDEN` |
39
+ | `NotFoundException` | 404 | `E_NOT_FOUND` |
40
+ | `MethodNotAllowedException` | 405 | `E_METHOD_NOT_ALLOWED` |
41
+ | `NotAcceptableException` | 406 | `E_NOT_ACCEPTABLE` |
42
+ | `RequestTimeoutException` | 408 | `E_REQUEST_TIMEOUT` |
43
+ | `ConflictException` | 409 | `E_CONFLICT` |
44
+ | `LengthRequiredException` | 411 | `E_LENGTH_REQUIRED` |
45
+ | `ValidationException` | 422 | `E_VALIDATION` |
46
+ | `TooManyRequestsException` | 429 | `E_TOO_MANY_REQUESTS` |
47
+ | `ServerErrorException` | 500 | `E_SERVER_ERROR` |
48
+ | `NotImplementedException` | 501 | `E_NOT_IMPLEMENTED` |
49
+ | `BadGatewayException` | 502 | `E_BAD_GATEWAY` |
50
+ | `ServiceUnavailableException` | 503 | `E_SERVICE_UNAVAILABLE` |
51
+
52
+ `ValidationException` is special — it takes a per-field error map first (see
53
+ [Validation errors](#validation-errors)); the rest take `(message?, data?)`.
54
+
55
+ ### Attaching structured data
56
+
57
+ Any exception can carry a `data` bag that lands in the JSON body under `data`:
58
+
59
+ ```ts
60
+ throw new ConflictException("Email already registered", { email: "a@b.com" });
61
+ // -> 409 { "error": "Email already registered", "status": 409,
62
+ // "code": "E_CONFLICT", "data": { "email": "a@b.com" } }
63
+ ```
64
+
65
+ Every exception also has a `toJSON()` that returns exactly this body shape
66
+ (`{ error, status, code?, data? }`, plus `errors` for `ValidationException`), so
67
+ you can serialize one yourself outside the HTTP kernel — logging, a queue payload,
68
+ a websocket frame.
69
+
70
+ A controller that always throws can be typed `: never`:
71
+
72
+ ```ts
73
+ show(c: Ctx): never {
74
+ throw new NotFoundException();
75
+ }
76
+ ```
77
+
78
+ The third `headers` argument is emitted on the response — handy for a `503` with
79
+ `Retry-After`, or a `429` with rate-limit headers:
80
+
81
+ ```ts
82
+ throw new HttpException(429, "Slow down", { "Retry-After": "30" });
83
+ ```
84
+
85
+ For terse, inline guards you don't need to construct an exception at all — the
86
+ [request](./request-response.md) object's `abort`, `abortIf`, and `abortUnless`
87
+ helpers throw a plain `HttpException` for you:
88
+
89
+ ```ts
90
+ request.abortUnless(user.isAdmin, "Forbidden", 403);
91
+ ```
92
+
93
+ ## How responses are rendered
94
+
95
+ The kernel negotiates the response by `Accept` and by your `app.debug` config:
96
+
97
+ | Situation | Response |
98
+ |-----------|----------|
99
+ | Client accepts JSON | `{ "error": "...", "status": 404 }` |
100
+ | Client accepts HTML | A rendered error page |
101
+ | `app.debug = true`, unexpected error | Full message + **stack trace** (page + JSON) |
102
+ | `app.debug = false`, unexpected 500 | Generic `Internal Server Error`, internals hidden |
103
+ | Thrown `HttpException` | Its status + message (shown even in production) |
104
+
105
+ Unexpected errors (anything that isn't an `HttpException`) become `500`. In
106
+ production their message and stack are hidden so you never leak internals; the
107
+ intentional message on an `HttpException` is always shown. A subclass `code` and
108
+ any `data` bag are added to the JSON body (`{ error, status, code, data }`), and
109
+ any `headers` you passed are set on the response.
110
+
111
+ The title on both the JSON and HTML paths comes from
112
+ [`STATUS_TEXT`](#status_text) — the kernel looks the status up there (`STATUS_TEXT[status] ?? "Error"`),
113
+ so a custom status still gets a sensible label as long as it's in the map.
114
+
115
+ ## Unmatched routes
116
+
117
+ Any request that doesn't match a route is turned into a `404` automatically —
118
+ same rendering as a thrown `NotFoundException`:
119
+
120
+ ```
121
+ GET /does-not-exist → 404 { "error": "No route for GET /does-not-exist", "status": 404, "code": "E_NOT_FOUND" }
122
+ ```
123
+
124
+ ## The debug error page
125
+
126
+ When `app.debug` is on and the client is a browser, the kernel renders a
127
+ readable error page with the status, message, request line, and a formatted
128
+ stack trace — so you see what broke without digging through logs. Turn debug off
129
+ (via `APP_DEBUG=false`) in production.
130
+
131
+ ## Validation errors
132
+
133
+ `ValidationException` is a `422` that carries per-field messages, which appear in
134
+ the JSON body under `errors`:
135
+
136
+ ```ts
137
+ import { ValidationException } from "@shaferllc/keel/core";
138
+
139
+ throw new ValidationException({ email: ["The email is invalid."] });
140
+ // -> 422 { "error": "The given data was invalid.", "status": 422,
141
+ // "code": "E_VALIDATION", "errors": { "email": ["The email is invalid."] } }
142
+ ```
143
+
144
+ ## Custom exceptions
145
+
146
+ Extend `HttpException` to model your domain errors. Add a `code` (surfaced in the
147
+ JSON body), and optionally make the exception render or report itself:
148
+
149
+ ```ts
150
+ import { HttpException } from "@shaferllc/keel/core";
151
+ import type { Context } from "hono";
152
+
153
+ export class PaymentRequiredException extends HttpException {
154
+ code = "E_PAYMENT_REQUIRED";
155
+
156
+ constructor() {
157
+ super(402, "Payment is required to continue.");
158
+ }
159
+
160
+ // Optional: render this exception itself.
161
+ handle(c: Context) {
162
+ return c.json({ error: this.message, code: this.code, upgrade: "/billing" }, this.status);
163
+ }
164
+
165
+ // Optional: called before rendering — log/report it.
166
+ report() {
167
+ metrics.increment("payment_required");
168
+ }
169
+ }
170
+
171
+ throw new PaymentRequiredException();
172
+ ```
173
+
174
+ - **`code`** → added to the JSON error body (`{ error, status, code }`).
175
+ - **`handle(c)`** → if it returns a `Response`, the kernel uses it verbatim. If it
176
+ returns anything else, the kernel falls back to the default rendering.
177
+ - **`report()`** → always called (and awaited) first; failures there never mask
178
+ the original error.
179
+
180
+ Both hooks are duck-typed, not tied to a base class: the kernel calls any thrown
181
+ value that happens to have a `report` and/or `handle` method. The built-in
182
+ subclasses don't define either — they render through the default path.
183
+
184
+ ## Coded errors with `createError`
185
+
186
+ When all you want is a coded error class — a stable `code`, a message, a status —
187
+ skip the boilerplate and mint one with `createError`. It's the ergonomic shortcut
188
+ for the common case, inspired by Fastify's `@fastify/error`:
189
+
190
+ ```ts
191
+ import { createError } from "@shaferllc/keel/core";
192
+
193
+ const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
194
+
195
+ throw new InsufficientFunds("$40");
196
+ // -> 402 { "error": "Balance too low: need $40", "status": 402, "code": "E_FUNDS" }
197
+ ```
198
+
199
+ `%s` placeholders in the message are filled, in order, from the constructor
200
+ arguments. The result is a real `HttpException` subclass, so it renders through
201
+ the same path (the `code` lands in the JSON body) and passes
202
+ `instanceof HttpException`. Define your app's errors once and throw them anywhere:
203
+
204
+ ```ts
205
+ export const TenantSuspended = createError("E_TENANT_SUSPENDED", "Tenant %s is suspended.", 403);
206
+ export const RateExceeded = createError("E_RATE", "Slow down.", 429);
207
+ ```
208
+
209
+ The **built-in** exceptions carry stable codes too, so `code` shows up without
210
+ any work: `NotFoundException` → `E_NOT_FOUND`, `UnauthorizedException` →
211
+ `E_UNAUTHORIZED`, `ForbiddenException` → `E_FORBIDDEN`, `ValidationException` →
212
+ `E_VALIDATION`.
213
+
214
+ Reach for a hand-written subclass (above) only when you need behavior — a
215
+ `handle(c)` renderer or a `report()` hook. For a plain coded error, `createError`
216
+ is all you need.
217
+
218
+ ## Customizing the handler
219
+
220
+ Override the whole thing from your app's HTTP kernel with `onError()`:
221
+
222
+ ```ts
223
+ // app/Http/Kernel.ts
224
+ export class Kernel extends HttpKernel {
225
+ constructor(app: Application) {
226
+ super(app);
227
+ this.onError((err, c) => {
228
+ // report to your logging service, then render however you like
229
+ return c.json({ oops: true }, 500);
230
+ });
231
+ }
232
+ }
233
+ ```
234
+
235
+ Or override the protected `renderException(err, c)` method to keep the default
236
+ routing but change the presentation.
237
+
238
+ > A registered `onError` handler takes precedence over an exception's own
239
+ > `handle(c)` — the custom handler wins, and self-handling is skipped. `report()`
240
+ > still runs first, either way.
241
+
242
+ ---
243
+
244
+ ## API reference
245
+
246
+ Every exception below is a subclass of `HttpException`, which extends the native
247
+ `Error`. You throw them; you never catch them yourself — the kernel does. All are
248
+ exported from `@shaferllc/keel/core`.
249
+
250
+ ### `HttpException`
251
+
252
+ The base semantic HTTP error: a status code, a message, and optional response
253
+ headers. Throw it directly for any status that doesn't have a dedicated subclass.
254
+
255
+ #### `new HttpException(status, message?, headers?)`
256
+
257
+ `new HttpException(status: number, message?: string, headers?: Record<string, string>): HttpException`
258
+
259
+ Constructs an error carrying `status` and `message`. Omit `message` to fall back
260
+ to the status text.
261
+
262
+ ```ts
263
+ throw new HttpException(409, "That email is taken");
264
+ throw new HttpException(503, "Down for maintenance", { "Retry-After": "120" });
265
+ throw new HttpException(429); // message defaults to "Too Many Requests"
266
+ ```
267
+
268
+ **Notes:** the message defaults to `STATUS_TEXT[status]`, then `"Error"` if the
269
+ status isn't in the map. Exposes three readonly-ish fields the kernel reads:
270
+ `status` (number), `headers` (optional, emitted on the response), and `code`
271
+ (optional `string`, added to the JSON body when set). It also sets `name` to
272
+ `"HttpException"`. There's no built-in `handle`/`report` — add those on a subclass
273
+ to self-render or self-report (see [Custom exceptions](#custom-exceptions)).
274
+
275
+ ### `NotFoundException`
276
+
277
+ A `404`. Thrown automatically for unmatched routes, and by `Model.findOrFail`.
278
+
279
+ #### `new NotFoundException(message?)`
280
+
281
+ `new NotFoundException(message?: string): NotFoundException`
282
+
283
+ Constructs a `404`. Message defaults to `"Not Found"`.
284
+
285
+ ```ts
286
+ throw new NotFoundException(); // "Not Found"
287
+ throw new NotFoundException("Widget 42 not found");
288
+ ```
289
+
290
+ **Notes:** `status` is fixed at `404`; `name` is `"NotFoundException"`. The kernel
291
+ also throws this for any request that matches no route.
292
+
293
+ ### `UnauthorizedException`
294
+
295
+ A `401` — the request isn't authenticated. Reach for it when there's no valid
296
+ session or credentials; use `ForbiddenException` when the user is known but not
297
+ allowed.
298
+
299
+ #### `new UnauthorizedException(message?)`
300
+
301
+ `new UnauthorizedException(message?: string): UnauthorizedException`
302
+
303
+ Constructs a `401`. Message defaults to `"Unauthorized"`.
304
+
305
+ ```ts
306
+ throw new UnauthorizedException();
307
+ throw new UnauthorizedException("Session expired");
308
+ ```
309
+
310
+ **Notes:** `status` is fixed at `401`; `name` is `"UnauthorizedException"`.
311
+
312
+ ### `ForbiddenException`
313
+
314
+ A `403` — the request is authenticated but not permitted.
315
+
316
+ #### `new ForbiddenException(message?)`
317
+
318
+ `new ForbiddenException(message?: string): ForbiddenException`
319
+
320
+ Constructs a `403`. Message defaults to `"Forbidden"`.
321
+
322
+ ```ts
323
+ throw new ForbiddenException();
324
+ throw new ForbiddenException("You can't edit this post");
325
+ ```
326
+
327
+ **Notes:** `status` is fixed at `403`; `name` is `"ForbiddenException"`.
328
+
329
+ ### `ValidationException`
330
+
331
+ A `422` carrying per-field error messages. The kernel adds them to the JSON body
332
+ under `errors`.
333
+
334
+ #### `new ValidationException(errors, message?)`
335
+
336
+ `new ValidationException(errors: Record<string, string[]>, message?: string): ValidationException`
337
+
338
+ Constructs a `422` from a map of field name → messages.
339
+
340
+ ```ts
341
+ throw new ValidationException({
342
+ email: ["The email is invalid."],
343
+ password: ["Too short.", "Must contain a number."],
344
+ });
345
+ // -> 422 { "error": "The given data was invalid.", "status": 422,
346
+ // "code": "E_VALIDATION", "errors": { "email": [...], "password": [...] } }
347
+ ```
348
+
349
+ **Notes:** `status` is fixed at `422`; `name` is `"ValidationException"`. Message
350
+ defaults to `"The given data was invalid."`. The field map is exposed as the
351
+ readonly `errors` property, which the kernel serializes into the response body.
352
+ Keel's [`validate()`](./validation.md) helper throws this for you on a failed
353
+ parse. `code` is `"E_VALIDATION"`.
354
+
355
+ ### `createError(code, message, status?)`
356
+
357
+ `createError(code: string, message: string, status?: number): new (...args: (string | number)[]) => HttpException`
358
+
359
+ Mints a reusable, coded `HttpException` subclass. `message` may contain `%s`
360
+ placeholders, filled in order from the constructor arguments; `status` defaults
361
+ to `500`.
362
+
363
+ ```ts
364
+ const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
365
+ throw new InsufficientFunds("$40");
366
+ // -> 402 { "error": "Balance too low: need $40", "status": 402, "code": "E_FUNDS" }
367
+ ```
368
+
369
+ **Notes:** the returned class extends `HttpException`, so it renders through the
370
+ default path (with `code` in the JSON body) and passes `instanceof HttpException`;
371
+ its `name` is the `code`. A missing argument leaves its `%s` in place. For an
372
+ error that needs a custom `handle(c)`/`report()`, subclass `HttpException`
373
+ directly instead.
374
+
375
+ ### Constants
376
+
377
+ #### `STATUS_TEXT`
378
+
379
+ `const STATUS_TEXT: Record<number, string>`
380
+
381
+ Maps HTTP status codes to their reason phrases. Used to title error pages/bodies
382
+ and to supply the default message for `HttpException`.
383
+
384
+ ```ts
385
+ import { STATUS_TEXT } from "@shaferllc/keel/core";
386
+
387
+ STATUS_TEXT[404]; // "Not Found"
388
+ STATUS_TEXT[419]; // "Page Expired"
389
+ STATUS_TEXT[418] ?? "Error"; // not in the map
390
+ ```
391
+
392
+ **Notes:** covers the statuses Keel uses (`400`, `401`, `403`, `404`, `405`,
393
+ `409`, `419`, `422`, `429`, `500`, `503`). Lookups for anything else are
394
+ `undefined`, which the kernel falls back to `"Error"` for. It's a plain mutable
395
+ object — you can add entries for custom statuses so they render with a label.