@shaferllc/keel 0.12.0 → 0.35.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 (48) hide show
  1. package/README.md +28 -3
  2. package/dist/core/application.js +9 -0
  3. package/dist/core/auth.d.ts +41 -0
  4. package/dist/core/auth.js +70 -0
  5. package/dist/core/cache.d.ts +41 -0
  6. package/dist/core/cache.js +81 -0
  7. package/dist/core/casts.d.ts +19 -0
  8. package/dist/core/casts.js +69 -0
  9. package/dist/core/crypto.d.ts +24 -0
  10. package/dist/core/crypto.js +97 -0
  11. package/dist/core/database.d.ts +58 -0
  12. package/dist/core/database.js +140 -0
  13. package/dist/core/debug.d.ts +12 -0
  14. package/dist/core/debug.js +55 -0
  15. package/dist/core/events.d.ts +21 -0
  16. package/dist/core/events.js +46 -0
  17. package/dist/core/exceptions.d.ts +10 -1
  18. package/dist/core/exceptions.js +10 -1
  19. package/dist/core/factory.d.ts +84 -0
  20. package/dist/core/factory.js +154 -0
  21. package/dist/core/helpers.d.ts +13 -0
  22. package/dist/core/helpers.js +24 -0
  23. package/dist/core/http/kernel.js +21 -2
  24. package/dist/core/http/router.d.ts +32 -8
  25. package/dist/core/http/router.js +74 -5
  26. package/dist/core/index.d.ts +31 -2
  27. package/dist/core/index.js +17 -1
  28. package/dist/core/logger.d.ts +29 -0
  29. package/dist/core/logger.js +49 -0
  30. package/dist/core/mail.d.ts +94 -0
  31. package/dist/core/mail.js +157 -0
  32. package/dist/core/migrations.d.ts +76 -0
  33. package/dist/core/migrations.js +211 -0
  34. package/dist/core/model.d.ts +75 -0
  35. package/dist/core/model.js +202 -0
  36. package/dist/core/queue.d.ts +73 -0
  37. package/dist/core/queue.js +88 -0
  38. package/dist/core/rate-limit.d.ts +23 -0
  39. package/dist/core/rate-limit.js +50 -0
  40. package/dist/core/relations.d.ts +90 -0
  41. package/dist/core/relations.js +229 -0
  42. package/dist/core/request.d.ts +53 -1
  43. package/dist/core/request.js +187 -0
  44. package/dist/core/session.d.ts +46 -0
  45. package/dist/core/session.js +112 -0
  46. package/dist/core/static.d.ts +27 -0
  47. package/dist/core/static.js +61 -0
  48. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A small queue for background work. Dispatch a `Job` (or a plain function) and
