@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.
- package/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +14 -2
- package/dist/core/auth.d.ts +47 -0
- package/dist/core/auth.js +77 -0
- package/dist/core/authorization.d.ts +9 -1
- package/dist/core/authorization.js +22 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/container.d.ts +20 -0
- package/dist/core/container.js +52 -0
- package/dist/core/cors.d.ts +29 -0
- package/dist/core/cors.js +72 -0
- package/dist/core/crypto.d.ts +40 -4
- package/dist/core/crypto.js +66 -6
- package/dist/core/csrf.d.ts +25 -0
- package/dist/core/csrf.js +78 -0
- package/dist/core/database.d.ts +49 -4
- package/dist/core/database.js +89 -21
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +15 -3
- package/dist/core/helpers.js +23 -3
- package/dist/core/index.d.ts +33 -18
- package/dist/core/index.js +16 -8
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/model.d.ts +2 -0
- package/dist/core/model.js +16 -14
- package/dist/core/provider.d.ts +7 -0
- package/dist/core/provider.js +7 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/rate-limit.js +3 -0
- package/dist/core/relations.js +13 -13
- package/dist/core/request.d.ts +26 -0
- package/dist/core/request.js +77 -0
- package/dist/core/shield.d.ts +39 -0
- package/dist/core/shield.js +60 -0
- package/dist/core/social.d.ts +173 -0
- package/dist/core/social.js +337 -0
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/core/tokens.d.ts +74 -0
- package/dist/core/tokens.js +155 -0
- package/dist/db/d1.d.ts +32 -0
- package/dist/db/d1.js +26 -0
- package/dist/db/libsql.d.ts +29 -0
- package/dist/db/libsql.js +32 -0
- package/dist/db/pg.d.ts +29 -0
- package/dist/db/pg.js +33 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- 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),
|
|
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
|
|
package/bin/keel-mcp.mjs
ADDED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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). */
|
package/dist/core/application.js
CHANGED
|
@@ -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,
|
|
99
|
-
await this.make(Events).emit(event,
|
|
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
|
/**
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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);
|
package/dist/core/cache.d.ts
CHANGED
|
@@ -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
|
|
39
|
-
/** Like remember(), but cached forever (no TTL). */
|
|
40
|
-
rememberForever<T>(key: string, factory: () => 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
|
}
|