@shaferllc/keel 0.66.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,453 @@
1
+ # Authentication
2
+
3
+ Session-based auth built on the pieces you already have: [sessions](./sessions.md)
4
+ hold the login, [hashing](./hashing.md) checks passwords. `auth()` ties them
5
+ together.
6
+
7
+ Requires [`sessionMiddleware()`](./sessions.md) in your HTTP kernel — every
8
+ `auth()` call reaches through the session, so without the middleware the first
9
+ call throws `Session is not available…`.
10
+
11
+ ## Tell Keel how to load a user
12
+
13
+ Register a **user provider** once (in a service provider) — a function that
14
+ returns a user for an id. Keel stays database-agnostic, so this is wherever your
15
+ users live:
16
+
17
+ ```ts
18
+ import { setUserProvider } from "@shaferllc/keel/core";
19
+
20
+ setUserProvider((id) => db.users.find(id));
21
+ ```
22
+
23
+ The id handed to your provider is always a **string** — `login()` normalizes
24
+ whatever you pass (number or string) with `String(id)` before stashing it. If
25
+ your ids are numeric, coerce inside the provider (`db.users.find(Number(id))`).
26
+
27
+ ## Logging in
28
+
29
+ Verify the password yourself with `hash`, then `login()` the user's id:
30
+
31
+ ```ts
32
+ import { auth, hash, response } from "@shaferllc/keel/core";
33
+
34
+ async login() {
35
+ const { email, password } = await request.only(["email", "password"]);
36
+ const user = await db.users.findByEmail(email);
37
+
38
+ if (!user || !(await hash.verify(user.password, password))) {
39
+ return response.abort("Invalid credentials", 401);
40
+ }
41
+
42
+ auth().login(user.id);
43
+ return response.redirect("/dashboard");
44
+ }
45
+ ```
46
+
47
+ `login()` only writes the id to the session — it does no lookup and no password
48
+ check. Verifying credentials is your job (above); `login()` is the "trust this id
49
+ from now on" step.
50
+
51
+ **Verify in constant time.** The snippet above skips the password check when no
52
+ user is found, so a missing account answers faster than a wrong password — a
53
+ timing signal that leaks which emails are registered. Compare against `hash.dummy`
54
+ (a valid hash that never matches) so both paths cost the same:
55
+
56
+ ```ts
57
+ const user = await db.users.findByEmail(email);
58
+ const ok = await hash.verify(user?.password ?? hash.dummy, password);
59
+ if (ok && user) auth().login(user.id); // `user &&` so the dummy never logs anyone in
60
+ ```
61
+
62
+ ## Reading the current user
63
+
64
+ ```ts
65
+ auth().check(); // is someone logged in?
66
+ auth().guest(); // …or not?
67
+ auth().id(); // the user id (string), or null
68
+ await auth().user(); // the full user (via your provider), or null
69
+ ```
70
+
71
+ `user()` reads the id from the session and runs it back through your provider on
72
+ every call — there's no request-level cache, so if you need it twice in one
73
+ handler, hold onto the result. Type the row it returns with the generic:
74
+
75
+ ```ts
76
+ type User = { id: number; email: string };
77
+
78
+ const user = await auth().user<User>(); // User | null
79
+ ```
80
+
81
+ `user()` returns `null` when nobody is logged in. But if someone *is* logged in
82
+ and you never called `setUserProvider`, it throws — Keel has no way to turn the
83
+ id back into a user:
84
+
85
+ ```
86
+ Error: No user provider. Call setUserProvider((id) => findUser(id)).
87
+ ```
88
+
89
+ ## Logging out
90
+
91
+ ```ts
92
+ auth().logout();
93
+ return response.redirect("/");
94
+ ```
95
+
96
+ `logout()` forgets the id from the session; it doesn't destroy the whole session,
97
+ so anything else you flashed or stored survives. Clear the session yourself if
98
+ you want a clean slate on sign-out.
99
+
100
+ ## Protecting routes
101
+
102
+ `authGuard()` rejects unauthenticated requests. Register it as
103
+ [named middleware](./middleware.md) and apply it wherever you need:
104
+
105
+ ```ts
106
+ import { authGuard } from "@shaferllc/keel/core";
107
+
108
+ router.named({ auth: authGuard({ redirectTo: "/login" }) });
109
+
110
+ router.get("/dashboard", [DashboardController, "index"]).use("auth");
111
+ router.group(() => { /* … */ }).use("auth");
112
+ ```
113
+
114
+ Without `redirectTo`, the guard returns `401 Unauthenticated` (ideal for APIs):
115
+
116
+ ```ts
117
+ router.named({ auth: authGuard() }); // 401 JSON on failure, no redirect
118
+ ```
119
+
120
+ The guard only checks that *someone* is logged in — it runs no provider lookup
121
+ and loads no user. It gates on `guest()`, so it's cheap; load the user inside the
122
+ handler with `auth().user()` when you actually need it.
123
+
124
+ ## Token (API) authentication
125
+
126
+ Sessions ride on a cookie — great for a server-rendered app, awkward for an SPA,
127
+ a mobile client, or another service. For those, issue a **stateless bearer
128
+ token**: an HS256 JWT signed with `config('app.key')`, built on the Web Crypto
129
+ API so it works the same on Node and the edge (no `jsonwebtoken`, no native
130
+ bindings). This is the Cloudflare-Workers-friendly path — nothing to store
131
+ server-side.
132
+
133
+ Issue a token in your login handler instead of (or alongside) `auth().login()`:
134
+
135
+ ```ts
136
+ import { jwt, hash, response } from "@shaferllc/keel/core";
137
+
138
+ async login() {
139
+ const { email, password } = await request.only(["email", "password"]);
140
+ const user = await db.users.findByEmail(email);
141
+ if (!user || !(await hash.verify(user.password, password))) {
142
+ return response.abort("Invalid credentials", 401);
143
+ }
144
+
145
+ const token = await jwt.sign({ sub: String(user.id) }, { expiresIn: "1h" });
146
+ return response.json({ token });
147
+ }
148
+ ```
149
+
150
+ Protect API routes with `bearerAuth()`. It reads `Authorization: Bearer <token>`,
151
+ verifies it, and makes the token's `sub` the authenticated id — so `auth()` works
152
+ downstream exactly as it does for sessions, provider lookup and all:
153
+
154
+ ```ts
155
+ import { bearerAuth, auth } from "@shaferllc/keel/core";
156
+
157
+ router.get("/api/me", async () => response.json(await auth().user())).use(bearerAuth());
158
+ ```
159
+
160
+ A missing or invalid token gets `401 Unauthenticated`. Pass `{ optional: true }`
161
+ to let the request through unauthenticated (`auth().check()` is then `false`). A
162
+ token verified this way takes precedence over any session cookie on the same
163
+ request, and — unlike sessions — needs no session store, so `bearerAuth()` works
164
+ without `sessionMiddleware()`.
165
+
166
+ `jwt` is a standalone primitive if you need tokens outside the guard:
167
+
168
+ ```ts
169
+ const token = await jwt.sign({ sub: "42", role: "admin" }, { expiresIn: "7d" });
170
+ const payload = await jwt.verify(token); // { sub, role, iat, exp } | null
171
+ ```
172
+
173
+ `verify()` returns `null` — never throws — for a token that's malformed,
174
+ tampered, expired, not-yet-valid, or fails an `issuer`/`audience` check. Only
175
+ HS256 is accepted: `alg: none` and asymmetric algorithms are refused, closing the
176
+ classic JWT algorithm-confusion hole. `sign()` accepts `expiresIn` (seconds, or a
177
+ duration string like `"30s"`, `"15m"`, `"1h"`, `"7d"`), plus `subject`, `issuer`,
178
+ `audience`, and a `secret` override.
179
+
180
+ ## Opaque access tokens
181
+
182
+ A JWT is stateless — you can't revoke one without extra machinery. When you need
183
+ **revocable, scoped** API tokens (a "personal access tokens" screen, per-token
184
+ abilities, "log out this device"), use the database-backed token store instead. A
185
+ token is a row you can delete, so revocation is instant.
186
+
187
+ Store them in a `personal_access_tokens` table (all timestamps epoch-ms):
188
+
189
+ ```ts
190
+ selector TEXT UNIQUE, hash TEXT, tokenable_id TEXT, name TEXT,
191
+ abilities TEXT, last_used_at INTEGER, expires_at INTEGER, created_at INTEGER
192
+ ```
193
+
194
+ Mint a token after verifying credentials — the plaintext is shown **once**:
195
+
196
+ ```ts
197
+ import { createToken } from "@shaferllc/keel/core";
198
+
199
+ const { token } = await createToken(user.id, {
200
+ abilities: ["posts:read", "posts:write"], // or ["*"] for everything
201
+ expiresIn: "30d", // omit for no expiry
202
+ name: "CLI token",
203
+ });
204
+ return response.json({ token }); // "keel_<selector>.<verifier>"
205
+ ```
206
+
207
+ Protect routes with `tokenAuth()` — it verifies the `Bearer` token, makes its
208
+ owner the authenticated user, and can require abilities:
209
+
210
+ ```ts
211
+ import { tokenAuth, auth, token, tokenCan } from "@shaferllc/keel/core";
212
+
213
+ router.get("/api/posts", async () => response.json(await auth().user()))
214
+ .use(tokenAuth({ abilities: ["posts:read"] }));
215
+
216
+ // inside a handler, inspect the verified token:
217
+ token(); // { tokenableId, abilities, expiresAt, … } | null
218
+ tokenCan("posts:write"); // boolean
219
+ ```
220
+
221
+ The token splits into a public **selector** (indexed, for lookup) and a secret
222
+ **verifier** (stored only as a SHA-256 hash), so a leaked database can't mint
223
+ working tokens — and verification needs no `RETURNING`, so it's portable across
224
+ every driver. Manage tokens with `listTokens(userId)`, `revokeToken(selector)`,
225
+ and `revokeTokens(userId)` (log out everywhere). Verifying an expired token
226
+ deletes it in passing, so the table self-prunes.
227
+
228
+ **JWT vs. opaque:** reach for `jwt` when you want zero-lookup, stateless tokens
229
+ (and don't need revocation); reach for `createToken`/`tokenAuth` when you need
230
+ revocation, per-token scopes, or last-used tracking.
231
+
232
+ ## Basic authentication
233
+
234
+ For internal tools and quick gates, `basicAuth()` implements HTTP Basic auth —
235
+ the browser's native `username` / `password` prompt. Always behind HTTPS, since
236
+ the credentials ride on every request:
237
+
238
+ ```ts
239
+ import { basicAuth, auth, hash } from "@shaferllc/keel/core";
240
+
241
+ router.get("/admin", () => response.json(auth().id())).use(
242
+ basicAuth(async (username, password) => {
243
+ const user = await db.users.findByEmail(username);
244
+ const ok = await hash.verify(user?.password ?? hash.dummy, password);
245
+ return ok && user ? user.id : false; // return the id to log them in, or false
246
+ }, { realm: "Admin" }),
247
+ );
248
+ ```
249
+
250
+ The verifier returns the user's id (logs them in for the request), `true` (allow
251
+ without an identity), or a falsy value (reject). On rejection `basicAuth` answers
252
+ `401` with a `WWW-Authenticate` challenge so the browser re-prompts.
253
+
254
+ ## Social sign-in
255
+
256
+ "Sign in with GitHub/Google/Discord" lives in its own guide —
257
+ [Social authentication](./social-auth.md).
258
+
259
+ ## Registration
260
+
261
+ Registration is the same flow in reverse — hash the password on the way in:
262
+
263
+ ```ts
264
+ const user = await db.users.create({
265
+ email,
266
+ password: await hash.make(password),
267
+ });
268
+ auth().login(user.id);
269
+ ```
270
+
271
+ ## Working with `Auth` directly
272
+
273
+ `auth()` is a thin accessor — it returns a fresh, stateless `Auth` bound to the
274
+ current request's session. You can construct one yourself if you prefer; it reads
275
+ the same session, so the two are interchangeable:
276
+
277
+ ```ts
278
+ import { Auth } from "@shaferllc/keel/core";
279
+
280
+ if (new Auth().check()) { /* … */ }
281
+ ```
282
+
283
+ There's nothing to share between instances — all state lives in the session — so
284
+ `auth()` and `new Auth()` behave identically.
285
+
286
+ ---
287
+
288
+ ## API reference
289
+
290
+ ### `auth()`
291
+
292
+ `auth(): Auth`
293
+
294
+ Returns an `Auth` accessor bound to the current request's session.
295
+
296
+ ```ts
297
+ import { auth } from "@shaferllc/keel/core";
298
+
299
+ auth().login(userId);
300
+ await auth().user();
301
+ ```
302
+
303
+ **Notes:** constructs a fresh `Auth` each call — it's stateless, so there's no
304
+ cost to calling it repeatedly. Every method underneath reaches through
305
+ `session()`, which throws if `sessionMiddleware()` isn't installed.
306
+
307
+ ### `setUserProvider(fn)`
308
+
309
+ `setUserProvider(fn: UserProvider): void`
310
+
311
+ Registers the function Keel uses to turn a stored id back into a user.
312
+
313
+ ```ts
314
+ import { setUserProvider } from "@shaferllc/keel/core";
315
+
316
+ setUserProvider((id) => db.users.find(id));
317
+ ```
318
+
319
+ **Notes:** global — the last call wins. Register it once in a service provider.
320
+ Until it's set, `auth().user()` throws for a logged-in request (but still returns
321
+ `null` for a guest).
322
+
323
+ ### `authGuard(options?)`
324
+
325
+ `authGuard(options?: { redirectTo?: string }): MiddlewareHandler`
326
+
327
+ Builds a middleware that blocks unauthenticated requests.
328
+
329
+ ```ts
330
+ import { authGuard } from "@shaferllc/keel/core";
331
+
332
+ router.named({
333
+ auth: authGuard({ redirectTo: "/login" }),
334
+ api: authGuard(), // 401 instead
335
+ });
336
+ ```
337
+
338
+ **Notes:** with `redirectTo`, guests get a redirect; without it, a
339
+ `401 { error: "Unauthenticated", status: 401 }` JSON response. Authenticated
340
+ requests pass straight through to the next handler. The check is `guest()` only —
341
+ no user is loaded.
342
+
343
+ ### `Auth`
344
+
345
+ The accessor returned by `auth()`. Stateless — all its state lives in the
346
+ session — so you rarely construct it directly, though `new Auth()` works and is
347
+ equivalent to `auth()`.
348
+
349
+ #### `login(id)`
350
+
351
+ `login(id: string | number): void`
352
+
353
+ Marks the given id as the authenticated user by storing it in the session.
354
+
355
+ ```ts
356
+ auth().login(user.id);
357
+ ```
358
+
359
+ **Notes:** does no lookup or password check — verify credentials before calling.
360
+ The id is coerced with `String(id)`, so `id()` and your provider always receive a
361
+ string.
362
+
363
+ #### `logout()`
364
+
365
+ `logout(): void`
366
+
367
+ Forgets the authenticated id from the session.
368
+
369
+ ```ts
370
+ auth().logout();
371
+ ```
372
+
373
+ **Notes:** only removes the auth key — other session data (flashes, cart, etc.)
374
+ survives. Call `session().clear()` yourself for a full reset.
375
+
376
+ #### `id()`
377
+
378
+ `id(): string | null`
379
+
380
+ The authenticated user's id, or `null` if nobody is logged in.
381
+
382
+ ```ts
383
+ const uid = auth().id(); // "42" | null
384
+ ```
385
+
386
+ **Notes:** always a string (see `login`). Returns `null`, not `undefined`, for a
387
+ guest.
388
+
389
+ #### `check()`
390
+
391
+ `check(): boolean`
392
+
393
+ `true` when a user is authenticated.
394
+
395
+ ```ts
396
+ if (auth().check()) { /* logged in */ }
397
+ ```
398
+
399
+ **Notes:** a pure `id() != null` test — reads the session, runs no provider.
400
+
401
+ #### `guest()`
402
+
403
+ `guest(): boolean`
404
+
405
+ `true` when the request is unauthenticated — the inverse of `check()`.
406
+
407
+ ```ts
408
+ if (auth().guest()) return response.redirect("/login");
409
+ ```
410
+
411
+ #### `user(...)`
412
+
413
+ `user<User = unknown>(): Promise<User | null>`
414
+
415
+ Loads the full authenticated user by running the session id through the
416
+ registered provider.
417
+
418
+ ```ts
419
+ type User = { id: number; email: string };
420
+ const user = await auth().user<User>(); // User | null
421
+ ```
422
+
423
+ **Notes:** returns `null` when nobody is logged in. Throws
424
+ `No user provider…` if a user *is* logged in but `setUserProvider` was never
425
+ called. No caching — each call re-invokes the provider. The generic only types
426
+ the result; it does not validate the row's shape at runtime.
427
+
428
+ ### Interfaces & types
429
+
430
+ #### `UserProvider`
431
+
432
+ ```ts
433
+ type UserProvider = (id: string) => unknown | Promise<unknown>;
434
+ ```
435
+
436
+ The seam between Keel and your user store. Implement it once and register it with
437
+ `setUserProvider` — Keel calls it with the string id from the session whenever
438
+ `auth().user()` runs, and treats the return value as the authenticated user.
439
+
440
+ ```ts
441
+ import { setUserProvider, type UserProvider } from "@shaferllc/keel/core";
442
+
443
+ const provider: UserProvider = async (id) => {
444
+ // `id` is always a string; coerce if your keys are numeric
445
+ return db.users.find(Number(id));
446
+ };
447
+
448
+ setUserProvider(provider);
449
+ ```
450
+
451
+ **Notes:** may be sync or async — `user()` awaits it either way. Return the user
452
+ object (any shape) when found, or a nullish value when not; that value flows back
453
+ out of `auth().user()`.
@@ -0,0 +1,167 @@
1
+ # Authorization
2
+
3
+ Where [authentication](./authentication.md) answers *who you are*, authorization
4
+ answers *what you're allowed to do*. Keel gives you **gates** (ad-hoc abilities)
5
+ and **policies** (abilities grouped per model) — a compact authorization layer.
6
+
7
+ The current user is resolved from `auth().user()` by default, so authorization
8
+ composes with the session auth you already have.
9
+
10
+ ## Gates
11
+
12
+ A gate is a named ability with a callback that receives the user and whatever you
13
+ pass to the check:
14
+
15
+ ```ts
16
+ import { define, can, authorize } from "@shaferllc/keel/core";
17
+
18
+ // register once, at boot (e.g. in a service provider):
19
+ define("update-post", (user, post) => post.authorId === user.id);
20
+ define("access-admin", (user) => user.role === "admin");
21
+
22
+ // check anywhere:
23
+ if (await can("update-post", post)) {
24
+ // …
25
+ }
26
+
27
+ await authorize("update-post", post); // throws a 403 ForbiddenException if denied
28
+ ```
29
+
30
+ `can(ability, ...args)` returns a boolean; `cannot(...)` is its negation;
31
+ `authorize(...)` throws a `403` when denied (the HTTP kernel renders it).
32
+
33
+ ## Policies
34
+
35
+ For a model with several abilities, group them in a **policy** class — one method
36
+ per ability — and register it. `can("update", post)` then routes to
37
+ `PostPolicy.update(user, post)` automatically, by the argument's class:
38
+
39
+ ```ts
40
+ import { policy, can } from "@shaferllc/keel/core";
41
+
42
+ class PostPolicy {
43
+ view(user, post) {
44
+ return post.published || post.authorId === user.id;
45
+ }
46
+ update(user, post) {
47
+ return post.authorId === user.id;
48
+ }
49
+ delete(user, post) {
50
+ return user.admin || post.authorId === user.id;
51
+ }
52
+ }
53
+
54
+ policy(Post, PostPolicy); // register the class (or an instance)
55
+
56
+ await can("view", post); // → PostPolicy.view(user, post)
57
+ await authorize("delete", post); // → PostPolicy.delete(user, post) or 403
58
+ ```
59
+
60
+ A policy is a plain class — no base class, no framework glue. The method name is
61
+ the ability; the first argument to the check is the model.
62
+
63
+ ## Admin bypass (before hooks)
64
+
65
+ Register a `gateBefore` callback to decide checks up front — return a boolean to
66
+ short-circuit, or `undefined` to fall through to the gate/policy. Perfect for a
67
+ super-admin:
68
+
69
+ ```ts
70
+ import { gateBefore } from "@shaferllc/keel/core";
71
+
72
+ gateBefore((user) => (user.role === "superadmin" ? true : undefined));
73
+ ```
74
+
75
+ A `gateAfter` callback brackets the other end: it runs *after* the gate/policy
76
+ and receives the result, returning a boolean to override it or `undefined` to keep
77
+ it. Use it to audit every decision, or to veto late:
78
+
79
+ ```ts
80
+ import { gateAfter } from "@shaferllc/keel/core";
81
+
82
+ gateAfter((user, ability, args, result) => {
83
+ log.info("authz", { user: user.id, ability, result });
84
+ return undefined; // keep the original decision
85
+ });
86
+ ```
87
+
88
+ ## In a controller
89
+
90
+ ```ts
91
+ export class PostController {
92
+ async update(c: Ctx) {
93
+ const post = await Post.findOrFail(param("id"));
94
+ await authorize("update", post); // 403 unless allowed
95
+ // … safe to proceed
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## Checking a specific user
101
+
102
+ `can`/`authorize` use the current user. To check someone else (background jobs,
103
+ tests, impersonation), use the `For` variants:
104
+
105
+ ```ts
106
+ import { canFor, authorizeFor } from "@shaferllc/keel/core";
107
+
108
+ await canFor(otherUser, "update-post", post);
109
+ await authorizeFor(otherUser, "update-post", post);
110
+ ```
111
+
112
+ Resolving the current user differently (token auth instead of session)?
113
+ `setUserResolver(() => currentUserSomehow())`.
114
+
115
+ ## API reference
116
+
117
+ ### `define(ability, callback)`
118
+
119
+ `define(ability: string, callback: (user, ...args) => boolean | Promise<boolean>): void`
120
+
121
+ Registers a gate. The callback receives the resolved user and the check
122
+ arguments.
123
+
124
+ ### `policy(model, impl)`
125
+
126
+ `policy(model: Constructor, impl: Policy | (new () => Policy)): void`
127
+
128
+ Registers a policy (class or instance) for a model. `can(ability, instance)`
129
+ routes to `impl[ability](user, instance)` when the ability matches a method.
130
+
131
+ ### `can(ability, ...args)` / `cannot(...)`
132
+
133
+ `can(ability: string, ...args): Promise<boolean>`
134
+
135
+ Whether the current user is allowed. `cannot` is the negation. Policy (matching
136
+ model argument) is tried first, then a gate; unknown abilities **deny**.
137
+
138
+ ### `authorize(ability, ...args)`
139
+
140
+ `authorize(ability: string, ...args): Promise<void>`
141
+
142
+ Throws a `403` `ForbiddenException` unless allowed.
143
+
144
+ ### `canFor(user, ...)` / `authorizeFor(user, ...)`
145
+
146
+ The `can` / `authorize` pair for an explicit user rather than the current one.
147
+
148
+ ### `gateBefore(callback)`
149
+
150
+ `gateBefore(callback: (user, ability, args) => boolean | undefined | Promise<…>): void`
151
+
152
+ Runs before every check; a boolean short-circuits, `undefined` falls through.
153
+
154
+ ### `setUserResolver(resolver)` / `clearAuthorization()`
155
+
156
+ Override how the current user is resolved (default `auth().user()`), and reset
157
+ all gates/policies/hooks (a test helper).
158
+
159
+ ### Interfaces & types
160
+
161
+ #### `GateCallback`
162
+
163
+ `type GateCallback = (user: unknown, ...args: unknown[]) => boolean | Promise<boolean>`
164
+
165
+ #### `BeforeCallback`
166
+
167
+ `type BeforeCallback = (user, ability: string, args: unknown[]) => boolean | undefined | Promise<boolean | undefined>`