3
+ * a pluggable `QueueDriver` decides when it runs — immediately (the default),
4
+ * held in memory for a worker to drain, or handed to a real broker. The API
5
+ * mirrors the database and mail layers (`setQueue` / `dispatch` are to queues
6
+ * what `setConnection` / `db()` are to the database). The core imports no broker,
7
+ * so it stays edge-safe.
8
+ *
9
+ * class SendWelcome extends Job {
10
+ * constructor(private userId: number) { super(); }
11
+ * async handle() { await mail().to(...).send(); }
12
+ * }
13
+ *
14
+ * await dispatch(new SendWelcome(user.id)); // runs now with the sync driver
15
+ *
16
+ * // Defer instead, and drain with a worker:
17
+ * setQueue(new MemoryDriver());
18
+ * await dispatch(new SendWelcome(user.id)); // queued
19
+ * await work(); // runs everything pending
20
+ */
21
+ /** A unit of background work. Subclass and implement `handle`. */
22
+ export class Job {
23
+ }
24
+ function run(job) {
25
+ return Promise.resolve(job instanceof Job ? job.handle() : job());
26
+ }
27
+ /* -------------------------------- drivers --------------------------------- */
28
+ /** Runs jobs the moment they're dispatched — the default; ideal for dev/tests. */
29
+ export class SyncDriver {
30
+ async push(job, _options) {
31
+ await run(job);
32
+ }
33
+ }
34
+ /** Holds jobs in memory; `work()` drains them. Assert on `.jobs` in tests. */
35
+ export class MemoryDriver {
36
+ jobs = [];
37
+ async push(job, options) {
38
+ this.jobs.push({ job, options });
39
+ }
40
+ get size() {
41
+ return this.jobs.length;
42
+ }
43
+ /** Run every pending job in FIFO order; returns how many ran. */
44
+ async work() {
45
+ let count = 0;
46
+ while (this.jobs.length) {
47
+ const next = this.jobs.shift();
48
+ await run(next.job);
49
+ count++;
50
+ }
51
+ return count;
52
+ }
53
+ }
54
+ /* --------------------------------- queue ---------------------------------- */
55
+ export class Queue {
56
+ driver;
57
+ constructor(driver) {
58
+ this.driver = driver;
59
+ }
60
+ /** Place a job on the queue (the driver decides when it runs). */
61
+ async dispatch(job, options = {}) {
62
+ await this.driver.push(job, options);
63
+ }
64
+ /** Drain the driver if it holds jobs locally; returns how many ran. */
65
+ async work() {
66
+ const drainable = this.driver;
67
+ return typeof drainable.work === "function" ? drainable.work() : 0;
68
+ }
69
+ }
70
+ /* --------------------------------- global --------------------------------- */
71
+ let queue = new Queue(new SyncDriver());
72
+ /** Register the default queue driver used by `dispatch()`. */
73
+ export function setQueue(driver) {
74
+ queue = new Queue(driver);
75
+ return queue;
76
+ }
77
+ /** The default queue instance. */
78
+ export function getQueue() {
79
+ return queue;
80
+ }
81
+ /** Dispatch a job (or function) onto the default queue. */
82
+ export function dispatch(job, options) {
83
+ return queue.dispatch(job, options);
84
+ }
85
+ /** Drain the default queue's pending jobs (no-op for immediate drivers). */
86
+ export function work() {
87
+ return queue.work();
88
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A fixed-window rate-limiting middleware. Limits requests per key (the client
3
+ * IP by default) within a time window, and sets the standard `X-RateLimit-*`
4
+ * and `Retry-After` headers.
5
+ *
6
+ * this.use(rateLimiter({ max: 60, window: 60 })); // 60 req / minute
7
+ * router.post("/login", handler).use(rateLimiter({ max: 5, window: 60 }));
8
+ *
9
+ * The default store is in-memory (per process / per isolate). For distributed
10
+ * limiting, pass a store backed by Redis/KV.
11
+ */
12
+ import type { Context, MiddlewareHandler } from "hono";
13
+ export interface RateLimiterOptions {
14
+ /** Max requests allowed per window. Default: 60. */
15
+ max?: number;
16
+ /** Window length in seconds. Default: 60. */
17
+ window?: number;
18
+ /** Derive the bucket key from the request. Default: client IP. */
19
+ key?: (c: Context) => string;
20
+ /** Response message on 429. */
21
+ message?: string;
22
+ }
23
+ export declare function rateLimiter(options?: RateLimiterOptions): MiddlewareHandler;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A fixed-window rate-limiting middleware. Limits requests per key (the client
3
+ * IP by default) within a time window, and sets the standard `X-RateLimit-*`
4
+ * and `Retry-After` headers.
5
+ *
6
+ * this.use(rateLimiter({ max: 60, window: 60 })); // 60 req / minute
7
+ * router.post("/login", handler).use(rateLimiter({ max: 5, window: 60 }));
8
+ *
9
+ * The default store is in-memory (per process / per isolate). For distributed
10
+ * limiting, pass a store backed by Redis/KV.
11
+ */
12
+ function defaultKey(c) {
13
+ return (c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
14
+ c.req.header("x-real-ip") ??
15
+ "global");
16
+ }
17
+ export function rateLimiter(options = {}) {
18
+ const max = options.max ?? 60;
19
+ const windowMs = (options.window ?? 60) * 1000;
20
+ const keyFor = options.key ?? defaultKey;
21
+ const buckets = new Map();
22
+ return async (c, next) => {
23
+ const now = Date.now();
24
+ const key = keyFor(c);
25
+ // Occasionally prune expired buckets to bound memory.
26
+ if (buckets.size > 10_000) {
27
+ for (const [k, b] of buckets)
28
+ if (b.reset <= now)
29
+ buckets.delete(k);
30
+ }
31
+ let bucket = buckets.get(key);
32
+ if (!bucket || bucket.reset <= now) {
33
+ bucket = { count: 0, reset: now + windowMs };
34
+ buckets.set(key, bucket);
35
+ }
36
+ bucket.count++;
37
+ const remaining = Math.max(0, max - bucket.count);
38
+ if (bucket.count > max) {
39
+ return c.json({ error: options.message ?? "Too Many Requests", status: 429 }, 429, {
40
+ "X-RateLimit-Limit": String(max),
41
+ "X-RateLimit-Remaining": "0",
42
+ "Retry-After": String(Math.ceil((bucket.reset - now) / 1000)),
43
+ });
44
+ }
45
+ await next();
46
+ // Set headers on the final response (survives any handler type).
47
+ c.header("X-RateLimit-Limit", String(max));
48
+ c.header("X-RateLimit-Remaining", String(remaining));
49
+ };
50
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Model relationships over the query builder. Define a relationship as a method
3
+ * on your model that returns one of these, then read it lazily (`await`) or
4
+ * eager-load many at once with `Model.load()` to avoid N+1 queries.
5
+ *
6
+ * class User extends Model {
7
+ * static table = "users";
8
+ * posts() { return this.hasMany(Post); } // user_id on posts
9
+ * profile() { return this.hasOne(Profile); }
10
+ * roles() { return this.belongsToMany(Role); } // role_user pivot
11
+ * }
12
+ * class Post extends Model {
13
+ * static table = "posts";
14
+ * author() { return this.belongsTo(User); } // user_id on posts
15
+ * }
16
+ *
17
+ * const posts = await user.posts(); // relations are awaitable
18
+ * const author = await post.author();
19
+ *
20
+ * const users = await User.all();
21
+ * await User.load(users, "posts"); // one extra query, not N
22
+ * users[0].getRelation("posts");
23
+ *
24
+ * Every relation is edge-safe: it only uses the driver-agnostic query builder
25
+ * (belongsToMany runs two `whereIn` reads instead of a JOIN).
26
+ */
27
+ import { type QueryBuilder, type Row } from "./database.js";
28
+ import type { Model } from "./model.js";
29
+ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
30
+ /** Base class: a relationship is awaitable (resolves to its loaded result). */
31
+ export declare abstract class Relation<TRelated extends Model, TResult> implements PromiseLike<TResult> {
32
+ protected parent: Model;
33
+ protected related: ModelClass<TRelated>;
34
+ constructor(parent: Model, related: ModelClass<TRelated>);
35
+ /** The query builder with the relationship constraint applied. */
36
+ abstract query(): QueryBuilder;
37
+ /** Load the relationship for this single parent. */
38
+ abstract get(): Promise<TResult>;
39
+ /** Batch-load this relationship onto many parents, then assign each result. */
40
+ abstract eager(models: Model[], name: string): Promise<void>;
41
+ then<R1 = TResult, R2 = never>(onFulfilled?: ((value: TResult) => R1 | PromiseLike<R1>) | null, onRejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): PromiseLike<R1 | R2>;
42
+ protected hydrate(rows: Row[]): TRelated[];
43
+ }
44
+ export declare class HasMany<T extends Model> extends Relation<T, T[]> {
45
+ private foreignKey;
46
+ private localKey;
47
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, localKey: string);
48
+ private localValue;
49
+ query(): QueryBuilder;
50
+ get(): Promise<T[]>;
51
+ eager(models: Model[], name: string): Promise<void>;
52
+ }
53
+ export declare class HasOne<T extends Model> extends Relation<T, T | null> {
54
+ private foreignKey;
55
+ private localKey;
56
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, localKey: string);
57
+ private localValue;
58
+ query(): QueryBuilder;
59
+ get(): Promise<T | null>;
60
+ eager(models: Model[], name: string): Promise<void>;
61
+ }
62
+ export declare class BelongsTo<T extends Model> extends Relation<T, T | null> {
63
+ private foreignKey;
64
+ private ownerKey;
65
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, ownerKey: string);
66
+ private foreignValue;
67
+ query(): QueryBuilder;
68
+ get(): Promise<T | null>;
69
+ eager(models: Model[], name: string): Promise<void>;
70
+ }
71
+ export declare class BelongsToMany<T extends Model> extends Relation<T, T[]> {
72
+ private pivotTable;
73
+ private foreignPivotKey;
74
+ private relatedPivotKey;
75
+ private parentKey;
76
+ private relatedKey;
77
+ constructor(parent: Model, related: ModelClass<T>, pivotTable: string, foreignPivotKey: string, relatedPivotKey: string, parentKey: string, relatedKey: string);
78
+ private parentValue;
79
+ /** The query against the related table, once pivot rows are known. */
80
+ query(): QueryBuilder;
81
+ get(): Promise<T[]>;
82
+ eager(models: Model[], name: string): Promise<void>;
83
+ /** Attach a related row by linking it through the pivot table. */
84
+ attach(id: unknown, extra?: Row): Promise<void>;
85
+ /** Detach one related row (or all, when no id is given). */
86
+ detach(id?: unknown): Promise<void>;
87
+ /** Make the pivot contain exactly `ids` (detach the rest, attach the new). */
88
+ sync(ids: unknown[]): Promise<void>;
89
+ }
90
+ export {};
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Model relationships over the query builder. Define a relationship as a method
3
+ * on your model that returns one of these, then read it lazily (`await`) or
4
+ * eager-load many at once with `Model.load()` to avoid N+1 queries.
5
+ *
6
+ * class User extends Model {
7
+ * static table = "users";
8
+ * posts() { return this.hasMany(Post); } // user_id on posts
9
+ * profile() { return this.hasOne(Profile); }
10
+ * roles() { return this.belongsToMany(Role); } // role_user pivot
11
+ * }
12
+ * class Post extends Model {
13
+ * static table = "posts";
14
+ * author() { return this.belongsTo(User); } // user_id on posts
15
+ * }
16
+ *
17
+ * const posts = await user.posts(); // relations are awaitable
18
+ * const author = await post.author();
19
+ *
20
+ * const users = await User.all();
21
+ * await User.load(users, "posts"); // one extra query, not N
22
+ * users[0].getRelation("posts");
23
+ *
24
+ * Every relation is edge-safe: it only uses the driver-agnostic query builder
25
+ * (belongsToMany runs two `whereIn` reads instead of a JOIN).
26
+ */
27
+ import { db } from "./database.js";
28
+ function unique(values) {
29
+ return [...new Set(values)];
30
+ }
31
+ /** Base class: a relationship is awaitable (resolves to its loaded result). */
32
+ export class Relation {
33
+ parent;
34
+ related;
35
+ constructor(parent, related) {
36
+ this.parent = parent;
37
+ this.related = related;
38
+ }
39
+ then(onFulfilled, onRejected) {
40
+ return this.get().then(onFulfilled, onRejected);
41
+ }
42
+ hydrate(rows) {
43
+ return rows.map((row) => new this.related(row));
44
+ }
45
+ }
46
+ /* --------------------------------- has-many -------------------------------- */
47
+ export class HasMany extends Relation {
48
+ foreignKey;
49
+ localKey;
50
+ constructor(parent, related, foreignKey, localKey) {
51
+ super(parent, related);
52
+ this.foreignKey = foreignKey;
53
+ this.localKey = localKey;
54
+ }
55
+ localValue() {
56
+ return this.parent[this.localKey];
57
+ }
58
+ query() {
59
+ return db(this.related.table).where(this.foreignKey, this.localValue());
60
+ }
61
+ async get() {
62
+ return this.hydrate(await this.query().get());
63
+ }
64
+ async eager(models, name) {
65
+ const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
66
+ const rows = keys.length
67
+ ? await db(this.related.table).whereIn(this.foreignKey, keys).get()
68
+ : [];
69
+ const grouped = new Map();
70
+ for (const row of rows) {
71
+ const bucket = grouped.get(row[this.foreignKey]) ?? [];
72
+ bucket.push(new this.related(row));
73
+ grouped.set(row[this.foreignKey], bucket);
74
+ }
75
+ for (const m of models) {
76
+ m.setRelation(name, grouped.get(m[this.localKey]) ?? []);
77
+ }
78
+ }
79
+ }
80
+ /* --------------------------------- has-one --------------------------------- */
81
+ export class HasOne extends Relation {
82
+ foreignKey;
83
+ localKey;
84
+ constructor(parent, related, foreignKey, localKey) {
85
+ super(parent, related);
86
+ this.foreignKey = foreignKey;
87
+ this.localKey = localKey;
88
+ }
89
+ localValue() {
90
+ return this.parent[this.localKey];
91
+ }
92
+ query() {
93
+ return db(this.related.table).where(this.foreignKey, this.localValue());
94
+ }
95
+ async get() {
96
+ const row = await this.query().first();
97
+ return row ? new this.related(row) : null;
98
+ }
99
+ async eager(models, name) {
100
+ const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
101
+ const rows = keys.length
102
+ ? await db(this.related.table).whereIn(this.foreignKey, keys).get()
103
+ : [];
104
+ const byKey = new Map();
105
+ for (const row of rows) {
106
+ if (!byKey.has(row[this.foreignKey]))
107
+ byKey.set(row[this.foreignKey], new this.related(row));
108
+ }
109
+ for (const m of models) {
110
+ m.setRelation(name, byKey.get(m[this.localKey]) ?? null);
111
+ }
112
+ }
113
+ }
114
+ /* -------------------------------- belongs-to ------------------------------- */
115
+ export class BelongsTo extends Relation {
116
+ foreignKey;
117
+ ownerKey;
118
+ constructor(parent, related, foreignKey, ownerKey) {
119
+ super(parent, related);
120
+ this.foreignKey = foreignKey;
121
+ this.ownerKey = ownerKey;
122
+ }
123
+ foreignValue() {
124
+ return this.parent[this.foreignKey];
125
+ }
126
+ query() {
127
+ return db(this.related.table).where(this.ownerKey, this.foreignValue());
128
+ }
129
+ async get() {
130
+ if (this.foreignValue() == null)
131
+ return null;
132
+ const row = await this.query().first();
133
+ return row ? new this.related(row) : null;
134
+ }
135
+ async eager(models, name) {
136
+ const keys = unique(models.map((m) => m[this.foreignKey]).filter((v) => v != null));
137
+ const rows = keys.length
138
+ ? await db(this.related.table).whereIn(this.ownerKey, keys).get()
139
+ : [];
140
+ const byKey = new Map();
141
+ for (const row of rows)
142
+ byKey.set(row[this.ownerKey], new this.related(row));
143
+ for (const m of models) {
144
+ m.setRelation(name, byKey.get(m[this.foreignKey]) ?? null);
145
+ }
146
+ }
147
+ }
148
+ /* ----------------------------- belongs-to-many ----------------------------- */
149
+ export class BelongsToMany extends Relation {
150
+ pivotTable;
151
+ foreignPivotKey;
152
+ relatedPivotKey;
153
+ parentKey;
154
+ relatedKey;
155
+ constructor(parent, related, pivotTable, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
156
+ super(parent, related);
157
+ this.pivotTable = pivotTable;
158
+ this.foreignPivotKey = foreignPivotKey;
159
+ this.relatedPivotKey = relatedPivotKey;
160
+ this.parentKey = parentKey;
161
+ this.relatedKey = relatedKey;
162
+ }
163
+ parentValue() {
164
+ return this.parent[this.parentKey];
165
+ }
166
+ /** The query against the related table, once pivot rows are known. */
167
+ query() {
168
+ return db(this.related.table);
169
+ }
170
+ async get() {
171
+ if (this.parentValue() == null)
172
+ return [];
173
+ const pivots = await db(this.pivotTable)
174
+ .where(this.foreignPivotKey, this.parentValue())
175
+ .get();
176
+ const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
177
+ if (!relatedIds.length)
178
+ return [];
179
+ const rows = await db(this.related.table).whereIn(this.relatedKey, relatedIds).get();
180
+ return this.hydrate(rows);
181
+ }
182
+ async eager(models, name) {
183
+ const parentIds = unique(models.map((m) => m[this.parentKey]).filter((v) => v != null));
184
+ if (!parentIds.length) {
185
+ for (const m of models)
186
+ m.setRelation(name, []);
187
+ return;
188
+ }
189
+ const pivots = await db(this.pivotTable).whereIn(this.foreignPivotKey, parentIds).get();
190
+ const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
191
+ const rows = relatedIds.length
192
+ ? await db(this.related.table).whereIn(this.relatedKey, relatedIds).get()
193
+ : [];
194
+ const relatedById = new Map(rows.map((row) => [row[this.relatedKey], row]));
195
+ const grouped = new Map();
196
+ for (const pivot of pivots) {
197
+ const row = relatedById.get(pivot[this.relatedPivotKey]);
198
+ if (!row)
199
+ continue;
200
+ const bucket = grouped.get(pivot[this.foreignPivotKey]) ?? [];
201
+ bucket.push(new this.related(row));
202
+ grouped.set(pivot[this.foreignPivotKey], bucket);
203
+ }
204
+ for (const m of models) {
205
+ m.setRelation(name, grouped.get(m[this.parentKey]) ?? []);
206
+ }
207
+ }
208
+ /** Attach a related row by linking it through the pivot table. */
209
+ async attach(id, extra = {}) {
210
+ await db(this.pivotTable).insert({
211
+ [this.foreignPivotKey]: this.parentValue(),
212
+ [this.relatedPivotKey]: id,
213
+ ...extra,
214
+ });
215
+ }
216
+ /** Detach one related row (or all, when no id is given). */
217
+ async detach(id) {
218
+ let q = db(this.pivotTable).where(this.foreignPivotKey, this.parentValue());
219
+ if (id !== undefined)
220
+ q = q.where(this.relatedPivotKey, id);
221
+ await q.delete();
222
+ }
223
+ /** Make the pivot contain exactly `ids` (detach the rest, attach the new). */
224
+ async sync(ids) {
225
+ await this.detach();
226
+ for (const id of ids)
227
+ await this.attach(id);
228
+ }
229
+ }
@@ -11,7 +11,9 @@
11
11
  * These resolve the active context from async-context storage, which the HTTP
