@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Task Scheduling
|
|
2
|
+
|
|
3
|
+
Declare recurring work with a fluent cadence, then let a **single cron trigger**
|
|
4
|
+
drive it — a code-defined scheduler, edge-first. Instead of a crontab entry per
|
|
5
|
+
job, you register tasks in code and run the scheduler once a minute; it runs
|
|
6
|
+
whatever's due.
|
|
7
|
+
|
|
8
|
+
## Scheduling tasks
|
|
9
|
+
|
|
10
|
+
A task is a [`Job`](./queues.md) or a plain function. Register it, then set the
|
|
11
|
+
cadence:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { schedule } from "@shaferllc/keel/core";
|
|
15
|
+
|
|
16
|
+
schedule(new PruneSessions()).daily();
|
|
17
|
+
schedule(() => syncInventory()).everyFiveMinutes();
|
|
18
|
+
schedule(new SendDigest()).cron("0 9 * * 1"); // 9am Mondays
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Cadences
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
task.everyMinute();
|
|
25
|
+
task.everyFiveMinutes(); // and Ten / Fifteen / Thirty
|
|
26
|
+
task.hourly();
|
|
27
|
+
task.hourlyAt(15); // :15 past every hour
|
|
28
|
+
task.daily();
|
|
29
|
+
task.dailyAt("13:30");
|
|
30
|
+
task.weekly(1); // Monday (0 = Sunday) at midnight
|
|
31
|
+
task.monthly(15); // the 15th at midnight
|
|
32
|
+
task.cron("*/10 9-17 * * 1-5"); // any 5-field cron expression
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Running the scheduler
|
|
36
|
+
|
|
37
|
+
Run the scheduler once a minute from a cron trigger; `runDue(now)` runs every task
|
|
38
|
+
whose expression matches `now` (to the minute).
|
|
39
|
+
|
|
40
|
+
### Cloudflare (Cron Triggers)
|
|
41
|
+
|
|
42
|
+
Add a trigger to `wrangler.jsonc` (`"triggers": { "crons": ["* * * * *"] }`) and
|
|
43
|
+
call the scheduler from the Worker's `scheduled` handler:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export default {
|
|
47
|
+
async scheduled(event, env, ctx) {
|
|
48
|
+
ctx.waitUntil(scheduler().runDue(new Date(event.scheduledTime)));
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Node
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
setInterval(() => scheduler().runDue(new Date()), 60_000);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Because the scheduler decides what's due, you only ever wire **one** trigger, no
|
|
60
|
+
matter how many tasks you schedule.
|
|
61
|
+
|
|
62
|
+
## Inspecting
|
|
63
|
+
|
|
64
|
+
`scheduler().due(now)` returns the due tasks without running them, and every task
|
|
65
|
+
has an `expression` and optional `name`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
schedule(job).named("prune-sessions").daily();
|
|
69
|
+
for (const t of scheduler().due(new Date())) logger().info("due", { task: t.name });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API reference
|
|
73
|
+
|
|
74
|
+
### `schedule(job)`
|
|
75
|
+
|
|
76
|
+
`schedule(job: Job | (() => void | Promise<void>)): ScheduledTask`
|
|
77
|
+
|
|
78
|
+
Registers a task on the default scheduler and returns it for cadence
|
|
79
|
+
configuration.
|
|
80
|
+
|
|
81
|
+
### `scheduler()` / `setScheduler(next)`
|
|
82
|
+
|
|
83
|
+
The default `Scheduler`; `setScheduler` replaces it (reset between tests).
|
|
84
|
+
|
|
85
|
+
### `ScheduledTask`
|
|
86
|
+
|
|
87
|
+
Returned by `schedule`. Cadence setters return `this` (chainable):
|
|
88
|
+
`everyMinute` / `everyFiveMinutes` / `everyTenMinutes` / `everyFifteenMinutes` /
|
|
89
|
+
`everyThirtyMinutes` / `hourly` / `hourlyAt(m)` / `daily` / `dailyAt("HH:MM")` /
|
|
90
|
+
`weekly(weekday?)` / `monthly(day?)` / `cron(expr)` / `named(name)`. `isDue(now)`
|
|
91
|
+
reports whether it matches a `Date`.
|
|
92
|
+
|
|
93
|
+
### `Scheduler`
|
|
94
|
+
|
|
95
|
+
| Method | Notes |
|
|
96
|
+
|--------|-------|
|
|
97
|
+
| `schedule(job)` | register a task |
|
|
98
|
+
| `due(now?)` | the tasks due at `now` (no run) |
|
|
99
|
+
| `runDue(now?)` | run every due task; returns the count |
|
|
100
|
+
| `tasks` | all registered tasks |
|
|
101
|
+
|
|
102
|
+
### `cronMatches(expression, date)`
|
|
103
|
+
|
|
104
|
+
`cronMatches(expression: string, date: Date): boolean`
|
|
105
|
+
|
|
106
|
+
Whether a 5-field cron expression (`min hour dom month dow`) matches `date` to the
|
|
107
|
+
minute. Supports `*`, exact values, lists (`1,15`), ranges (`9-17`), and steps
|
|
108
|
+
(`*/5`, `0-30/10`). Day-of-month and day-of-week follow standard cron: when both
|
|
109
|
+
are restricted, either matching counts. Fields use the `Date`'s own values (UTC in
|
|
110
|
+
a Worker, local under Node).
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Securing SSR apps
|
|
2
|
+
|
|
3
|
+
Two middlewares harden server-rendered apps: `securityHeaders()` sets the
|
|
4
|
+
defensive HTTP headers browsers act on, and `csrf()` blocks cross-site form
|
|
5
|
+
submissions. (For cross-origin API access, see [CORS](./cors.md).)
|
|
6
|
+
|
|
7
|
+
## Security headers
|
|
8
|
+
|
|
9
|
+
`securityHeaders()` sets a Content-Security-Policy, HSTS, and the clickjacking /
|
|
10
|
+
MIME-sniffing / referrer guards in one place:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { securityHeaders } from "@shaferllc/keel/core";
|
|
14
|
+
|
|
15
|
+
this.use(securityHeaders()); // sensible defaults
|
|
16
|
+
|
|
17
|
+
this.use(
|
|
18
|
+
securityHeaders({
|
|
19
|
+
csp: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"] },
|
|
20
|
+
hsts: { maxAge: 15552000, includeSubDomains: true },
|
|
21
|
+
frameGuard: "DENY",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Option | Header | Default |
|
|
27
|
+
|--------|--------|---------|
|
|
28
|
+
| `csp` | `Content-Security-Policy` | off (opt-in) |
|
|
29
|
+
| `hsts` | `Strict-Transport-Security` | off (opt-in) |
|
|
30
|
+
| `frameGuard` | `X-Frame-Options` | `"SAMEORIGIN"` |
|
|
31
|
+
| `noSniff` | `X-Content-Type-Options: nosniff` | on |
|
|
32
|
+
| `referrerPolicy` | `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
33
|
+
|
|
34
|
+
Set any of these to `false` to omit the header. `csp` accepts a ready-made string
|
|
35
|
+
or a **directives object** whose camelCase keys become the kebab-case spelling
|
|
36
|
+
(`defaultSrc` → `default-src`, `scriptSrc` → `script-src`).
|
|
37
|
+
|
|
38
|
+
**HSTS is sticky** — once a browser sees it, it refuses plain HTTP for `maxAge`
|
|
39
|
+
seconds. Only enable it once HTTPS works everywhere, and start with a short
|
|
40
|
+
`maxAge` (say a day) while you test.
|
|
41
|
+
|
|
42
|
+
## CSRF protection
|
|
43
|
+
|
|
44
|
+
`csrf()` guards against forged form submissions. It keeps a token in the
|
|
45
|
+
[session](./sessions.md) and rejects any `POST`/`PUT`/`PATCH`/`DELETE` that
|
|
46
|
+
doesn't echo it back — with `419 Page Expired`. It needs `sessionMiddleware()`
|
|
47
|
+
installed first:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { sessionMiddleware, csrf } from "@shaferllc/keel/core";
|
|
51
|
+
|
|
52
|
+
this.use(sessionMiddleware());
|
|
53
|
+
this.use(csrf());
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### In forms
|
|
57
|
+
|
|
58
|
+
Drop `csrfField()` into any form — it renders a hidden `_token` input:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { csrfField } from "@shaferllc/keel/core";
|
|
62
|
+
|
|
63
|
+
`<form method="POST" action="/posts">
|
|
64
|
+
${csrfField()}
|
|
65
|
+
<button>Save</button>
|
|
66
|
+
</form>`
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or read the raw token with `csrfToken()` to place it yourself.
|
|
70
|
+
|
|
71
|
+
### In SPAs
|
|
72
|
+
|
|
73
|
+
`csrf()` also writes a readable `XSRF-TOKEN` cookie. Axios and most fetch wrappers
|
|
74
|
+
send it back automatically as the `X-XSRF-TOKEN` header, so AJAX requests are
|
|
75
|
+
covered with no extra code. The token is accepted from the `X-CSRF-Token` /
|
|
76
|
+
`X-XSRF-Token` header or a `_token` / `_csrf` body field.
|
|
77
|
+
|
|
78
|
+
### Exempting routes
|
|
79
|
+
|
|
80
|
+
Webhooks and provider callbacks can't send your token — exempt them (a trailing
|
|
81
|
+
`*` matches a prefix):
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
this.use(csrf({ except: ["/webhooks/*", "/payments/callback"] }));
|
|
85
|
+
```
|
package/docs/sessions.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# Sessions
|
|
2
|
+
|
|
3
|
+
Keel ships a cookie-backed session store. There's no external service to run, so
|
|
4
|
+
it works the same on Node and on the edge. Session data lives in an HTTP-only
|
|
5
|
+
cookie: the middleware reads it before your handler runs and writes it back
|
|
6
|
+
afterward.
|
|
7
|
+
|
|
8
|
+
> **The cookie is base64-encoded, not signed or encrypted.** Anyone can decode
|
|
9
|
+
> and forge it. Keep secrets out of the session (store an id, not a password
|
|
10
|
+
> hash or a role you trust for authorization on its own), and set
|
|
11
|
+
> `cookie: { secure: true }` in production so it's only sent over HTTPS.
|
|
12
|
+
|
|
13
|
+
## Enable it
|
|
14
|
+
|
|
15
|
+
Add the middleware to your HTTP kernel:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { HttpKernel, sessionMiddleware } from "@shaferllc/keel/core";
|
|
19
|
+
|
|
20
|
+
export class Kernel extends HttpKernel {
|
|
21
|
+
constructor(app: Application) {
|
|
22
|
+
super(app);
|
|
23
|
+
this.use(sessionMiddleware());
|
|
24
|
+
// options: sessionMiddleware({ cookieName: "sid", cookie: { secure: true } })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The cookie is named `keel_session` by default and is written `httpOnly`,
|
|
30
|
+
`path: "/"`, `sameSite: "Lax"`. Anything you pass in `cookie` is merged over
|
|
31
|
+
those defaults, so `cookie: { secure: true }` adds the `Secure` flag while
|
|
32
|
+
keeping the rest.
|
|
33
|
+
|
|
34
|
+
## Use it
|
|
35
|
+
|
|
36
|
+
Reach the session anywhere with `session()`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { session } from "@shaferllc/keel/core";
|
|
40
|
+
|
|
41
|
+
session().put("userId", user.id);
|
|
42
|
+
const id = session().get("userId");
|
|
43
|
+
const id2 = session().get("userId", null); // with a fallback
|
|
44
|
+
|
|
45
|
+
session().has("userId");
|
|
46
|
+
session().forget("userId");
|
|
47
|
+
session().pull("cart"); // read and remove
|
|
48
|
+
session().increment("visits");
|
|
49
|
+
session().clear();
|
|
50
|
+
session().all();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`session()` returns a fresh `Session` wrapper each call, but every wrapper points
|
|
54
|
+
at the *same* underlying data object stashed on the request context — so writes
|
|
55
|
+
made through one call are visible through the next. There's no need to hold onto
|
|
56
|
+
the instance:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
session().put("step", 1);
|
|
60
|
+
session().get("step"); // 1 — same request, same data
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Calling `session()` without `sessionMiddleware()` installed throws
|
|
64
|
+
`Session is not available. Add sessionMiddleware() to your HTTP kernel.` — the
|
|
65
|
+
guard is there so a missing middleware fails loudly instead of silently dropping
|
|
66
|
+
writes.
|
|
67
|
+
|
|
68
|
+
## Flash messages
|
|
69
|
+
|
|
70
|
+
Flash data survives exactly one request — perfect for post-redirect messages:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// during a request that redirects
|
|
74
|
+
session().flash("status", "Profile saved!");
|
|
75
|
+
return redirect("/profile");
|
|
76
|
+
|
|
77
|
+
// on the next request
|
|
78
|
+
session().flashed("status"); // "Profile saved!"
|
|
79
|
+
session().flashed("status"); // still there this request…
|
|
80
|
+
// …gone on the request after
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Flash and the values you `put` live in separate compartments. `flash(key, …)`
|
|
84
|
+
writes to a pending-flash bucket; `flashed(key)` reads the bucket that was
|
|
85
|
+
flashed on the *previous* request. So within the request that calls `flash`, a
|
|
86
|
+
`flashed` for the same key won't see it yet — it lands on the next request:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
session().flash("status", "Saved!");
|
|
90
|
+
session().flashed("status"); // undefined — not flashed until next request
|
|
91
|
+
session().get("status"); // undefined — flash isn't a normal key
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`all()` returns only your own keys — the internal flash and "old flash" buckets
|
|
95
|
+
are filtered out, so they never leak into a template's session dump.
|
|
96
|
+
|
|
97
|
+
## Counters
|
|
98
|
+
|
|
99
|
+
`increment` and `decrement` treat a missing key as `0` and step by `1` (or any
|
|
100
|
+
amount you pass):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
session().increment("visits"); // 0 -> 1
|
|
104
|
+
session().increment("credits", 10); // += 10
|
|
105
|
+
session().decrement("credits", 3); // -= 3
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Both coerce the stored value to a number, so seeding a counter with a non-numeric
|
|
109
|
+
value will produce `NaN` — keep counters numeric.
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
`sessionMiddleware()` reads the session cookie before your handler runs and
|
|
114
|
+
writes it back afterward. Data is JSON, base64-encoded into the cookie. On each
|
|
115
|
+
request it rotates flash: last request's pending flash becomes this request's
|
|
116
|
+
"old" (what `flashed` reads), and a fresh empty flash bucket starts. After your
|
|
117
|
+
handler returns, the "old" bucket is dropped and the cookie is rewritten — so a
|
|
118
|
+
value only ever survives one hop.
|
|
119
|
+
|
|
120
|
+
Because it's cookie-backed there's a ~4KB size budget — keep sessions small (an
|
|
121
|
+
id, a few flags), not whole objects. For larger sessions, swap in your own
|
|
122
|
+
middleware that persists to a store and stashes the data on the context under the
|
|
123
|
+
`"session"` key the same way.
|
|
124
|
+
|
|
125
|
+
> **Latin1 only.** Values are serialized with `btoa`, which throws on characters
|
|
126
|
+
> outside the Latin1 range. A flash message or stored string containing an emoji
|
|
127
|
+
> or many non-Latin scripts will throw `InvalidCharacterError` when the cookie is
|
|
128
|
+
> written. Keep session values ASCII/Latin1, or encode them yourself first.
|
|
129
|
+
|
|
130
|
+
## Related
|
|
131
|
+
|
|
132
|
+
Sessions underpin [authentication](./authentication.md) — the auth layer stores
|
|
133
|
+
the logged-in user's id in the session. Install `sessionMiddleware()` before any
|
|
134
|
+
middleware that reads the session.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## API reference
|
|
139
|
+
|
|
140
|
+
### `session()`
|
|
141
|
+
|
|
142
|
+
`session(): Session`
|
|
143
|
+
|
|
144
|
+
Returns the current request's `Session`, wrapping the data on the request
|
|
145
|
+
context.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
session().put("userId", 1);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Notes:** throws `Session is not available…` if `sessionMiddleware()` isn't
|
|
152
|
+
installed (no `"session"` on the context). Returns a new wrapper each call, but
|
|
153
|
+
all wrappers share the same underlying data for the request, so writes persist
|
|
154
|
+
across calls.
|
|
155
|
+
|
|
156
|
+
### `sessionMiddleware(options?)`
|
|
157
|
+
|
|
158
|
+
`sessionMiddleware(options?: SessionOptions): MiddlewareHandler`
|
|
159
|
+
|
|
160
|
+
Builds the middleware that loads the session from its cookie before the request
|
|
161
|
+
and writes it back after. Register it in your HTTP kernel.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
this.use(sessionMiddleware({ cookieName: "sid", cookie: { secure: true } }));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Notes:** cookie name defaults to `"keel_session"`. A missing or malformed
|
|
168
|
+
cookie starts an empty session (parse errors are swallowed — "tampered/expired,
|
|
169
|
+
start fresh"). The write applies `httpOnly`, `path: "/"`, `sameSite: "Lax"`,
|
|
170
|
+
merged with (and overridable by) `options.cookie`. Rotates flash on every
|
|
171
|
+
request.
|
|
172
|
+
|
|
173
|
+
### `Session`
|
|
174
|
+
|
|
175
|
+
The wrapper `session()` returns. You don't construct it directly in app code
|
|
176
|
+
(the middleware and `session()` build it), though its constructor takes the raw
|
|
177
|
+
data object. Mutating methods return `this`, so they chain.
|
|
178
|
+
|
|
179
|
+
#### `all()`
|
|
180
|
+
|
|
181
|
+
`all(): SessionData`
|
|
182
|
+
|
|
183
|
+
Returns every key you've stored, excluding the internal flash/old-flash buckets.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const data = session().all(); // Record<string, unknown>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Notes:** `SessionData` is `Record<string, unknown>`. A shallow copy of the
|
|
190
|
+
public keys — mutating the result doesn't write back to the session.
|
|
191
|
+
|
|
192
|
+
#### `get(key, fallback?)`
|
|
193
|
+
|
|
194
|
+
`get<T = unknown>(key: string, fallback?: T): T`
|
|
195
|
+
|
|
196
|
+
Reads a value, returning `fallback` when the key is absent.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const id = session().get<number>("userId");
|
|
200
|
+
const theme = session().get("theme", "light");
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Notes:** presence is checked with `key in data`, so a key explicitly set to
|
|
204
|
+
`null` returns `null` (not the fallback). The `T` type parameter is an unchecked
|
|
205
|
+
cast — it doesn't validate the runtime value.
|
|
206
|
+
|
|
207
|
+
#### `put(key, value)`
|
|
208
|
+
|
|
209
|
+
`put(key: string, value: unknown): this`
|
|
210
|
+
|
|
211
|
+
Stores a value under `key`.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
session().put("userId", user.id).put("theme", "dark");
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Notes:** chainable. Values must be JSON-serializable (they're `JSON.stringify`d
|
|
218
|
+
into the cookie) and Latin1 once stringified.
|
|
219
|
+
|
|
220
|
+
#### `set(key, value)`
|
|
221
|
+
|
|
222
|
+
`set(key: string, value: unknown): this`
|
|
223
|
+
|
|
224
|
+
Alias for `put`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
session().set("locale", "en");
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### `has(key)`
|
|
231
|
+
|
|
232
|
+
`has(key: string): boolean`
|
|
233
|
+
|
|
234
|
+
`true` when the key is present *and* non-null.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
if (session().has("userId")) { /* logged in */ }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Notes:** returns `false` for a key set to `null` or `undefined`, even though
|
|
241
|
+
`get` would still return that stored `null`.
|
|
242
|
+
|
|
243
|
+
#### `forget(key)`
|
|
244
|
+
|
|
245
|
+
`forget(key: string): this`
|
|
246
|
+
|
|
247
|
+
Deletes a key.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
session().forget("userId");
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### `pull(key, fallback?)`
|
|
254
|
+
|
|
255
|
+
`pull<T = unknown>(key: string, fallback?: T): T`
|
|
256
|
+
|
|
257
|
+
Reads a value and removes it in one step (`get` + `forget`).
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const cart = session().pull("cart", []);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Notes:** returns `fallback` when absent, same rules as `get`. Useful for
|
|
264
|
+
one-shot values you don't want lingering.
|
|
265
|
+
|
|
266
|
+
#### `increment(key, by?)`
|
|
267
|
+
|
|
268
|
+
`increment(key: string, by = 1): this`
|
|
269
|
+
|
|
270
|
+
Adds `by` to a numeric value, treating a missing key as `0`.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
session().increment("visits");
|
|
274
|
+
session().increment("credits", 25);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Notes:** coerces the current value with `as number` — non-numeric stored values
|
|
278
|
+
yield `NaN`. Chainable.
|
|
279
|
+
|
|
280
|
+
#### `decrement(key, by?)`
|
|
281
|
+
|
|
282
|
+
`decrement(key: string, by = 1): this`
|
|
283
|
+
|
|
284
|
+
Subtracts `by` from a numeric value (`increment` by `-by`).
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
session().decrement("credits", 3);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### `clear()`
|
|
291
|
+
|
|
292
|
+
`clear(): this`
|
|
293
|
+
|
|
294
|
+
Removes every key, including the flash buckets.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
session().clear();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Notes:** deletes all keys off the underlying data object, so it also wipes
|
|
301
|
+
pending and old flash. Use to reset on logout.
|
|
302
|
+
|
|
303
|
+
#### `flash(key, value)`
|
|
304
|
+
|
|
305
|
+
`flash(key: string, value: unknown): this`
|
|
306
|
+
|
|
307
|
+
Stores a value that survives only the *next* request.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
session().flash("status", "Profile saved!");
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Notes:** writes to a separate flash bucket, not the normal keyspace — `get`
|
|
314
|
+
and `all` won't see it. Read it next request with `flashed`. Chainable.
|
|
315
|
+
|
|
316
|
+
#### `flashed(key, fallback?)`
|
|
317
|
+
|
|
318
|
+
`flashed<T = unknown>(key: string, fallback?: T): T`
|
|
319
|
+
|
|
320
|
+
Reads a value flashed on the *previous* request.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const status = session().flashed("status");
|
|
324
|
+
const msg = session().flashed("msg", "");
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Notes:** reads the "old" bucket the middleware rotated in at the start of the
|
|
328
|
+
request. A value flashed and read in the same request won't appear here (it's not
|
|
329
|
+
yet rotated). Returns `fallback` when absent.
|
|
330
|
+
|
|
331
|
+
### Interfaces & types
|
|
332
|
+
|
|
333
|
+
#### `SessionOptions`
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
interface SessionOptions {
|
|
337
|
+
cookieName?: string;
|
|
338
|
+
cookie?: CookieOptions; // hono's setCookie options (Parameters<typeof setCookie>[3])
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Configures `sessionMiddleware()`. Use it to rename the cookie or set flags like
|
|
343
|
+
`secure`, `maxAge`, or `domain`.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
sessionMiddleware({
|
|
347
|
+
cookieName: "sid",
|
|
348
|
+
cookie: { secure: true, maxAge: 60 * 60 * 24 },
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Notes:** `cookieName` defaults to `"keel_session"`. `cookie` is merged *over*
|
|
353
|
+
the middleware's defaults (`httpOnly`, `path: "/"`, `sameSite: "Lax"`), so you
|
|
354
|
+
can override any of them.
|