@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,134 @@
1
+ // Type-check harness for docs/container.md. Every type-checkable snippet in the
2
+ // guide is exercised here against the real exports, so a renamed method or wrong
3
+ // argument type fails `npm run typecheck:docs`. Compile-only — never executed.
4
+ import {
5
+ Container,
6
+ Application,
7
+ ServiceProvider,
8
+ bind,
9
+ singleton,
10
+ instance,
11
+ make,
12
+ bound,
13
+ alias,
14
+ swap,
15
+ restore,
16
+ type Ctx,
17
+ type Token,
18
+ type Constructor,
19
+ type Factory,
20
+ } from "@shaferllc/keel/core";
21
+
22
+ // Stand-ins for app-defined services referenced in the guide.
23
+ class Mailer {
24
+ constructor(_dep?: unknown) {}
25
+ }
26
+ class ReportService {
27
+ constructor(private app: Container) {}
28
+ }
29
+ class Router {
30
+ constructor(_app?: unknown) {}
31
+ }
32
+ declare const fakeMailer: Mailer;
33
+
34
+ declare const app: Container;
35
+
36
+ export class AppServiceProvider extends ServiceProvider {
37
+ register(): void {
38
+ this.app.bind("clock", () => new Date());
39
+ this.app.singleton(Mailer, (app) => new Mailer(app.make("config")));
40
+ this.app.instance("version", "0.1.0");
41
+ }
42
+ }
43
+
44
+ export function globalHelpers() {
45
+ bind("clock", () => new Date());
46
+ singleton(Mailer, (app) => new Mailer(app));
47
+ instance("version", "0.6.0");
48
+
49
+ const mailer = make(Mailer);
50
+ const version = make<string>("version");
51
+ if (bound("clock")) {
52
+ /* … */
53
+ }
54
+ return { mailer, version };
55
+ }
56
+
57
+ export function resolving() {
58
+ const mailer = make(Mailer);
59
+ const version = make<string>("version");
60
+
61
+ const report = app.make(ReportService); // no explicit binding
62
+
63
+ make("nope"); // throws at runtime; type-checks fine
64
+ const clock = bound("clock") ? make<Date>("clock") : new Date();
65
+ return { mailer, version, report, clock };
66
+ }
67
+
68
+ export class InvoiceController {
69
+ constructor(private app: Container) {}
70
+
71
+ index(c: Ctx) {
72
+ const config = this.app.make(Application).config();
73
+ return c.json({ currency: config.get("app.currency", "USD") });
74
+ }
75
+ }
76
+
77
+ export function containerReference() {
78
+ app.bind("clock", () => new Date());
79
+ app.bind(Mailer, (c) => new Mailer(c.make("config")));
80
+ app.singleton(Mailer, (c) => new Mailer(c.make("config")));
81
+ const version = app.instance("version", "0.30.0");
82
+
83
+ const mailer = app.make(Mailer);
84
+ const ver = app.make<string>("version");
85
+ const same = app.get(Mailer);
86
+ const report = app.build(ReportService);
87
+ const isBound = app.bound("clock");
88
+
89
+ return { version, mailer, ver, same, report, isBound };
90
+ }
91
+
92
+ export function helperReference() {
93
+ bind("clock", () => new Date());
94
+ singleton(Mailer, (app) => new Mailer(app));
95
+ const version = instance("version", "0.30.0");
96
+ const mailer = make(Mailer);
97
+ const ver = make<string>("version");
98
+ const isBound = bound("clock");
99
+ return { version, mailer, ver, isBound };
100
+ }
101
+
102
+ export function aliasesAndSwapping() {
103
+ // Guide: Aliases
104
+ alias("router", Router);
105
+ const viaAlias = make(Router);
106
+
107
+ // Guide: Swapping bindings in tests (global-helper forms)
108
+ swap(Mailer, () => fakeMailer);
109
+ make(Mailer);
110
+ restore(Mailer);
111
+ restore();
112
+
113
+ return { viaAlias };
114
+ }
115
+
116
+ export function aliasesAndSwappingContainer() {
117
+ app.singleton(Router, () => new Router(app));
118
+ app.alias("router", Router);
119
+ const same = app.make("router") === app.make(Router);
120
+
121
+ app.swap(Mailer, () => fakeMailer);
122
+ app.make(Mailer);
123
+ app.restore(Mailer);
124
+ app.restore();
125
+ return { same };
126
+ }
127
+
128
+ // Type seams.
129
+ const nameKey: Token<string> = "app.name";
130
+ const svcKey: Token<Mailer> = Mailer;
131
+ const ctor: Constructor<ReportService> = ReportService;
132
+ const mailerFactory: Factory<Mailer> = (app) => new Mailer(app.make("config"));
133
+
134
+ export { nameKey, svcKey, ctor, mailerFactory };
@@ -0,0 +1,86 @@
1
+ // Type-check harness for docs/controllers.md. Every type-checkable snippet in
2
+ // the guide is exercised here against the real exports, so a renamed method or a
3
+ // wrong handler shape fails `npm run typecheck:docs`. Compile-only — never run.
4
+ import {
5
+ json,
6
+ param,
7
+ Container,
8
+ type Ctx,
9
+ type RouteHandler,
10
+ } from "@shaferllc/keel/core";
11
+
12
+ // A stand-in service the DI example resolves.
13
+ class Mailer {
14
+ send() {}
15
+ }
16
+
17
+ // --- Basic controller: methods are actions, take Ctx or nothing ---
18
+ export class PostController {
19
+ index() {
20
+ return json({ posts: [] });
21
+ }
22
+
23
+ // Take the context explicitly…
24
+ show(c: Ctx) {
25
+ return c.json({ id: c.req.param("id") });
26
+ }
27
+
28
+ // …or lean on the ambient helpers and drop the argument.
29
+ edit() {
30
+ return json({ id: param("id") });
31
+ }
32
+ }
33
+
34
+ // --- Dependency injection: constructor receives the container ---
35
+ export class UserController {
36
+ constructor(private app: Container) {}
37
+
38
+ store() {
39
+ const mailer = this.app.make(Mailer);
40
+ return json({ sent: typeof mailer.send });
41
+ }
42
+
43
+ show(c: Ctx) {
44
+ const id = c.req.param("id");
45
+ const q = c.req.query("expand");
46
+ return c.json({ id, expand: q });
47
+ }
48
+ }
49
+
50
+ // --- Single-action controller: define handle() ---
51
+ export class PublishPost {
52
+ handle() {
53
+ return json({ published: true });
54
+ }
55
+ }
56
+
57
+ // --- Resource controller: the seven conventional actions ---
58
+ export class ResourcePostController {
59
+ index(c: Ctx) { return c.json({ action: "index" }); }
60
+ create(c: Ctx) { return c.json({ action: "create" }); }
61
+ store(c: Ctx) { return c.json({ action: "store" }); }
62
+ show(c: Ctx) { return c.json({ action: "show" }); }
63
+ edit(c: Ctx) { return c.json({ action: "edit" }); }
64
+ update(c: Ctx) { return c.json({ action: "update" }); }
65
+ destroy(c: Ctx) { return c.json({ action: "destroy" }); }
66
+ }
67
+
68
+ // --- Default export, for the lazy-loading snippet ---
69
+ export default class ReportController {
70
+ index(c: Ctx) {
71
+ return c.json({ action: "index" });
72
+ }
73
+ }
74
+
75
+ // --- RouteHandler: every arm of the union type-checks ---
76
+ export function handlerForms() {
77
+ const closure: RouteHandler = () => json({ ok: true });
78
+ const staticResp: RouteHandler = json({ status: "ok" });
79
+ const action: RouteHandler = [UserController, "show"];
80
+ const single: RouteHandler = [PublishPost];
81
+ const lazy: RouteHandler = [
82
+ () => Promise.resolve({ default: ReportController }),
83
+ "index",
84
+ ];
85
+ return { closure, staticResp, action, single, lazy };
86
+ }
@@ -0,0 +1,118 @@
1
+ // Type-check harness for docs/database.md. Every snippet in the reference is
2
+ // exercised here against the real exports, so a renamed method or wrong argument
3
+ // type fails `npm run typecheck:docs`. Compile-only — never executed.
4
+ import {
5
+ db,
6
+ setConnection,
7
+ type Connection,
8
+ type WriteResult,
9
+ type Row,
10
+ type Dialect,
11
+ type Operator,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ declare const email: string;
15
+ declare const name: string;
16
+ declare const now: number;
17
+
18
+ export async function connect() {
19
+ const d1 = {} as {
20
+ prepare(sql: string): {
21
+ bind(...b: unknown[]): {
22
+ all(): Promise<{ results: Row[]; meta: { changes: number; last_row_id: number } }>;
23
+ run(): Promise<{ meta: { changes: number; last_row_id: number } }>;
24
+ };
25
+ };
26
+ };
27
+
28
+ const connection: Connection = {
29
+ select: (sql, bindings) => d1.prepare(sql).bind(...bindings).all().then((r) => r.results),
30
+ write: async (sql, bindings) => {
31
+ const r = await d1.prepare(sql).bind(...bindings).run();
32
+ return { rowsAffected: r.meta.changes, insertId: r.meta.last_row_id };
33
+ },
34
+ };
35
+ setConnection(connection, "sqlite");
36
+ setConnection(connection, "postgres");
37
+ }
38
+
39
+ export async function querying() {
40
+ await db("users").where("active", true).orderBy("name").get();
41
+ await db("users").where("id", 1).first();
42
+ await db("users").where("age", ">", 18).count();
43
+ await db("posts").whereIn("id", [1, 2, 3]).get();
44
+ await db("posts").whereNull("deleted_at").limit(20).offset(40).get();
45
+ await db("orders").select("id", "total").where("status", "paid").orWhere("status", "shipped").get();
46
+ }
47
+
48
+ export async function writing() {
49
+ const id = await db("users").insertGetId({ email, name });
50
+ await db("users").where("id", id).update({ name: "Grace" });
51
+ await db("users").where("id", id).delete();
52
+ }
53
+
54
+ export async function typedRows() {
55
+ type User = {
56
+ id: number;
57
+ email: string;
58
+ };
59
+ const user = await db<User>("users").where("id", 1).first();
60
+ const all = await db<User>("users").get();
61
+ return { user, all };
62
+ }
63
+
64
+ export async function reference() {
65
+ db("users");
66
+ db<{ id: number }>("users");
67
+
68
+ db("users").select("id", "email").get();
69
+ db("users").where("active", true);
70
+ db("users").where("age", ">", 18);
71
+ db("users").where("email", "like", "%@example.com");
72
+ db("orders").where("status", "paid").orWhere("status", "shipped").get();
73
+ db("posts").whereIn("id", [1, 2, 3]).get();
74
+ db("posts").whereNull("deleted_at").get();
75
+ db("users").whereNotNull("verified_at").get();
76
+ db("users").orderBy("last_name").orderBy("created_at", "desc").get();
77
+ db("posts").limit(20).offset(40).get();
78
+
79
+ const rows = await db("users").where("active", true).get();
80
+ const first = await db("users").where("email", email).first();
81
+ const active = await db("users").where("active", true).count();
82
+ const taken = await db("users").where("email", email).exists();
83
+
84
+ const result: WriteResult = await db("users").insert({ email, name });
85
+ const newId = await db("users").insertGetId({ email, name });
86
+ const upd: WriteResult = await db("users").where("id", 1).update({ name: "Grace" });
87
+ await db("sessions").where("expires_at", "<", now).delete();
88
+
89
+ return { rows, first, active, taken, result, newId, upd };
90
+ }
91
+
92
+ export async function ormExtras() {
93
+ await db("posts").whereBetween("views", [10, 100]).get();
94
+ await db("posts").whereNotIn("id", [4, 5]).get();
95
+ await db("posts").whereLike("title", "%keel%").get();
96
+ await db("posts").latest().get();
97
+ await db("posts").oldest("published_at").get();
98
+
99
+ const total: number = await db("orders").where("paid", true).sum("total");
100
+ const a: number = await db("orders").avg("total");
101
+ const mn: number = await db("orders").min("total");
102
+ const mx: number = await db("orders").max("total");
103
+ const email = await db("users").where("id", 1).value<string>("email");
104
+ const titles = await db("posts").pluck<string>("title");
105
+ const page = await db("posts").latest().paginate(2, 15);
106
+ const rows = page.data;
107
+ return { total, a, mn, mx, email, titles, page: page.currentPage, last: page.lastPage, rows };
108
+ }
109
+
110
+ // Interface / type seams
111
+ const mock: Connection = {
112
+ select: async () => [{ id: 1 }],
113
+ write: async (): Promise<WriteResult> => ({ rowsAffected: 1, insertId: 1 }),
114
+ };
115
+ const dialect: Dialect = "postgres";
116
+ const op: Operator = "like";
117
+ const row: Row = { id: 1 };
118
+ export { mock, dialect, op, row };
@@ -0,0 +1,41 @@
1
+ // Type-check harness for docs/debugging.md. Every type-checkable snippet in the
2
+ // reference is exercised here against the real exports, so a renamed function or
3
+ // wrong argument type fails `npm run typecheck:docs`. Compile-only — never run.
4
+ import { dump, dd } from "@shaferllc/keel/core";
5
+
6
+ declare const user: { id: number; name: string };
7
+ declare const order: { total: number };
8
+ declare const request: {
9
+ all(): Promise<Record<string, unknown>>;
10
+ headers(): Record<string, string>;
11
+ };
12
+ declare function computeTotal(): number;
13
+ declare function save(): Promise<{ id: number }>;
14
+
15
+ export function dumpBasics() {
16
+ dump(user, order); // logs both, returns `user`
17
+
18
+ const total = dump(computeTotal()); // logs the total AND uses it
19
+ return total;
20
+ }
21
+
22
+ export async function dumpInline() {
23
+ return dump(await save()); // inspect the saved value, still return it
24
+ }
25
+
26
+ export function dumpReference() {
27
+ const first = dump(user, order, request); // returns `user`
28
+ first.name; // typed as the first argument's type
29
+ return first;
30
+ }
31
+
32
+ export async function ddBasics() {
33
+ dd(await request.all(), request.headers());
34
+ // unreachable — dd() throws
35
+ }
36
+
37
+ export async function ddReference() {
38
+ // `dd` returns `never`, so TS treats everything after it as unreachable.
39
+ const value = await request.all();
40
+ dd(value);
41
+ }
@@ -0,0 +1,40 @@
1
+ // Type-check harness for docs/decorators.md. Compile-only — never executed.
2
+ import {
3
+ decorateRequest,
4
+ decorated,
5
+ setRequestValue,
6
+ hasRequestDecorator,
7
+ clearRequestDecorators,
8
+ type RequestResolver,
9
+ } from "@shaferllc/keel/core";
10
+
11
+ type User = { id: number; name: string };
12
+ declare function findUser(auth: string | undefined): Promise<User | null>;
13
+
14
+ export function registering() {
15
+ decorateRequest("locale", (c) => c.req.header("accept-language") ?? "en");
16
+ decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
17
+ decorateRequest("tenant", (c) => c.req.header("x-tenant") ?? "public");
18
+ }
19
+
20
+ export async function accessing() {
21
+ const locale = await decorated<string>("locale");
22
+ const user = await decorated<User | null>("user");
23
+ const tenant = await decorated<string>("tenant");
24
+ return { locale, user, tenant };
25
+ }
26
+
27
+ export async function setting(theUser: User) {
28
+ setRequestValue("user", theUser);
29
+ return decorated<User>("user");
30
+ }
31
+
32
+ export function introspection() {
33
+ const has = hasRequestDecorator("user");
34
+ clearRequestDecorators();
35
+ return has;
36
+ }
37
+
38
+ // The resolver type
39
+ const resolver: RequestResolver<string> = (c) => c.req.path;
40
+ export { resolver };
@@ -0,0 +1,121 @@
1
+ // Type-check harness for docs/errors.md. Every type-checkable snippet in the
2
+ // reference is exercised here against the real exports, so a renamed field or a
3
+ // wrong constructor signature fails `npm run typecheck:docs`. Compile-only —
4
+ // never executed.
5
+ import {
6
+ HttpException,
7
+ NotFoundException,
8
+ UnauthorizedException,
9
+ ForbiddenException,
10
+ ValidationException,
11
+ createError,
12
+ STATUS_TEXT,
13
+ HttpKernel,
14
+ Application,
15
+ } from "@shaferllc/keel/core";
16
+ import type { Context } from "hono";
17
+
18
+ // createError --------------------------------------------------------------
19
+ export const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
20
+ export const TenantSuspended = createError("E_TENANT_SUSPENDED", "Tenant %s is suspended.", 403);
21
+ export const RateExceeded = createError("E_RATE", "Slow down.", 429);
22
+
23
+ export function throwCoded(): never {
24
+ const err = new InsufficientFunds("$40");
25
+ const isHttp: boolean = err instanceof HttpException;
26
+ void isHttp;
27
+ throw err;
28
+ }
29
+
30
+ // HTTP exceptions ----------------------------------------------------------
31
+ export function httpExceptions(): never {
32
+ throw new NotFoundException("Widget not found"); // 404
33
+ }
34
+
35
+ export function moreThrows() {
36
+ const throwers: Array<() => never> = [
37
+ () => {
38
+ throw new UnauthorizedException();
39
+ },
40
+ () => {
41
+ throw new ForbiddenException();
42
+ },
43
+ () => {
44
+ throw new HttpException(429, "Slow down");
45
+ },
46
+ () => {
47
+ throw new HttpException(503, "Down for maintenance", { "Retry-After": "120" });
48
+ },
49
+ () => {
50
+ throw new HttpException(429, "Slow down", { "Retry-After": "30" });
51
+ },
52
+ () => {
53
+ throw new HttpException(409, "That email is taken");
54
+ },
55
+ () => {
56
+ throw new HttpException(429); // message defaults to "Too Many Requests"
57
+ },
58
+ ];
59
+ return throwers;
60
+ }
61
+
62
+ // Validation ---------------------------------------------------------------
63
+ export function validation(): never {
64
+ throw new ValidationException({
65
+ email: ["The email is invalid."],
66
+ password: ["Too short.", "Must contain a number."],
67
+ });
68
+ }
69
+
70
+ export function readsErrors() {
71
+ const ex = new ValidationException({ email: ["nope"] });
72
+ const fields: Record<string, string[]> = ex.errors;
73
+ return fields;
74
+ }
75
+
76
+ // Fields the kernel reads --------------------------------------------------
77
+ export function fields() {
78
+ const ex = new HttpException(503, "Down", { "Retry-After": "120" });
79
+ const status: number = ex.status;
80
+ const headers: Record<string, string> | undefined = ex.headers;
81
+ ex.code = "E_DOWN";
82
+ const code: string | undefined = ex.code;
83
+ return { status, headers, code };
84
+ }
85
+
86
+ // Custom exception with self-render / self-report --------------------------
87
+ declare const metrics: { increment(name: string): void };
88
+
89
+ export class PaymentRequiredException extends HttpException {
90
+ code = "E_PAYMENT_REQUIRED";
91
+
92
+ constructor() {
93
+ super(402, "Payment is required to continue.");
94
+ }
95
+
96
+ handle(c: Context) {
97
+ // hono's `json` wants a status-code literal; the exception's numeric
98
+ // `status` is asserted to it (402 here).
99
+ return c.json({ error: this.message, code: this.code, upgrade: "/billing" }, this.status as 402);
100
+ }
101
+
102
+ report() {
103
+ metrics.increment("payment_required");
104
+ }
105
+ }
106
+
107
+ // Custom error handler on the kernel ---------------------------------------
108
+ export class Kernel extends HttpKernel {
109
+ constructor(app: Application) {
110
+ super(app);
111
+ this.onError((_err, c) => c.json({ oops: true }, 500));
112
+ }
113
+ }
114
+
115
+ // STATUS_TEXT --------------------------------------------------------------
116
+ export function statusText() {
117
+ const notFound: string | undefined = STATUS_TEXT[404]; // "Not Found"
118
+ const expired: string | undefined = STATUS_TEXT[419]; // "Page Expired"
119
+ const fallback: string = STATUS_TEXT[418] ?? "Error"; // not in the map
120
+ return { notFound, expired, fallback };
121
+ }