12
12
  * kernel enables for every request. They only work inside a request.
13
13
  */
14
+ import { setCookie } from "hono/cookie";
14
15
  import type { Context } from "hono";
16
+ type CookieOptions = Parameters<typeof setCookie>[3];
15
17
  /** The current request context. Throws if called outside a request. */
16
18
  export declare function ctx(): Context;
17
19
  export declare function json(data: unknown, status?: number): Response;
@@ -31,10 +33,28 @@ interface ResponseHelper {
31
33
  text(body: string, status?: number): Response;
32
34
  html(body: string, status?: number): Response;
33
35
  redirect(location: string, status?: number): Response;
36
+ /** Send a value — objects become JSON, everything else becomes text. */
37
+ send(data: unknown, status?: number): Response;
34
38
  /** Set the response status (chainable). */
35
39
  status(code: number): ResponseHelper;
36
40
  /** Set a response header (chainable). */
37
41
  header(name: string, value: string): ResponseHelper;
42
+ /** Set the Content-Type (chainable). */
43
+ type(mime: string): ResponseHelper;
44
+ /** Append a value to a (possibly multi-value) header (chainable). */
45
+ append(name: string, value: string): ResponseHelper;
46
+ /** Remove a response header (chainable). */
47
+ removeHeader(name: string): ResponseHelper;
48
+ /** Queue a Set-Cookie on the response (chainable). */
49
+ cookie(name: string, value: string, options?: CookieOptions): ResponseHelper;
50
+ /** Clear a cookie (chainable). */
51
+ clearCookie(name: string, options?: CookieOptions): ResponseHelper;
52
+ /** Abort the request with an HTTP exception. */
53
+ abort(message: string, status?: number): never;
54
+ /** Abort only if the condition is truthy. */
55
+ abortIf(condition: unknown, message: string, status?: number): void;
56
+ /** Abort unless the condition is truthy. */
57
+ abortUnless(condition: unknown, message: string, status?: number): void;
38
58
  }
