@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,155 @@
1
+ # Rate Limiting
2
+
3
+ `rateLimiter()` is a middleware that caps how many requests a client can make in
4
+ a window. It sets the standard `X-RateLimit-*` and `Retry-After` headers, and
5
+ returns `429 Too Many Requests` when the limit is exceeded.
6
+
7
+ It's a **fixed-window** limiter: each key gets a bucket that counts requests
8
+ until a fixed reset time, then starts over. Simple, cheap, and edge-safe — the
9
+ default store is a plain in-memory `Map`, so nothing is imported that can't run
10
+ on a Worker.
11
+
12
+ ## Global or per-route
13
+
14
+ ```ts
15
+ import { rateLimiter } from "@shaferllc/keel/core";
16
+
17
+ // every request: 60 per minute per IP
18
+ export class Kernel extends HttpKernel {
19
+ constructor(app: Application) {
20
+ super(app);
21
+ this.use(rateLimiter({ max: 60, window: 60 }));
22
+ }
23
+ }
24
+
25
+ // a stricter limit on a sensitive route
26
+ router.post("/login", [AuthController, "login"]).use(rateLimiter({ max: 5, window: 60 }));
27
+ ```
28
+
29
+ Each call to `rateLimiter()` owns its own bucket store, so a global limiter and a
30
+ per-route limiter count independently — a request to `/login` ticks both, but in
31
+ separate buckets. Stack as many as you like.
32
+
33
+ ## Options
34
+
35
+ ```ts
36
+ rateLimiter({
37
+ max: 60, // requests per window (default 60)
38
+ window: 60, // window seconds (default 60)
39
+ key: (c) => c.req.header("x-api-key") ?? "anon", // bucket key (default: IP)
40
+ message: "Slow down!", // 429 body message
41
+ });
42
+ ```
43
+
44
+ The `key` function decides what to limit by — per IP (default), per API key, per
45
+ user id, etc. Different keys get independent buckets. Return the same string to
46
+ share a bucket; return `"global"` (or any constant) to limit everyone together.
47
+
48
+ ### The default key
49
+
50
+ With no `key`, the bucket is derived from the client IP, tried in this order:
51
+
52
+ 1. the first entry of `X-Forwarded-For` (trimmed),
53
+ 2. `X-Real-IP`,
54
+ 3. the literal `"global"` if neither header is present.
55
+
56
+ That fallback means that behind a proxy that strips those headers, *every* client
57
+ shares the `"global"` bucket — so set an explicit `key` if your platform doesn't
58
+ surface the client IP.
59
+
60
+ ## Response headers
61
+
62
+ Every response carries:
63
+
64
+ | Header | Meaning |
65
+ |--------|---------|
66
+ | `X-RateLimit-Limit` | the ceiling for the window |
67
+ | `X-RateLimit-Remaining` | requests left in the window |
68
+ | `Retry-After` | (on 429) seconds until the window resets |
69
+
70
+ On an allowed request the two `X-RateLimit-*` headers are written *after* the
71
+ handler runs. On a rejected request all three are set on the `429` itself.
72
+
73
+ ## What happens at the limit
74
+
75
+ The counter increments on every request. The request that pushes the count
76
+ *past* `max` is the one that's rejected — so `max: 5` lets five requests through
77
+ and blocks the sixth within the window. The `429` body is:
78
+
79
+ ```json
80
+ { "error": "Too Many Requests", "status": 429 }
81
+ ```
82
+
83
+ `error` is your `message` if you passed one. `Retry-After` reports the whole
84
+ seconds until the bucket resets. Once the window elapses the next request starts
85
+ a fresh bucket at count 1.
86
+
87
+ ## Storage
88
+
89
+ The default store is **in-memory** — per process, and per isolate on the edge.
90
+ That's fine for a single instance, but limits aren't shared across instances.
91
+ The store self-prunes: once it holds more than 10,000 keys it sweeps expired
92
+ buckets on the next request, so a large key space (e.g. per-IP) won't grow
93
+ unbounded.
94
+
95
+ For distributed limiting, wrap your own middleware that counts in Redis/KV and
96
+ returns `429` the same way.
97
+
98
+ ---
99
+
100
+ ## API reference
101
+
102
+ ### `rateLimiter(options?)`
103
+
104
+ `rateLimiter(options?: RateLimiterOptions): MiddlewareHandler`
105
+
106
+ Builds a fixed-window rate-limiting middleware. Each returned handler keeps its
107
+ own private bucket store, counts requests per key within the window, sets the
108
+ `X-RateLimit-*` headers, and short-circuits with `429` past the limit.
109
+
110
+ ```ts
111
+ import { rateLimiter } from "@shaferllc/keel/core";
112
+
113
+ const limit = rateLimiter({ max: 100, window: 60 });
114
+ // register it: this.use(limit) globally, or .use(limit) on a route
115
+ ```
116
+
117
+ **Notes:** all options are optional — `rateLimiter()` with no arguments is 60
118
+ requests per 60 seconds, keyed by client IP. The returned handler is a Hono
119
+ `MiddlewareHandler`, so it works anywhere middleware is accepted (`use`, per
120
+ route, per group). Buckets live in a closure over the single call, so reusing the
121
+ *same* handler shares state while a second `rateLimiter(...)` call does not.
122
+ The `429` is returned before `next()`, so downstream handlers never run for a
123
+ throttled request.
124
+
125
+ ### Interfaces & types
126
+
127
+ #### `RateLimiterOptions`
128
+
129
+ ```ts
130
+ interface RateLimiterOptions {
131
+ max?: number; // requests per window; default 60
132
+ window?: number; // window length in seconds; default 60
133
+ key?: (c: Context) => string; // bucket key; default: client IP
134
+ message?: string; // 429 body message; default "Too Many Requests"
135
+ }
136
+ ```
137
+
138
+ The shape you pass to `rateLimiter()`. Every field is optional; `key` receives the
139
+ Hono `Context` and returns the string that identifies the bucket.
140
+
141
+ ```ts
142
+ import { rateLimiter, type RateLimiterOptions } from "@shaferllc/keel/core";
143
+
144
+ const perUser: RateLimiterOptions = {
145
+ max: 30,
146
+ window: 60,
147
+ key: (c) => c.req.header("authorization") ?? "anon",
148
+ message: "Easy there — try again shortly.",
149
+ };
150
+ rateLimiter(perUser);
151
+ ```
152
+
153
+ **Notes:** `window` is **seconds**, not milliseconds (it's multiplied by 1000
154
+ internally). `key` is called on every request, so keep it cheap and pure. Returning
155
+ a constant string collapses all clients into one shared bucket.
package/docs/redis.md ADDED
@@ -0,0 +1,178 @@
1
+ # Redis
2
+
3
+ A Redis integration built on a small pluggable driver — like the database and
4
+ mail layers, the core imports no client, so it runs on Node and on the edge.
5
+ Point it at Upstash (HTTP/`fetch`), ioredis, node-redis, or the built-in
6
+ `MemoryRedis` for tests and local dev.
7
+
8
+ ## Using it
9
+
10
+ Register a driver once (in a service provider), then reach Redis anywhere with
11
+ `redis()`:
12
+
13
+ ```ts
14
+ import { redis, setRedis, MemoryRedis } from "@shaferllc/keel/core";
15
+
16
+ setRedis(new MemoryRedis()); // swap for an Upstash / ioredis adapter in production
17
+
18
+ await redis().set("views", "1");
19
+ await redis().incr("views"); // 2
20
+ await redis().get("views"); // "2"
21
+ await redis().set("token", "abc", { ex: 60 }); // expire in 60s
22
+ await redis().del("token");
23
+ ```
24
+
25
+ The default client is a `MemoryRedis`, so `redis()` works out of the box in
26
+ tests without any setup.
27
+
28
+ ## Commands
29
+
30
+ ```ts
31
+ const r = redis();
32
+
33
+ await r.get(key); // string | null
34
+ await r.set(key, value, { ex }); // { ex: seconds } or { px: ms }
35
+ await r.del(...keys); // number removed
36
+ await r.exists(...keys); // number present
37
+ await r.has(key); // boolean
38
+
39
+ await r.incr(key); // +1
40
+ await r.decr(key); // -1
41
+ await r.incrBy(key, 5);
42
+
43
+ await r.expire(key, 60); // set a TTL (seconds)
44
+ await r.ttl(key); // seconds left, -1 (no expiry), -2 (no key)
45
+ await r.keys("user:*"); // glob match
46
+ await r.flushAll(); // clear everything
47
+ ```
48
+
49
+ ### JSON & remember
50
+
51
+ `getJson` / `setJson` handle serialization, and `remember` is the read-through
52
+ cache pattern:
53
+
54
+ ```ts
55
+ await redis().setJson("user:1", { id: 1, name: "Ada" });
56
+ const user = await redis().getJson<{ id: number; name: string }>("user:1");
57
+
58
+ // Compute once, cache for 300s, serve from cache after:
59
+ const stats = await redis().remember("stats", 300, () => computeStats());
60
+ ```
61
+
62
+ ## As a cache store
63
+
64
+ `redisStore()` adapts the Redis client into a [`CacheStore`](./cache.md), so the
65
+ cache can be Redis-backed — shared across instances instead of per-process:
66
+
67
+ ```ts
68
+ import { Cache, redisStore, redis } from "@shaferllc/keel/core";
69
+
70
+ const cache = new Cache(redisStore(redis()));
71
+ await cache.remember("home", 60, () => renderHome());
72
+ ```
73
+
74
+ ## Writing a driver
75
+
76
+ A driver is the `RedisConnection` interface. Here's the shape for an Upstash
77
+ REST client over `fetch` (edge-safe):
78
+
79
+ ```ts
80
+ import type { RedisConnection } from "@shaferllc/keel/core";
81
+
82
+ const upstash = (url: string, token: string): RedisConnection => {
83
+ const call = (...args: (string | number)[]) =>
84
+ fetch(url, {
85
+ method: "POST",
86
+ headers: { Authorization: `Bearer ${token}` },
87
+ body: JSON.stringify(args),
88
+ }).then((r) => r.json());
89
+ return {
90
+ async get(key) { return (await call("GET", key)).result ?? null; },
91
+ async set(key, value, o) {
92
+ await (o?.ex ? call("SET", key, value, "EX", o.ex) : call("SET", key, value));
93
+ },
94
+ async del(...keys) { return (await call("DEL", ...keys)).result; },
95
+ async exists(...keys) { return (await call("EXISTS", ...keys)).result; },
96
+ async incrBy(key, n) { return (await call("INCRBY", key, n)).result; },
97
+ async expire(key, s) { return (await call("EXPIRE", key, s)).result === 1; },
98
+ async ttl(key) { return (await call("TTL", key)).result; },
99
+ async keys(pattern) { return (await call("KEYS", pattern)).result; },
100
+ async flushAll() { await call("FLUSHALL"); },
101
+ };
102
+ };
103
+ setRedis(upstash(env("UPSTASH_URL"), env("UPSTASH_TOKEN")));
104
+ ```
105
+
106
+ ## In tests
107
+
108
+ `MemoryRedis` is a full in-memory implementation with TTL support — no server:
109
+
110
+ ```ts
111
+ import { setRedis, MemoryRedis, redis } from "@shaferllc/keel/core";
112
+
113
+ setRedis(new MemoryRedis());
114
+ await redis().incr("signups");
115
+ assert.equal(await redis().get("signups"), "1");
116
+ ```
117
+
118
+ ## API reference
119
+
120
+ ### `redis()`
121
+
122
+ `redis(): Redis`
123
+
124
+ The default client. Register a driver with `setRedis` first; defaults to a
125
+ `MemoryRedis`.
126
+
127
+ ### `setRedis(conn)`
128
+
129
+ `setRedis(conn: RedisConnection): Redis`
130
+
131
+ Registers the driver behind `redis()` and returns the wrapping client. Last call
132
+ wins.
133
+
134
+ ### `Redis`
135
+
136
+ Wraps a `RedisConnection` with conveniences.
137
+
138
+ | Method | Signature | Notes |
139
+ |--------|-----------|-------|
140
+ | `get` | `(key) => Promise<string \| null>` | raw string value |
141
+ | `set` | `(key, value, options?) => Promise<void>` | `{ ex: seconds }` / `{ px: ms }` |
142
+ | `del` / `exists` | `(...keys) => Promise<number>` | count removed / present |
143
+ | `has` | `(key) => Promise<boolean>` | `exists(key) > 0` |
144
+ | `incr` / `decr` | `(key) => Promise<number>` | ±1 |
145
+ | `incrBy` | `(key, amount) => Promise<number>` | atomic add |
146
+ | `expire` | `(key, seconds) => Promise<boolean>` | false if the key is gone |
147
+ | `ttl` | `(key) => Promise<number>` | seconds, `-1` no expiry, `-2` no key |
148
+ | `keys` | `(pattern?) => Promise<string[]>` | glob; default `"*"` |
149
+ | `flushAll` | `() => Promise<void>` | clear all |
150
+ | `getJson` / `setJson` | JSON convenience over `get`/`set` | |
151
+ | `remember` | `(key, seconds, factory) => Promise<T>` | read-through cache |
152
+
153
+ ### `MemoryRedis`
154
+
155
+ `class MemoryRedis implements RedisConnection`
156
+
157
+ An in-memory driver with TTL support — the default and ideal for tests. Not
158
+ shared across processes.
159
+
160
+ ### `redisStore(client?)`
161
+
162
+ `redisStore(client?: Redis): CacheStore`
163
+
164
+ Adapts a `Redis` client into a `CacheStore` for the cache layer. Defaults to the
165
+ global `redis()`.
166
+
167
+ ### Interfaces & types
168
+
169
+ #### `RedisConnection`
170
+
171
+ The driver seam — implement it to back Redis with any client. Methods: `get`,
172
+ `set`, `del`, `exists`, `incrBy`, `expire`, `ttl`, `keys`, `flushAll`.
173
+
174
+ #### `SetOptions`
175
+
176
+ `interface SetOptions { ex?: number; px?: number }`
177
+
178
+ Expiry for `set` — `ex` in seconds, `px` in milliseconds.