@shaferllc/keel 0.59.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 (166) 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 +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
package/AGENTS.md ADDED
@@ -0,0 +1,167 @@
1
+ # AGENTS.md — working in Keel with an AI agent
2
+
3
+ Guidance for AI coding agents (Claude Code, Cursor, …) editing **Keel** — the
4
+ house framework for Node.js — or an app built on it. Humans: this is also a fast
5
+ orientation. For prose guides see [`docs/`](./docs); for a machine-readable
6
+ surface, run the [MCP server](#mcp-server-recommended).
7
+
8
+ > **The fastest path:** connect the Keel MCP server (below) and call
9
+ > `keel_overview` first. It returns conventions, the folder map, every doc
10
+ > topic, and the generators. Then `keel_search_docs` / `keel_read_doc` for
11
+ > depth, `keel_search_api` to find exports, `keel_scaffold` to generate code.
12
+
13
+ ---
14
+
15
+ ## What Keel is
16
+
17
+ A small, legible MVC framework: a real **service container**, **service
18
+ providers**, dot-notation **config**, expressive **routing**, active-record
19
+ **models**, a **queue**, **auth**, and a code-generating **console**.
20
+ [Hono](https://hono.dev) powers HTTP; everything above it is Keel's. The whole
21
+ core is a few hundred lines per file in `src/core/` — read it, nothing is magic.
22
+
23
+ ## The one import rule
24
+
25
+ Everything userland comes from a single entry point:
26
+
27
+ ```ts
28
+ import { Router, Model, config, ServiceProvider } from "@shaferllc/keel/core";
29
+ ```
30
+
31
+ - **In a consuming app** (the `keel-app` starter, or your own): use
32
+ `@shaferllc/keel/core`. This is what generated stubs emit.
33
+ - **Inside this framework repo's example app** (`app/`, `routes/`, `bootstrap/`):
34
+ the code uses the `@keel/core` tsconfig **path alias** that points at
35
+ `src/core/`. Match the file you're editing — don't "fix" `@keel/core` to the
36
+ package name inside this repo.
37
+
38
+ Never deep-import `@shaferllc/keel/core/model.js`. If a symbol isn't exported
39
+ from `/core`, that's intentional. Find exports with `keel_search_api` or read
40
+ `src/core/index.ts`.
41
+
42
+ ## Folder layout of a Keel app
43
+
44
+ | Path | What lives there |
45
+ |------|------------------|
46
+ | `app/Controllers/` | Controller classes (resolved from the container → DI) |
47
+ | `app/Providers/` | Service providers (`register()` then `boot()`) |
48
+ | `app/Http/Middleware/` | Hono middleware handlers |
49
+ | `app/Http/Kernel.ts` | The HTTP kernel — global middleware, error handling |
50
+ | `app/Models/` | Active-record models extending `Model` |
51
+ | `app/Jobs/`, `app/Notifications/`, `app/Transformers/` | Queue jobs, notifications, API serializers |
52
+ | `config/*.ts` | Config files; read via `config("app.name")` |
53
+ | `routes/web.ts` | Route definitions — default export receives the `Router` |
54
+ | `database/factories/`, `database/seeders/`, `database/migrations/` | Data tooling |
55
+ | `bootstrap/app.ts` | Assembles the `Application`, boots providers, loads routes |
56
+ | `.env` | Environment, loaded into `env(...)` and config |
57
+
58
+ ## Core mental model
59
+
60
+ 1. **Container.** Every service is registered in and resolved out of the
61
+ container. `bind` (transient), `singleton` (once), `instance` (existing
62
+ value), `make(Token)` (resolve). Constructor injection: a controller/service
63
+ with `constructor(private app: Container)` gets it automatically.
64
+ 2. **Providers.** `register()` binds things into the container; `boot()` runs
65
+ after all providers registered, to wire things together. Register providers
66
+ in `bootstrap/providers.ts`.
67
+ 3. **Config.** `config("app.port", 3000)` reads `config/app.ts` merged with
68
+ `.env`. Dot-notation, with a default.
69
+ 4. **Routing.** `router.get("/x", [Controller, "method"])` or a closure. Name
70
+ routes with `.name()`, group with `.group()`, constrain params with
71
+ `.where()`, build URLs with `router.url("name", params)`.
72
+ 5. **Request context.** Inside a handler, `param()`, `query()`, `body()`,
73
+ `json()`, `text()` read/write the current request via async-local context —
74
+ no need to thread `c` everywhere (though the Hono `Ctx` is available).
75
+
76
+ ## How to add things (prefer the generators)
77
+
78
+ Use `keel_scaffold` (MCP) or the console — both emit the correct stub with the
79
+ right imports and target path. Then wire it up.
80
+
81
+ | Goal | Command | Then |
82
+ |------|---------|------|
83
+ | Controller | `keel make:controller Post` (`-r` for REST) | add a route in `routes/web.ts` |
84
+ | Provider | `keel make:provider Billing` | register in `bootstrap/providers.ts` |
85
+ | Middleware | `keel make:middleware Auth` | attach in `app/Http/Kernel.ts` or per-route |
86
+ | Model | *(no generator — extend `Model`)* | set `static table`; add relations |
87
+ | Factory | `keel make:factory User` | reference the model |
88
+ | Seeder | `keel make:seeder Database` | call factories in `run()` |
89
+ | Job | `keel make:job SendWelcome` | `dispatch(new SendWelcomeJob())` |
90
+ | Notification | `keel make:notification Paid` | `notify(user, new PaidNotification())` |
91
+ | Transformer | `keel make:transformer User` | return it from a controller |
92
+
93
+ **Scaffolding does not write files** via MCP — it returns code + path; you write
94
+ it. The console (`keel make:*`) does write, and refuses to overwrite.
95
+
96
+ A minimal controller and route:
97
+
98
+ ```ts
99
+ // app/Controllers/PostController.ts
100
+ import type { Ctx } from "@shaferllc/keel/core";
101
+ export class PostController {
102
+ index(c: Ctx) { return c.json({ ok: true }); }
103
+ }
104
+
105
+ // routes/web.ts
106
+ router.get("/posts", [PostController, "index"]).name("posts.index");
107
+ ```
108
+
109
+ ## Commands
110
+
111
+ ```bash
112
+ npm run dev # example app on http://localhost:3000 (tsx watch)
113
+ npm run serve # keel serve
114
+ npm run keel -- routes # list registered routes
115
+ npm run keel -- make:controller Foo
116
+ npm run mcp # start the MCP server over stdio (dev)
117
+ npm test # node --test over tests/*.test.ts
118
+ npm run typecheck # tsc --noEmit — run before you finish
119
+ npm run build # regenerate AI artifacts + compile to dist/
120
+ npm run build:ai # regenerate llms.txt, llms-full.txt, docs/ai-manifest.json
121
+ ```
122
+
123
+ ## Conventions & guardrails
124
+
125
+ - **TypeScript, strict, ESM.** `.js` extensions in relative imports (NodeNext).
126
+ No default exports except where the pattern already uses them (routes).
127
+ - **Match the surrounding file** — comment density, naming, idioms. Keel prizes
128
+ legibility; write code that reads like `src/core/`.
129
+ - **Test with the real thing.** `TestClient`/`testClient` drives the HTTP stack
130
+ in-process; `hash.fake()`, `fakeDisk()`, `ArrayTransport`, `MemoryDriver`, and
131
+ `EventBuffer` make side-effecting subsystems deterministic. See
132
+ [`docs/testing.md`](./docs/testing.md).
133
+ - **Run `npm run typecheck` before finishing.** The whole framework is typed end
134
+ to end; a green `tsc` is the bar.
135
+ - **After editing docs or the export surface, run `npm run build:ai`** so
136
+ `llms.txt`, `llms-full.txt`, and `docs/ai-manifest.json` stay in sync (the MCP
137
+ server reads the manifest).
138
+
139
+ ## MCP server (recommended)
140
+
141
+ Keel ships an MCP server exposing its docs, the full public API surface, the
142
+ generators, and framework conventions to any MCP-capable agent.
143
+
144
+ **Claude Code:**
145
+ ```bash
146
+ claude mcp add keel -- npx -y keel-mcp # in an app that depends on @shaferllc/keel
147
+ ```
148
+ Or, hacking on the framework itself:
149
+ ```bash
150
+ claude mcp add keel -- npm --prefix /path/to/keel run mcp
151
+ ```
152
+
153
+ **`.mcp.json` / Cursor / other clients:**
154
+ ```json
155
+ { "mcpServers": { "keel": { "command": "npx", "args": ["-y", "keel-mcp"] } } }
156
+ ```
157
+
158
+ Tools: `keel_overview`, `keel_search_docs`, `keel_read_doc`, `keel_search_api`,
159
+ `keel_list_generators`, `keel_scaffold`. Resources: `keel://overview`,
160
+ `keel://llms-full`, `keel://docs/<slug>`. See [`docs/ai.md`](./docs/ai.md).
161
+
162
+ ## Where to read next
163
+
164
+ - [`docs/getting-started.md`](./docs/getting-started.md) — a guided first hour
165
+ - [`docs/architecture.md`](./docs/architecture.md) — a request from socket to response
166
+ - [`docs/container.md`](./docs/container.md) — the DI core everything rests on
167
+ - [`llms-full.txt`](./llms-full.txt) — every guide in one file, for a fresh context
package/README.md CHANGED
@@ -108,11 +108,38 @@ npm run keel serve --port 8080 # start the server on a chosen port
108
108
  npm run keel make:controller Post # -> app/Controllers/PostController.ts
109
109
  npm run keel make:provider Billing # -> app/Providers/BillingServiceProvider.ts
110
110
  npm run keel make:middleware Auth # -> app/Http/Middleware/authMiddleware.ts
111
+ npm run keel mcp # start the MCP server (docs + API for AI agents)
111
112
  ```
112
113
 
113
114
  Under the hood the console binary is `bin/keel.ts`; the npm scripts wrap it with
114
115
  `tsx`.
115
116
 
117
+ ## Built for AI ⚓🤖
118
+
119
+ Keel is designed to be **written with an AI agent**. Alongside the human docs it
120
+ ships a machine-readable surface that stays generated-in-sync, never stale:
121
+
122
+ - **An MCP server.** `keel-mcp` exposes Keel's docs, its full public API (380+
123
+ exports), the generators, and its conventions to any [MCP](https://modelcontextprotocol.io)
124
+ client. Connect it in Claude Code:
125
+ ```bash
126
+ claude mcp add keel -- npx -y keel-mcp
127
+ ```
128
+ Tools: `keel_overview`, `keel_search_docs`, `keel_read_doc`, `keel_search_api`,
129
+ `keel_list_generators`, `keel_scaffold`. Resources: `keel://overview`,
130
+ `keel://llms-full`, `keel://docs/<slug>`.
131
+ - **[`AGENTS.md`](./AGENTS.md).** The agent playbook — the one import rule, the
132
+ folder map, the container/provider model, a "how to add X" table, and the
133
+ guardrails. `CLAUDE.md` points to it.
134
+ - **[`llms.txt`](./llms.txt) + [`llms-full.txt`](./llms-full.txt).** A
135
+ [spec-compliant](https://llmstxt.org) doc index and a one-file concatenation of
136
+ every guide, both shipped in the npm package for drop-in context.
137
+ - **Generators an agent can drive.** `keel_scaffold` (or `keel make:*`) emits the
138
+ correct stub with the right imports and path for every construct.
139
+
140
+ Full guide: **[docs/ai.md](./docs/ai.md)**. Regenerate the surface after doc or
141
+ export changes with `npm run build:ai` (also runs automatically on `npm run build`).
142
+
116
143
  ## Project layout
117
144
 
118
145
  ```
@@ -191,7 +218,8 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
191
218
  | [Redis](./docs/redis.md) | Pluggable client, memory driver, cache adapter |
192
219
  | [Logger](./docs/logger.md) | Structured logging, per-request `reqId`, redaction |
193
220
  | [Static Files](./docs/static-files.md) | serveStatic(), caching, dot-file safety |
194
- | [Storage](./docs/storage.md) | Pluggable disks (local/S3/R2), edge-safe |
221
+ | [Storage](./docs/storage.md) | Pluggable disks (local/S3/R2), signed URLs, direct uploads |
222
+ | [Health Checks](./docs/health.md) | `/health/live` + `/health/ready`, pluggable checks |
195
223
  | [Views](./docs/views.md) | Hono JSX components, layouts, the View service |
196
224
  | [Templates](./docs/templates.md) | `{{ }}` + `@`-tag templating engine, edge-safe |
197
225
  | [Middleware](./docs/middleware.md) | Global middleware, writing your own |
@@ -205,6 +233,7 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
205
233
  | [The Console](./docs/console.md) | `serve`, `routes`, `make:*` generators |
206
234
  | [Architecture](./docs/architecture.md) | Container, kernel, request lifecycle |
207
235
  | [Built on Hono](./docs/hono.md) | The Hono layer underneath, and using it directly |
236
+ | [Building with AI](./docs/ai.md) | MCP server, `AGENTS.md`, `llms.txt`, agent workflow |
208
237
 
209
238
  ## Testing
210
239
 
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // Published entry for the Keel MCP server. Registered as the `keel-mcp` bin,
3
+ // so an installed app can wire it up with `npx keel-mcp` (see docs/ai.md).
4
+ import { runMcpServer } from "../dist/mcp/server.js";
5
+
6
+ runMcpServer().catch((err) => {
7
+ console.error(err);
8
+ process.exit(1);
9
+ });
@@ -12,7 +12,7 @@ import { Config, type ConfigData } from "./config.js";
12
12
  import { Router } from "./http/router.js";
13
13
  import { View } from "./view.js";
14
14
  import { type ProviderClass } from "./provider.js";
15
- import type { Listener } from "./events.js";
15
+ import type { Listener, EventName, EmitArgs, Resolve } from "./events.js";
16
16
  /** A configurator: a plain function that sets the app up and may return anything. */
17
17
  export type Configurator = (app: Application) => unknown;
18
18
  export interface BootOptions {
@@ -58,13 +58,13 @@ export declare class Application extends Container {
58
58
  * `app.on(...)` and the global `listen()` helper reach the same emitter.
59
59
  * Returns an unsubscribe function.
60
60
  */
61
- on<T = unknown>(event: string, listener: Listener<T>): () => void;
61
+ on<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): () => void;
62
62
  /** Subscribe for a single emission. Returns an unsubscribe function. */
63
- once<T = unknown>(event: string, listener: Listener<T>): () => void;
63
+ once<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): () => void;
64
64
  /** Unsubscribe a listener registered with `on()`/`once()`. */
65
- off<T = unknown>(event: string, listener: Listener<T>): this;
65
+ off<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): this;
66
66
  /** Emit an app event, awaiting every listener in registration order. */
67
- emit<T = unknown>(event: string, payload?: T): Promise<void>;
67
+ emit<T = unknown, E extends EventName = EventName>(event: E, ...args: EmitArgs<Resolve<T, E>>): Promise<void>;
68
68
  /** Merge a config object into the repository under its top-level keys. */
69
69
  private mergeConfig;
70
70
  /** Load .env via dotenv (Node only; no-op elsewhere). */
@@ -95,8 +95,8 @@ export class Application extends Container {
95
95
  return this;
96
96
  }
97
97
  /** Emit an app event, awaiting every listener in registration order. */
98
- async emit(event, payload) {
99
- await this.make(Events).emit(event, payload);
98
+ async emit(event, ...args) {
99
+ await this.make(Events).emit(event, ...args);
100
100
  }
101
101
  /** Merge a config object into the repository under its top-level keys. */
102
102
  mergeConfig(data) {
@@ -168,9 +168,21 @@ export class Application extends Container {
168
168
  for (const provider of this.providers) {
169
169
  await provider.boot();
170
170
  }
171
+ // A provider's shutdown() joins the app's shutdown hooks, so it runs (LIFO)
172
+ // on terminate() alongside any onShutdown() a provider registered by hand.
173
+ // ready()/shutdown() are optional — plain duck-typed providers may omit them.
174
+ for (const provider of this.providers) {
175
+ if (typeof provider.shutdown === "function") {
176
+ this.onShutdown(() => provider.shutdown());
177
+ }
178
+ }
171
179
  this.booted = true;
172
180
  for (const hook of this.readyHooks)
173
181
  await hook(this);
182
+ for (const provider of this.providers) {
183
+ if (typeof provider.ready === "function")
184
+ await provider.ready();
185
+ }
174
186
  return this;
175
187
  }
176
188
  /**
@@ -12,6 +12,7 @@
12
12
  * await auth().user(); // the full user (via your provider)
13
13
  */
14
14
  import type { MiddlewareHandler } from "hono";
15
+ import { type AccessToken } from "./tokens.js";
15
16
  /** Loads a user by the id stored in the session. Register with setUserProvider. */
16
17
  export type UserProvider = (id: string) => unknown | Promise<unknown>;
17
18
  /** Tell Keel how to load the authenticated user from its id. */
@@ -58,3 +59,49 @@ export declare function authGuard(options?: {
58
59
  export declare function bearerAuth(options?: {
59
60
  optional?: boolean;
60
61
  }): MiddlewareHandler;
62
+ /**
63
+ * Verifies a Basic-auth credential pair. Return the authenticated user's id to
64
+ * log them in for the request (so `auth().user()` resolves through your
65
+ * provider), `true` to allow without an identity, or a falsy value to reject.
66
+ * Verify the password with `hash.verify` — and reach for `hash.dummy` on a miss
67
+ * so timing doesn't reveal which usernames exist.
68
+ */
69
+ export type BasicVerifier = (username: string, password: string) => string | number | boolean | null | undefined | Promise<string | number | boolean | null | undefined>;
70
+ /**
71
+ * HTTP Basic authentication. Reads `Authorization: Basic <base64>`, decodes the
72
+ * `username:password` pair, and hands it to your `verify` callback. On failure
73
+ * it answers `401` with a `WWW-Authenticate` challenge (so a browser prompts).
74
+ * Handy for internal tools and quick API gates — always behind HTTPS, since the
75
+ * credentials ride on every request.
76
+ *
77
+ * router.get("/admin", handler).use(
78
+ * basicAuth(async (user, pass) => {
79
+ * const row = await findAdmin(user);
80
+ * return (await hash.verify(row?.password ?? hash.dummy, pass)) && row ? row.id : false;
81
+ * }),
82
+ * );
83
+ */
84
+ export declare function basicAuth(verify: BasicVerifier, options?: {
85
+ realm?: string;
86
+ }): MiddlewareHandler;
87
+ /**
88
+ * Opaque access-token auth — the revocable, ability-scoped counterpart to
89
+ * `bearerAuth()` (which verifies a stateless JWT). Reads `Authorization: Bearer
90
+ * keel_…`, verifies it against the [token store](./tokens.ts), and makes the
91
+ * token's owner the authenticated id — plus stashes the token itself so handlers
92
+ * can check abilities via `token()` / `tokenCan()`.
93
+ *
94
+ * router.get("/api/posts", handler).use(tokenAuth({ abilities: ["posts:read"] }));
95
+ *
96
+ * Rejects a missing, invalid, expired, or under-scoped token with `401`. Pass
97
+ * `{ optional: true }` to let unauthenticated requests through.
98
+ */
99
+ export declare function tokenAuth(options?: {
100
+ optional?: boolean;
101
+ abilities?: string[];
102
+ connection?: string;
103
+ }): MiddlewareHandler;
104
+ /** The opaque access token verified by `tokenAuth()` on this request, or null. */
105
+ export declare function token(): AccessToken | null;
106
+ /** Whether this request's access token grants an ability (`false` if there's none). */
107
+ export declare function tokenCan(ability: string): boolean;
package/dist/core/auth.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { session } from "./session.js";
15
15
  import { ctx } from "./request.js";
16
16
  import { jwt } from "./crypto.js";
17
+ import { verifyToken, tokenAllows } from "./tokens.js";
17
18
  const KEY = "auth_id";
18
19
  let provider;
19
20
  /** Tell Keel how to load the authenticated user from its id. */
@@ -103,3 +104,79 @@ export function bearerAuth(options = {}) {
103
104
  await next();
104
105
  };
105
106
  }
107
+ /**
108
+ * HTTP Basic authentication. Reads `Authorization: Basic <base64>`, decodes the
109
+ * `username:password` pair, and hands it to your `verify` callback. On failure
110
+ * it answers `401` with a `WWW-Authenticate` challenge (so a browser prompts).
111
+ * Handy for internal tools and quick API gates — always behind HTTPS, since the
112
+ * credentials ride on every request.
113
+ *
114
+ * router.get("/admin", handler).use(
115
+ * basicAuth(async (user, pass) => {
116
+ * const row = await findAdmin(user);
117
+ * return (await hash.verify(row?.password ?? hash.dummy, pass)) && row ? row.id : false;
118
+ * }),
119
+ * );
120
+ */
121
+ export function basicAuth(verify, options = {}) {
122
+ const realm = options.realm ?? "Restricted";
123
+ return async (c, next) => {
124
+ const challenge = () => c.json({ error: "Unauthenticated", status: 401 }, 401, {
125
+ "WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"`,
126
+ });
127
+ const raw = c.req.header("authorization")?.match(/^Basic\s+(.+)$/i)?.[1];
128
+ if (!raw)
129
+ return challenge();
130
+ let decoded;
131
+ try {
132
+ decoded = new TextDecoder().decode(Uint8Array.from(atob(raw), (ch) => ch.charCodeAt(0)));
133
+ }
134
+ catch {
135
+ return challenge(); // not valid base64
136
+ }
137
+ const sep = decoded.indexOf(":");
138
+ if (sep === -1)
139
+ return challenge();
140
+ const result = await verify(decoded.slice(0, sep), decoded.slice(sep + 1));
141
+ if (!result)
142
+ return challenge();
143
+ if (result !== true)
144
+ c.set("auth_id", String(result));
145
+ await next();
146
+ };
147
+ }
148
+ /**
149
+ * Opaque access-token auth — the revocable, ability-scoped counterpart to
150
+ * `bearerAuth()` (which verifies a stateless JWT). Reads `Authorization: Bearer
151
+ * keel_…`, verifies it against the [token store](./tokens.ts), and makes the
152
+ * token's owner the authenticated id — plus stashes the token itself so handlers
153
+ * can check abilities via `token()` / `tokenCan()`.
154
+ *
155
+ * router.get("/api/posts", handler).use(tokenAuth({ abilities: ["posts:read"] }));
156
+ *
157
+ * Rejects a missing, invalid, expired, or under-scoped token with `401`. Pass
158
+ * `{ optional: true }` to let unauthenticated requests through.
159
+ */
160
+ export function tokenAuth(options = {}) {
161
+ return async (c, next) => {
162
+ const raw = c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
163
+ const token = raw ? await verifyToken(raw, options.connection) : null;
164
+ const scoped = token && (options.abilities ?? []).every((a) => tokenAllows(token, a));
165
+ if (!token || !scoped) {
166
+ if (options.optional)
167
+ return next();
168
+ return c.json({ error: "Unauthenticated", status: 401 }, 401);
169
+ }
170
+ c.set("auth_id", token.tokenableId);
171
+ c.set("access_token", token);
172
+ await next();
173
+ };
174
+ }
175
+ /** The opaque access token verified by `tokenAuth()` on this request, or null. */
176
+ export function token() {
177
+ return ctx().get("access_token") ?? null;
178
+ }
179
+ /** Whether this request's access token grants an ability (`false` if there's none). */
180
+ export function tokenCan(ability) {
181
+ return tokenAllows(token(), ability);
182
+ }
@@ -25,6 +25,8 @@ type Constructor = new (...args: never[]) => object;
25
25
  export type GateCallback = (user: User, ...args: Args) => boolean | Promise<boolean>;