39
59
  export declare const response: ResponseHelper;
40
60
  /**
@@ -60,12 +80,44 @@ export declare const request: {
60
80
  readonly route: {
61
81
  name?: string;
62
82
  pattern: string;
63
- methods: import(".").Method[];
83
+ methods: import("./index.js").Method[];
64
84
  } | undefined;
65
85
  /** Whether the matched route has the given name. */
66
86
  routeIs(name: string): boolean;
67
87
  /** A subdomain parameter captured from a domain-bound route. */
68
88
  subdomain(name: string): string | undefined;
89
+ /** A request cookie by name, or all cookies when called with no argument. */
90
+ cookie(name?: string): string | undefined | Record<string, string>;
91
+ /** The client IP, from X-Forwarded-For / X-Real-IP. */
92
+ ip(): string | undefined;
93
+ /** All inputs — query string merged with the parsed body (async). */
94
+ all(): Promise<Record<string, unknown>>;
95
+ /** An uploaded file by field name (a web `File`), or undefined. */
96
+ file(name: string): Promise<File | undefined>;
97
+ /** All uploaded files for a field name. */
98
+ files(name: string): Promise<File[]>;
99
+ /** Every uploaded file, grouped by field name. */
100
+ allFiles(): Promise<Record<string, File | File[]>>;
101
+ /** True if the request declares a body. */
102
+ hasBody(): boolean;
103
+ /** All request headers as an object. */
104
+ headers(): Record<string, string>;
105
+ /** The full X-Forwarded-For chain (client first). */
106
+ ips(): string[];
107
+ /** The best of the offered content types per the Accept header, or null. */
108
+ accepts(types: string[]): string | null;
109
+ /** Accepted content types, ordered by preference. */
110
+ types(): string[];
111
+ /** The best of the offered languages per Accept-Language, or null. */
112
+ language(languages: string[]): string | null;
113
+ /** Accepted languages, ordered by preference. */
114
+ languages(): string[];
115
+ /** A single input (from query or body), with an optional fallback (async). */
116
+ input<T = unknown>(key: string, fallback?: T): Promise<T>;
117
+ /** Only the named inputs (async). */
118
+ only(keys: string[]): Promise<Record<string, unknown>>;
119
+ /** Every input except the named ones (async). */
120
+ except(keys: string[]): Promise<Record<string, unknown>>;
69
121
  };
70
122
  export declare function param(): Record<string, string>;
71
123
  export declare function param(name: string): string;