@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
package/docs/events.md ADDED
@@ -0,0 +1,496 @@
1
+ # Events
2
+
3
+ A tiny event emitter for decoupling — fire an event in one place, handle it in
4
+ another. The emitter is a container singleton, reachable through the global
5
+ `emit()` / `listen()` helpers.
6
+
7
+ ## Listen and emit
8
+
9
+ ```ts
10
+ import { emit, listen } from "@shaferllc/keel/core";
11
+
12
+ // register a listener (usually in a provider's boot())
13
+ listen("user.registered", (user) => {
14
+ sendWelcomeEmail(user);
15
+ });
16
+
17
+ // fire it from anywhere — emit awaits every listener
18
+ await emit("user.registered", user);
19
+ ```
20
+
21
+ `listen()` returns an unsubscribe function:
22
+
23
+ ```ts
24
+ const off = listen("tick", () => {});
25
+ off(); // stop listening
26
+ ```
27
+
28
+ ## Typed payloads
29
+
30
+ Both `emit` and `listen` take a payload type parameter, so the value you fire and
31
+ the value your listener receives line up at compile time:
32
+
33
+ ```ts
34
+ type OrderPaid = { id: number; total: number };
35
+
36
+ listen<OrderPaid>("order.paid", (order) => {
37
+ order.total; // number
38
+ });
39
+
40
+ await emit<OrderPaid>("order.paid", { id: 1, total: 4200 });
41
+ ```
42
+
43
+ This is a convenience for the caller: you're passing the same type on both sides
44
+ by hand, and nothing checks that you did. Get it wrong in one place and the two
45
+ drift apart silently.
46
+
47
+ ## The event registry
48
+
49
+ Declare an event once in `EventsList` and the emitter checks **both** sides of it
50
+ for you — no type argument to remember, and no way for the emitter and the
51
+ listener to disagree:
52
+
53
+ ```ts
54
+ declare module "@shaferllc/keel/core" {
55
+ interface EventsList {
56
+ "order.paid": { id: number; total: number };
57
+ "user.registered": User;
58
+ }
59
+ }
60
+ ```
61
+
62
+ Put that in a `types/events.ts` (anywhere the compiler sees it). From then on:
63
+
64
+ ```ts
65
+ listen("order.paid", (order) => {
66
+ order.total; // number — inferred from the registry
67
+ });
68
+
69
+ await emit("order.paid", { id: 1, total: 4200 }); // ✅
70
+
71
+ await emit("order.paid", { id: 1, total: "4200" }); // ❌ total must be a number
72
+ await emit("order.paid"); // ❌ this event requires a payload
73
+ listen("order.paid", (o: { nope: boolean }) => {}); // ❌ wrong listener payload
74
+ ```
75
+
76
+ Declaring events is **opt-in and incremental**. An event you haven't declared
77
+ behaves exactly as it always has — the payload type comes from the listener or an
78
+ explicit `listen<T>`, and falls back to `unknown` — so you can add entries to
79
+ `EventsList` one at a time.
80
+
81
+ Nothing validates the payload at *runtime*; this is a compile-time contract.
82
+
83
+ ## Where to register listeners
84
+
85
+ Register listeners in a service provider's `boot()` method, so they're wired up
86
+ once when the app starts:
87
+
88
+ ```ts
89
+ export class EventServiceProvider extends ServiceProvider {
90
+ boot(): void {
91
+ listen("order.paid", (order) => this.app.make(Fulfillment).ship(order));
92
+ }
93
+ }
94
+ ```
95
+
96
+ `boot()` runs after every provider has registered, so it's safe to resolve other
97
+ services there. Don't register listeners in `register()` — the services they
98
+ reach may not be bound yet.
99
+
100
+ ## The full API
101
+
102
+ Reach the emitter directly with `events()` for `once`, `off`, and more:
103
+
104
+ ```ts
105
+ import { events } from "@shaferllc/keel/core";
106
+
107
+ events().once("boot", () => {}); // fire once, then auto-unsubscribe
108
+ events().off("tick", listener);
109
+ events().listenerCount("tick");
110
+ events().clear("tick"); // or clear() for everything
111
+ ```
112
+
113
+ ## Ordering and awaiting
114
+
115
+ Listeners for an event run in registration order, and `emit()` **awaits** each
116
+ one before starting the next — so a slow async listener finishes before the
117
+ following listener begins, and before `emit()` resolves:
118
+
119
+ ```ts
120
+ listen("deploy", async () => { await step1(); });
121
+ listen("deploy", async () => { await step2(); }); // runs after step1 settles
122
+
123
+ await emit("deploy"); // resolves only after both steps finish
124
+ ```
125
+
126
+ `emit()` snapshots the listener set before iterating, so a listener that
127
+ subscribes (or unsubscribes) another during the same `emit()` doesn't change who
128
+ runs this time around — the change takes effect on the next emission.
129
+
130
+ ## Error behavior
131
+
132
+ **A listener that throws does not stop the others.** `emit()` runs every listener,
133
+ then reports what broke. That's the whole point of an emitter: the analytics
134
+ listener blowing up shouldn't silently cancel the welcome email.
135
+
136
+ ```ts
137
+ listen("user.registered", () => sendWelcomeEmail(user)); // runs
138
+ listen("user.registered", () => { throw new Error("boom"); }); // throws
139
+ listen("user.registered", () => trackSignup(user)); // still runs
140
+
141
+ try {
142
+ await emit("user.registered", user);
143
+ } catch (err) {
144
+ // every listener ran; err is the failure ("boom")
145
+ }
146
+ ```
147
+
148
+ Failures are never swallowed. With one failed listener `emit()` rejects with that
149
+ error; with several it rejects with an `AggregateError` whose `.errors` holds them
150
+ all.
151
+
152
+ ### Handling failures centrally
153
+
154
+ Register an `onError` handler and `emit()` stops rejecting — each failure goes to
155
+ your handler instead, with the event name and the payload that triggered it. This
156
+ is how you keep a background listener's bug from taking down the request that
157
+ happened to fire the event:
158
+
159
+ ```ts
160
+ events().onError((event, error, payload) => {
161
+ logger().error("listener failed", { event, error, payload });
162
+ });
163
+ ```
164
+
165
+ Firing an event that has no listeners is a no-op — `emit()` resolves immediately.
166
+
167
+ ## Observing every event
168
+
169
+ `onAny` subscribes to *all* events — for logging, metrics, and other
170
+ cross-cutting concerns. It runs before the event's own listeners and returns an
171
+ unsubscribe function:
172
+
173
+ ```ts
174
+ const off = events().onAny((event, payload) => {
175
+ logger().debug(`event: ${event}`, { payload });
176
+ });
177
+ ```
178
+
179
+ ## Testing
180
+
181
+ `events().fake()` records emissions **instead of running listeners**, so a test
182
+ can assert an event fired without triggering its side effects — no welcome email,
183
+ no queued job. It returns a buffer to assert against; `restore()` puts the real
184
+ emitter back.
185
+
186
+ ```ts
187
+ const buffer = events().fake();
188
+
189
+ await registerUser({ email: "a@b.com" });
190
+
191
+ buffer.assertEmitted("user.registered");
192
+ buffer.assertEmitted("order.paid", (o) => o.total === 4200); // with a predicate
193
+ buffer.assertEmittedCount("user.registered", 1);
194
+ buffer.assertNotEmitted("user.deleted");
195
+ buffer.assertNoneEmitted(); // nothing at all fired
196
+
197
+ events().restore();
198
+ ```
199
+
200
+ Pass event names to fake only those — everything else dispatches for real:
201
+
202
+ ```ts
203
+ const buffer = events().fake("user.registered"); // or ["a", "b"]
204
+ ```
205
+
206
+ `buffer.all()` returns every recorded `{ event, payload }` in order, and
207
+ `buffer.payloadsFor("order.paid")` returns just that event's payloads (typed, if
208
+ the event is declared in `EventsList`).
209
+
210
+ ## Notes
211
+
212
+ - A listener is stored once per event (the backing store is a `Set`), so
213
+ registering the exact same function twice for the same event only fires it
214
+ once. Register a fresh function each time if you want it to run twice.
215
+ - Events are in-process. For cross-process or durable events, have a listener
216
+ publish to your queue/broker of choice.
217
+
218
+ ---
219
+
220
+ ## API reference
221
+
222
+ ### `Events`
223
+
224
+ The emitter itself — a container singleton. You rarely construct it; resolve it
225
+ with `events()` (or `app().make(Events)`). Events are keyed by string; each key
226
+ holds a `Set` of listeners.
227
+
228
+ #### `on(event, listener)`
229
+
230
+ `on<T = unknown>(event: string, listener: Listener<T>): () => void`
231
+
232
+ Subscribes `listener` to `event` and returns an unsubscribe function.
233
+
234
+ ```ts
235
+ const off = events().on<{ id: number }>("user.deleted", (u) => audit(u.id));
236
+ off(); // later: stop listening
237
+ ```
238
+
239
+ **Notes:** the returned function removes exactly this listener. The same function
240
+ reference is de-duplicated per event (Set-backed), so subscribing it twice still
241
+ only registers — and fires — it once. The global `listen()` helper is a thin
242
+ wrapper over this.
243
+
244
+ #### `once(event, listener)`
245
+
246
+ `once<T = unknown>(event: string, listener: Listener<T>): () => void`
247
+
248
+ Subscribes for a single emission: the listener runs on the next `emit`, then
249
+ auto-unsubscribes. Also returns an unsubscribe function to cancel it beforehand.
250
+
251
+ ```ts
252
+ events().once("boot", () => console.log("started"));
253
+ ```
254
+
255
+ **Notes:** it unsubscribes itself *before* awaiting your callback, so a listener
256
+ that re-emits the same event won't re-trigger this one. The returned unsubscribe
257
+ removes the internal wrapper, so it works even if the event never fires.
258
+
259
+ #### `off(event, listener)`
260
+
261
+ `off<T = unknown>(event: string, listener: Listener<T>): void`
262
+
263
+ Removes `listener` from `event`. No-op if it wasn't subscribed.
264
+
265
+ ```ts
266
+ events().off("tick", handler);
267
+ ```
268
+
269
+ **Notes:** you must pass the same function reference you subscribed with —
270
+ anonymous inline functions can't be removed this way, so keep a reference (or use
271
+ the unsubscribe function `on`/`once` return). Removing the last listener leaves an
272
+ empty `Set` behind under that key; use `clear(event)` to drop the key entirely.
273
+
274
+ #### `emit(event, payload?)`
275
+
276
+ `emit<T = unknown>(event: string, payload?: T): Promise<void>`
277
+
278
+ Fires `event`, awaiting every listener in registration order with the given
279
+ payload. For an event declared in `EventsList`, the payload is required and
280
+ type-checked against the registry.
281
+
282
+ ```ts
283
+ await events().emit("order.paid", { id: 1, total: 4200 });
284
+ ```
285
+
286
+ **Notes:** listeners run sequentially, each awaited before the next. A listener
287
+ that throws or rejects **does not** skip the rest — they all run, and `emit`
288
+ rejects afterwards with that error (or an `AggregateError` if more than one
289
+ failed), unless an `onError` handler is registered. No listeners means an
290
+ immediate resolve. Both listener sets are snapshotted up front, so subscriptions
291
+ made mid-emit apply only to later emissions.
292
+
293
+ #### `onAny(listener)`
294
+
295
+ `onAny(listener: AnyListener): () => void`
296
+
297
+ Subscribes to every event. The listener receives `(event, payload)`. Returns an
298
+ unsubscribe function.
299
+
300
+ ```ts
301
+ const off = events().onAny((event, payload) => log(event, payload));
302
+ ```
303
+
304
+ **Notes:** any-listeners run *before* the event's own listeners, so a logger sees
305
+ the event even if a listener later throws. They're subject to the same error
306
+ handling as ordinary listeners.
307
+
308
+ #### `onError(handler)`
309
+
310
+ `onError(handler: ErrorHandler): void`
311
+
312
+ Handles listener failures instead of letting `emit` reject. The handler receives
313
+ `(event, error, payload)`.
314
+
315
+ ```ts
316
+ events().onError((event, error) => logger().error("listener failed", { event, error }));
317
+ ```
318
+
319
+ **Notes:** only one handler is active — registering again replaces it. Without
320
+ one, failures surface by rejecting `emit`; they are never silently dropped.
321
+
322
+ #### `fake(only?)` / `restore()`
323
+
324
+ `fake(only?: EventName | EventName[]): EventBuffer` — record emissions instead of
325
+ running listeners, and return a buffer to assert against. `restore()` undoes it.
326
+
327
+ ```ts
328
+ const buffer = events().fake();
329
+ await register(user);
330
+ buffer.assertEmitted("user.registered");
331
+ events().restore();
332
+ ```
333
+
334
+ **Notes:** with no argument every event is faked; pass names to fake only those,
335
+ leaving the rest to dispatch for real. Each `fake()` returns a **fresh** buffer.
336
+
337
+ #### `clearAll()`
338
+
339
+ `clearAll(): void`
340
+
341
+ Drops every listener, every `onAny` listener, and the error handler. `clear()` only
342
+ drops ordinary listeners.
343
+
344
+ #### `listenerCount(event)`
345
+
346
+ `listenerCount(event: string): number`
347
+
348
+ Returns how many listeners are currently subscribed to `event`.
349
+
350
+ ```ts
351
+ if (events().listenerCount("tick") === 0) startClock();
352
+ ```
353
+
354
+ **Notes:** returns `0` for an unknown event.
355
+
356
+ #### `clear(event?)`
357
+
358
+ `clear(event?: string): void`
359
+
360
+ Removes all listeners for `event`, or — with no argument — every listener for
361
+ every event.
362
+
363
+ ```ts
364
+ events().clear("tick"); // drop one event's listeners
365
+ events().clear(); // wipe everything (handy between tests)
366
+ ```
367
+
368
+ **Notes:** unlike `off`, this deletes the event's key outright, so it also cleans
369
+ up the empty-`Set` residue `off` can leave behind.
370
+
371
+ ### Global helpers
372
+
373
+ Convenience functions that resolve the active application's `Events` singleton —
374
+ no need to thread the container around.
375
+
376
+ #### `events()`
377
+
378
+ `events(): Events`
379
+
380
+ Returns the application's `Events` instance.
381
+
382
+ ```ts
383
+ import { events } from "@shaferllc/keel/core";
384
+ events().once("boot", () => {});
385
+ ```
386
+
387
+ **Notes:** resolves `Events` from the active application container, so every call
388
+ returns the same singleton. Throws `No Keel application has been bootstrapped…`
389
+ if no `Application` has been created yet.
390
+
391
+ #### `emit(event, payload?)`
392
+
393
+ `emit<T = unknown>(event: string, payload?: T): Promise<void>`
394
+
395
+ Shorthand for `events().emit(event, payload)` — fire an event from anywhere.
396
+
397
+ ```ts
398
+ import { emit } from "@shaferllc/keel/core";
399
+ await emit("user.registered", user);
400
+ ```
401
+
402
+ **Notes:** same awaiting/error semantics as `Events.emit`. Requires a bootstrapped
403
+ application.
404
+
405
+ #### `listen(event, listener)`
406
+
407
+ `listen<T = unknown>(event: string, listener: Listener<T>): () => void`
408
+
409
+ Shorthand for `events().on(event, listener)` — subscribe from anywhere; returns an
410
+ unsubscribe function.
411
+
412
+ ```ts
413
+ import { listen } from "@shaferllc/keel/core";
414
+ const off = listen<{ id: number }>("user.deleted", (u) => audit(u.id));
415
+ ```
416
+
417
+ **Notes:** wraps `on`, not `once`, so the listener stays subscribed until you call
418
+ the returned function. Requires a bootstrapped application.
419
+
420
+ ### Interfaces & types
421
+
422
+ #### `Listener`
423
+
424
+ `type Listener<T = unknown> = (payload: T) => void | Promise<void>`
425
+
426
+ The shape of an event handler: a function taking the payload, optionally async.
427
+ Use it to type a handler you store or pass around before subscribing.
428
+
429
+ ```ts
430
+ import { type Listener } from "@shaferllc/keel/core";
431
+
432
+ const onPaid: Listener<{ id: number }> = async (order) => {
433
+ await fulfill(order.id);
434
+ };
435
+ listen("order.paid", onPaid);
436
+ ```
437
+
438
+ **Notes:** async listeners are fully awaited by `emit`. A listener that returns a
439
+ rejected promise is treated exactly like a synchronous throw — the other listeners
440
+ still run, and the failure is reported afterwards.
441
+
442
+ #### `EventsList`
443
+
444
+ `interface EventsList {}`
445
+
446
+ The registry of declared events, keyed by name. Empty by default; augment it from
447
+ your app to type an event's payload (see [The event registry](#the-event-registry)).
448
+
449
+ ```ts
450
+ declare module "@shaferllc/keel/core" {
451
+ interface EventsList {
452
+ "order.paid": { id: number; total: number };
453
+ }
454
+ }
455
+ ```
456
+
457
+ #### `EventBuffer`
458
+
459
+ What `fake()` returns. Records every intercepted emission and asserts over them.
460
+
461
+ | Method | Signature |
462
+ |--------|-----------|
463
+ | `assertEmitted` | `(event, predicate?) => void` |
464
+ | `assertNotEmitted` | `(event) => void` |
465
+ | `assertEmittedCount` | `(event, count) => void` |
466
+ | `assertNoneEmitted` | `() => void` |
467
+ | `all` | `() => RecordedEvent[]` |
468
+ | `payloadsFor` | `(event) => PayloadOf<E>[]` |
469
+
470
+ Failed assertions throw with what actually fired.
471
+
472
+ #### `AnyListener`
473
+
474
+ `type AnyListener = (event: string, payload: unknown) => void | Promise<void>`
475
+
476
+ The shape of an `onAny` handler.
477
+
478
+ #### `ErrorHandler`
479
+
480
+ `type ErrorHandler = (event: string, error: unknown, payload: unknown) => void | Promise<void>`
481
+
482
+ The shape of an `onError` handler.
483
+
484
+ #### `RecordedEvent`
485
+
486
+ `interface RecordedEvent { event: string; payload: unknown }`
487
+
488
+ One emission captured by a fake.
489
+
490
+ #### `EventName` / `PayloadOf`
491
+
492
+ `type EventName = keyof EventsList | (string & {})` — a declared event name, or any
493
+ other string.
494
+
495
+ `type PayloadOf<E>` — the declared payload for an event, or `unknown` if it isn't in
496
+ `EventsList`.
@@ -0,0 +1,27 @@
1
+ // Type-check harness for the "application object" section of docs/architecture.md.
2
+ // Compile-only — never executed.
3
+ import { Application, type Configurator } from "@shaferllc/keel/core";
4
+
5
+ declare function sendWelcome(user: { id: number }): void;
6
+ declare function installBilling(app: Application): void;
7
+
8
+ export function configureApp(app: Application): Application {
9
+ const cfg: Configurator = (a) => a.set("mail.from", "hi@keel.dev");
10
+ return app.configure(cfg).configure(installBilling);
11
+ }
12
+
13
+ export function settings(app: Application) {
14
+ app.set("db.url", "sqlite://x");
15
+ const url: unknown = app.get("db.url");
16
+ const name: string = app.get<string>("app.name", "Keel"); // typed fallback
17
+ return { url, name };
18
+ }
19
+
20
+ export async function events(app: Application) {
21
+ const off = app.on<{ id: number }>("user.registered", (user) => sendWelcome(user));
22
+ const offOnce = app.once("tick", () => {});
23
+ await app.emit("user.registered", { id: 1 });
24
+ off();
25
+ offOnce();
26
+ app.off("user.registered", () => {});
27
+ }
@@ -0,0 +1,61 @@
1
+ // Type-check harness for docs/authentication.md. Every type-checkable snippet in
2
+ // the reference is exercised here against the real exports, so a renamed method
3
+ // or wrong argument type fails `npm run typecheck:docs`. Compile-only — never
4
+ // executed.
5
+ import type { MiddlewareHandler } from "hono";
6
+ import {
7
+ Auth,
8
+ auth,
9
+ authGuard,
10
+ setUserProvider,
11
+ type UserProvider,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ // Externals the snippets reference but the auth module doesn't own.
15
+ declare const db: {
16
+ users: { find(id: number | string): Promise<{ id: number; email: string } | null> };
17
+ };
18
+ declare const userId: number;
19
+
20
+ export function provider() {
21
+ setUserProvider((id) => db.users.find(id));
22
+ }
23
+
24
+ export async function readingUser() {
25
+ auth().check();
26
+ auth().guest();
27
+ const uid: string | null = auth().id();
28
+ const anyUser = await auth().user();
29
+
30
+ type User = { id: number; email: string };
31
+ const user = await auth().user<User>(); // User | null
32
+ return { uid, anyUser, user };
33
+ }
34
+
35
+ export function loginLogout() {
36
+ auth().login(userId); // number
37
+ auth().login("42"); // or string
38
+ auth().logout();
39
+ }
40
+
41
+ export function guards() {
42
+ const withRedirect: MiddlewareHandler = authGuard({ redirectTo: "/login" });
43
+ const api: MiddlewareHandler = authGuard(); // 401 JSON, no redirect
44
+ return { withRedirect, api };
45
+ }
46
+
47
+ export function directAuth() {
48
+ if (new Auth().check()) {
49
+ return new Auth().id();
50
+ }
51
+ return null;
52
+ }
53
+
54
+ // Interface / type seam: implement UserProvider, then register it.
55
+ const userProvider: UserProvider = async (id) => {
56
+ // `id` is always a string; coerce if your keys are numeric.
57
+ return db.users.find(Number(id));
58
+ };
59
+ setUserProvider(userProvider);
60
+
61
+ export { userProvider };
@@ -0,0 +1,79 @@
1
+ // Type-check harness for docs/authorization.md. Compile-only — never executed.
2
+ import {
3
+ define,
4
+ policy,
5
+ gateBefore,
6
+ setUserResolver,
7
+ clearAuthorization,
8
+ can,
9
+ cannot,
10
+ canFor,
11
+ authorize,
12
+ authorizeFor,
13
+ param,
14
+ type GateCallback,
15
+ type BeforeCallback,
16
+ } from "@shaferllc/keel/core";
17
+
18
+ type User = { id: number; role?: string; admin?: boolean };
19
+ class Post {
20
+ constructor(
21
+ public authorId: number,
22
+ public published = false,
23
+ ) {}
24
+ }
25
+ declare const post: Post;
26
+ declare const otherUser: User;
27
+
28
+ export function gates() {
29
+ define("update-post", (user, p) => (p as Post).authorId === (user as User).id);
30
+ define("access-admin", (user) => (user as User).role === "admin");
31
+ }
32
+
33
+ export async function checks() {
34
+ if (await can("update-post", post)) {
35
+ // …
36
+ }
37
+ const no: boolean = await cannot("update-post", post);
38
+ await authorize("update-post", post);
39
+ return no;
40
+ }
41
+
42
+ export function policies() {
43
+ class PostPolicy {
44
+ view(user: User, p: Post) {
45
+ return p.published || p.authorId === user.id;
46
+ }
47
+ update(user: User, p: Post) {
48
+ return p.authorId === user.id;
49
+ }
50
+ delete(user: User, p: Post) {
51
+ return Boolean(user.admin) || p.authorId === user.id;
52
+ }
53
+ }
54
+ policy(Post, PostPolicy);
55
+ }
56
+
57
+ export function beforeHook() {
58
+ gateBefore((user) => ((user as User).role === "superadmin" ? true : undefined));
59
+ }
60
+
61
+ export async function forUser() {
62
+ await canFor(otherUser, "update-post", post);
63
+ await authorizeFor(otherUser, "update-post", post);
64
+ }
65
+
66
+ export function resolver() {
67
+ setUserResolver(() => otherUser);
68
+ clearAuthorization();
69
+ }
70
+
71
+ export async function inController() {
72
+ void param("id");
73
+ await authorize("update", post);
74
+ }
75
+
76
+ // The type seams
77
+ const gate: GateCallback = (user, ...args) => Boolean(user) && args.length >= 0;
78
+ const before: BeforeCallback = (user) => (user ? undefined : false);
79
+ export { gate, before };