26
26
  /** Runs before every check; return a boolean to short-circuit (e.g. admin bypass). */
27
27
  export type BeforeCallback = (user: User, ability: string, args: Args) => boolean | undefined | Promise<boolean | undefined>;
28
+ /** Runs after every check; return a boolean to override the result (e.g. audit + veto). */
29
+ export type AfterCallback = (user: User, ability: string, args: Args, result: boolean) => boolean | undefined | Promise<boolean | undefined>;
28
30
  /** Define a gate — an ad-hoc ability keyed by name. */
29
31
  export declare function define(ability: string, callback: GateCallback): void;
30
32
  /**
@@ -35,9 +37,15 @@ export declare function define(ability: string, callback: GateCallback): void;
35
37
  export declare function policy(model: Constructor, impl: object | (new () => object)): void;
36
38
  /** Register a callback that runs before every check (return a boolean to decide). */
37
39
  export declare function gateBefore(callback: BeforeCallback): void;
40
+ /**
41
+ * Register a callback that runs *after* every check and can override its result
42
+ * — return a boolean to replace it, or `undefined` to keep it. Pairs with
43
+ * `gateBefore` to bracket every decision (logging, audit, a late veto).
44
+ */
45
+ export declare function gateAfter(callback: AfterCallback): void;
38
46
  /** Override how the "current user" is resolved (default: `auth().user()`). */
