@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
@@ -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).
@@ -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
+ ```
@@ -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.