39
47
  export declare function setUserResolver(resolver: () => User | Promise<User>): void;
40
- /** Reset gates, policies, the before-hook, and the resolver (test helper). */
48
+ /** Reset gates, policies, hooks, and the resolver (test helper). */
41
49
  export declare function clearAuthorization(): void;
42
50
  /** Whether the current user is allowed the ability (with the given arguments). */
43
51
  export declare function can(ability: string, ...args: Args): Promise<boolean>;
@@ -23,6 +23,7 @@ import { auth } from "./auth.js";
23
23
  const gates = new Map();
24
24
  const policies = new Map();
25
25
  let beforeCallback;
26
+ let afterCallback;
26
27
  let userResolver = () => auth().user();
27
28
  /** Define a gate — an ad-hoc ability keyed by name. */
28
29
  export function define(ability, callback) {
@@ -41,18 +42,28 @@ export function policy(model, impl) {
41
42
  export function gateBefore(callback) {
42
43
  beforeCallback = callback;
43
44
  }
45
+ /**
46
+ * Register a callback that runs *after* every check and can override its result
47
+ * — return a boolean to replace it, or `undefined` to keep it. Pairs with
48
+ * `gateBefore` to bracket every decision (logging, audit, a late veto).
49
+ */
50
+ export function gateAfter(callback) {
51
+ afterCallback = callback;
52
+ }
44
53
  /** Override how the "current user" is resolved (default: `auth().user()`). */
45
54
  export function setUserResolver(resolver) {
46
55
  userResolver = resolver;
47
56
  }
48
- /** Reset gates, policies, the before-hook, and the resolver (test helper). */
57
+ /** Reset gates, policies, hooks, and the resolver (test helper). */
49
58
  export function clearAuthorization() {
50
59
  gates.clear();
51
60
  policies.clear();
52
61
  beforeCallback = undefined;
62
+ afterCallback = undefined;
53
63
  userResolver = () => auth().user();
54
64
  }
55
- async function evaluate(user, ability, args) {
65
+ /** Resolve a check to a raw boolean, before the after-hook runs. */
66
+ async function decide(user, ability, args) {
56
67
  if (beforeCallback) {
57
68
  const decided = await beforeCallback(user, ability, args);
58
69
  if (typeof decided === "boolean")
@@ -71,6 +82,15 @@ async function evaluate(user, ability, args) {
71
82
  return Boolean(await gate(user, ...args));
72
83
  return false; // unknown ability — deny by default
73
84
  }
85
+ async function evaluate(user, ability, args) {
86
+ const result = await decide(user, ability, args);
87
+ if (afterCallback) {
88
+ const overridden = await afterCallback(user, ability, args, result);
89
+ if (typeof overridden === "boolean")
90
+ return overridden;
91
+ }
92
+ return result;
93
+ }
74
94
  /** Whether the current user is allowed the ability (with the given arguments). */
75
95
  export async function can(ability, ...args) {
76
96
  return evaluate(await userResolver(), ability, args);
@@ -5,6 +5,23 @@
5
5
  * the global `cache()` helper.
6
6
  *
7
7
  * const stats = await cache().remember("stats", 60, () => computeStats());
8
+ *
9
+ * Features borrowed from bentocache (AdonisJS' cache), kept inside keel's
10
+ * single-store, edge-safe model:
11
+ *
12
+ * - Stampede protection — concurrent `remember()` calls for the same cold key
13
+ * run the factory ONCE and share the result, instead of dog-piling.
14
+ * - Grace / stale-on-error — with a `grace` window, an expired value is kept
15
+ * a little longer and served if the refreshing factory throws.
16
+ * - Tags — group entries and invalidate them together with `deleteByTag()`.
17
+ * - Namespaces — `namespace("users")` scopes keys under a prefix, and its
18
+ * `flush()` clears only that namespace.
19
+ *
20
+ * Tag and namespace invalidation use version stamps, not a key index: each tag
21
+ * carries a counter, every entry records the counter values it was written at,
22
+ * and invalidation just bumps the counter — so an entry whose tag has moved on
23
+ * reads as a miss. O(1) invalidation, no key-set bookkeeping, works on any
24
+ * `CacheStore`.
8
25
  */
9
26
  export interface CacheStore {
10
27
  get(key: string): Promise<unknown> | unknown;
@@ -20,22 +37,82 @@ export declare class MemoryStore implements CacheStore {
20
37
  delete(key: string): void;
21
38
  clear(): void;
22
39
  }
40
+ /** Options for `put`/`add`. */
41
+ export interface PutOptions {
42
+ /** Tags to associate with this entry, for `deleteByTag` invalidation. */
43
+ tags?: string[];
44
+ }
45
+ /** Options for the `remember` family. */
46
+ export interface RememberOptions extends PutOptions {
47
+ /**
48
+ * Grace window in seconds. After the value's TTL lapses, keep it this much
49
+ * longer and serve it if the refreshing factory throws (stale-on-error).
50
+ */
51
+ grace?: number;
52
+ }
23
53
  export declare class Cache {
24
54
  private store;
25
55
  constructor(store?: CacheStore);
56
+ private prefix;
57
+ private nsTag?;
58
+ private inflight;
59
+ /** Apply this cache's namespace prefix to a logical key. */
60
+ private k;
61
+ private tagKey;
62
+ private tagVersion;
63
+ /** Snapshot the current version of each tag, to stamp onto a new entry. */
64
+ private stampTags;
65
+ /** True if any recorded tag version is behind the live one (invalidated). */
66
+ private tagsInvalidated;
67
+ /**
68
+ * Read the raw envelope for a key. Returns undefined when absent OR when a tag
69
+ * has invalidated it — tag/namespace invalidation is a hard miss (not grace-
70
+ * eligible). TTL freshness is left to callers, so grace can still see a stale
71
+ * entry.
72
+ */
73
+ private entry;
74
+ private write;
26
75
  get<T = unknown>(key: string, fallback?: T): Promise<T>;
27
- /** Store a value, optionally expiring after `ttlSeconds`. */
28
- put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
76
+ /** Store a value, optionally expiring after `ttlSeconds`, with optional tags. */
77
+ put(key: string, value: unknown, ttlSeconds?: number, options?: PutOptions): Promise<void>;
78
+ /** Store a value only if the key is absent. Returns whether it was written. */
79
+ add(key: string, value: unknown, ttlSeconds?: number, options?: PutOptions): Promise<boolean>;
29
80
  has(key: string): Promise<boolean>;
81
+ /** The inverse of `has` — true when the key is absent or expired. */
82
+ missing(key: string): Promise<boolean>;
30
83
  forget(key: string): Promise<void>;
84
+ /** Forget several keys at once. */
85
+ forgetMany(keys: string[]): Promise<void>;
31
86
  /** Read and remove a value. */
32
87
  pull<T = unknown>(key: string, fallback?: T): Promise<T>;
33
88
  flush(): Promise<void>;
89
+ /**
90
+ * Invalidate every entry carrying any of these tags by bumping the tag's
91
+ * version — entries stamped with the old version then read as a miss. O(#tags),
92
+ * no key scan; invalidated entries fall out on their own TTL.
93
+ */
94
+ deleteByTag(tags: string[]): Promise<void>;
95
+ /**
96
+ * A cache scoped under a key prefix. Keys written through it live at
97
+ * `name:key`, and its `flush()` clears only this namespace (via an implicit
98
+ * tag) — the rest of the store is untouched. Namespaces nest.
99
+ *
100
+ * const users = cache().namespace("users");
101
+ * await users.put("1", user); // stored at "users:1"
102
+ * await users.flush(); // clears only the users namespace
103
+ */
104
+ namespace(name: string): Cache;
34
105
  /**
35
106
  * Return the cached value, or compute it with `factory`, cache it for
36
107
  * `ttlSeconds`, and return it.
108
+ *
109
+ * Concurrent calls for the same cold key share one factory run (stampede
110
+ * protection). With `{ grace }`, an expired value is retained that many extra
111
+ * seconds and served if the refreshing factory throws (stale-on-error). With
112
+ * `{ tags }`, the cached value joins those tags for `deleteByTag`.
37
113
  */
38
- remember<T>(key: string, ttlSeconds: number, factory: () => T | Promise<T>): Promise<T>;
39
- /** Like remember(), but cached forever (no TTL). */
40
- rememberForever<T>(key: string, factory: () => T | Promise<T>): Promise<T>;
114
+ remember<T>(key: string, ttlSeconds: number, factory: () => T | Promise<T>, options?: RememberOptions): Promise<T>;
115
+ /** Like remember(), but cached forever (no TTL). Also stampede-protected. */
116
+ rememberForever<T>(key: string, factory: () => T | Promise<T>, options?: PutOptions): Promise<T>;
117
+ private resolve;
41
118
  }