iron-session 8.0.3 → 9.0.0-beta.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/MIGRATION.md ADDED
@@ -0,0 +1,123 @@
1
+ # Migration guide
2
+
3
+ ## v8 to v9
4
+
5
+ Most apps need two changes: Node 22 and one line if you store a `Date` in the
6
+ session. Everything else below is either a type error TypeScript will point at,
7
+ or a bug fix you want.
8
+
9
+ ### Requirements
10
+
11
+ - **Node 22.12 or later.** Node 20 reached end of life in April 2026.
12
+ - **The package is ESM-only.** There is no CommonJS build. `require()` still
13
+ works on Node 22.12+, which supports `require()` of an ES module, so most CJS
14
+ code keeps working. If you are bundling for an older target, keep v8.
15
+
16
+ ### Do this
17
+
18
+ **1. Store timestamps, not `Date` objects.**
19
+
20
+ ```diff
21
+ - session.lastSeen = new Date();
22
+ + session.lastSeen = Date.now();
23
+ ```
24
+
25
+ v8 quietly turned a `Date` into an ISO string when sealing and handed you back a
26
+ string when reading, so the type you wrote out was not the type you got back.
27
+ v9 throws with a message naming the fix instead.
28
+
29
+ **2. Check for missing session data.**
30
+
31
+ `IronSession<T>` properties are optional on read now, so this stops compiling:
32
+
33
+ ```diff
34
+ - const userId = session.user.id;
35
+ + const userId = session.user?.id;
36
+ ```
37
+
38
+ A session that does not exist yet is an empty object. The old type claimed
39
+ otherwise, so `session.user.id` compiled and threw on a first visit, on an
40
+ expired cookie, and after `destroy()`.
41
+
42
+ **3. Do not `save()` after `destroy()`.**
43
+
44
+ `destroy()` is terminal now, and `save()` after it throws. Before, the save
45
+ re-sealed the session and the browser kept the last `Set-Cookie`, so the logout
46
+ silently did not happen.
47
+
48
+ **4. Pass a real password to `updateConfig()`.**
49
+
50
+ `updateConfig()` now rebuilds the whole configuration, including the password,
51
+ and validates it. Before, a new password passed here was ignored: it kept
52
+ sealing with the old one and skipped the 32-character check. If you were
53
+ relying on that, you were not rotating anything.
54
+
55
+ ### Nothing to do
56
+
57
+ - **`getIronSession(req, res, options)` still works.** Express, Connect, plain
58
+ Node and pages-router API routes need no change.
59
+ - **`getIronSession(await cookies(), options)` still works**, and now typechecks
60
+ without a cast. If you had `as any` or
61
+ `as unknown as CookieStore` there, delete it.
62
+ - **Cookies stay valid.** v9 reads v8 cookies, and v8 reads v9 cookies, so you
63
+ can roll back a deploy without signing everyone out.
64
+
65
+ ### Removed
66
+
67
+ - **Pre-v8 cookies (iron-session v6 and earlier) are no longer read.** Those
68
+ users sign in once more. The format was selected by a version marker that sat
69
+ outside the seal's signature, which meant an attacker could flip it and
70
+ reshape the session without knowing the password.
71
+ - **`createSealData`, `createUnsealData` and `createGetIronSession` are gone.**
72
+ They existed to inject a `crypto` implementation, which is no longer a thing
73
+ anyone needs to pass. Import `sealData`, `unsealData` and `getIronSession`
74
+ directly.
75
+ - **`uncrypto` is no longer a dependency.** If you were pinning or aliasing it to
76
+ work around bundler problems, most notably `getRandomValues is not a function`
77
+ under Turbopack, you can drop that.
78
+
79
+ ### New
80
+
81
+ - **`onUnsealError(reason, error)`** tells you when a cookie could not be read
82
+ and why (`"expired"`, `"invalid"`, `"unknown-password"`). Worth wiring to your
83
+ logger: a burst of `"unknown-password"` means a broken password rotation, and
84
+ a burst of `"invalid"` can mean someone is probing your cookies.
85
+ - **`chunk: true`** splits a session that does not fit in one cookie across
86
+ several. Read the size warnings in the README first.
87
+ - **Adapters** for the runtimes that needed them:
88
+ - `nextProxyCookies(request, response)` for Next.js `proxy.ts` (called
89
+ `middleware.ts` before Next 16). This is the fix if saving a session in
90
+ middleware never seemed to take effect.
91
+ - `nodeCookies(req, res)` and `webCookies(request, responseOrHeaders)` if you
92
+ want to be explicit instead of relying on the `(req, res, options)` form.
93
+
94
+ ### Rotating a session in Next.js middleware
95
+
96
+ This did not work before. A cookie written in middleware only reaches the
97
+ current render when it goes through `response.cookies.set()`, so `session.save()`
98
+ appeared to succeed and then vanished.
99
+
100
+ ```ts
101
+ // proxy.ts (middleware.ts before Next 16)
102
+ import { NextResponse, type NextRequest } from "next/server";
103
+ import { getIronSession, nextProxyCookies } from "iron-session";
104
+
105
+ export async function proxy(request: NextRequest) {
106
+ const response = NextResponse.next();
107
+ const session = await getIronSession(nextProxyCookies(request, response), options);
108
+
109
+ session.lastSeen = Date.now();
110
+ await session.save();
111
+
112
+ return response;
113
+ }
114
+ ```
115
+
116
+ ## v6 to v8
117
+
118
+ See the [v8 release notes](https://github.com/vvo/iron-session/releases/tag/v8.0.0).
119
+ The short version: `withIronSessionApiRoute` and `withIronSessionSsr` are gone,
120
+ call `getIronSession(req, res, options)` directly instead.
121
+
122
+ Going from v6 straight to v9 works, but those cookies are not readable by v9, so
123
+ plan for everyone to sign in once.
package/README.md CHANGED
@@ -2,32 +2,13 @@
2
2
 
3
3
  **`iron-session` is a secure, stateless, and cookie-based session library for JavaScript.**
4
4
 
5
- <div align="right"><sub><i>our sponsor:</i></sup></div>
6
-
7
- ---
8
- <div align="center">
9
- <a href="https://clerk.com">
10
- <picture>
11
- <source width="200px" media="(prefers-color-scheme: dark)" srcset="./sponsor/clerk-light.svg">
12
- <source width="200px" media="(prefers-color-scheme: light)" srcset="./sponsor/clerk-dark.svg">
13
- <img width="200px" src="./sponsor/clerk-dark.svg" />
14
- </picture>
15
- </a>
16
- <p align="center">Clerk is a complete suite of embeddable UIs, flexible APIs, and admin dashboards to authenticate and manage your users.</p>
17
- <p align="center">
18
- <a href="https://go.clerk.com/198WZf0">
19
- <b>Add authentication in 7 minutes 👉</b>
20
- </a>
21
- </p>
22
- </div>
23
-
24
5
  ---
25
6
 
26
7
  The session data is stored in signed and encrypted cookies which are decoded by your server code in a stateless fashion (= no network involved). This is the same technique used by frameworks like
27
8
  [Ruby On Rails](https://guides.rubyonrails.org/security.html#session-storage).
28
9
 
29
10
  <p align="center"><i>Online demo and examples: <a href="https://get-iron-session.vercel.app/">https://get-iron-session.vercel.app</a></i> 👀 <br/>
30
- <i>Featured in the <a href="https://nextjs.org/docs/authentication">Next.js documentation</a></i> ⭐️</p>
11
+ <i>Featured in the <a href="https://nextjs.org/docs/app/guides/authentication">Next.js documentation</a></i> ⭐️</p>
31
12
 
32
13
  ## Table of Contents
33
14
 
@@ -35,6 +16,10 @@ The session data is stored in signed and encrypted cookies which are decoded by
35
16
  - [Installation](#installation)
36
17
  - [Usage](#usage)
37
18
  - [Examples](#examples)
19
+ - [Runtimes](#runtimes)
20
+ - [Session size](#session-size)
21
+ - [Watching for unreadable cookies](#watching-for-unreadable-cookies)
22
+ - [Validating session data](#validating-session-data)
38
23
  - [Project status](#project-status)
39
24
  - [Session options](#session-options)
40
25
  - [API](#api)
@@ -61,13 +46,13 @@ pnpm add iron-session
61
46
 
62
47
  ## Usage
63
48
 
64
- *We have extensive examples here too: https://get-iron-session.vercel.app/.*
49
+ _We have extensive examples here too: https://get-iron-session.vercel.app/._
65
50
 
66
51
  To get a session, there's a single method to know: `getIronSession`.
67
52
 
68
53
  ```ts
69
54
  // Next.js API Routes and Node.js/Express/Connect.
70
- import { getIronSession } from 'iron-session';
55
+ import { getIronSession } from "iron-session";
71
56
 
72
57
  export async function get(req, res) {
73
58
  const session = await getIronSession(req, res, { password: "...", cookieName: "..." });
@@ -83,16 +68,16 @@ export async function post(req, res) {
83
68
 
84
69
  ```ts
85
70
  // Next.js Route Handlers (App Router)
86
- import { cookies } from 'next/headers';
87
- import { getIronSession } from 'iron-session';
71
+ import { cookies } from "next/headers";
72
+ import { getIronSession } from "iron-session";
88
73
 
89
74
  export async function GET() {
90
- const session = await getIronSession(cookies(), { password: "...", cookieName: "..." });
75
+ const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
91
76
  return session;
92
77
  }
93
78
 
94
79
  export async function POST() {
95
- const session = await getIronSession(cookies(), { password: "...", cookieName: "..." });
80
+ const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
96
81
  session.username = "Alison";
97
82
  await session.save();
98
83
  }
@@ -100,12 +85,12 @@ export async function POST() {
100
85
 
101
86
  ```tsx
102
87
  // Next.js Server Components and Server Actions (App Router)
103
- import { cookies } from 'next/headers';
104
- import { getIronSession } from 'iron-session';
88
+ import { cookies } from "next/headers";
89
+ import { getIronSession } from "iron-session";
105
90
 
106
91
  async function getIronSessionData() {
107
- const session = await getIronSession(cookies(), { password: "...", cookieName: "..." });
108
- return session
92
+ const session = await getIronSession(await cookies(), { password: "...", cookieName: "..." });
93
+ return session;
109
94
  }
110
95
 
111
96
  async function Profile() {
@@ -115,22 +100,134 @@ async function Profile() {
115
100
  }
116
101
  ```
117
102
 
103
+ ```ts
104
+ // Next.js proxy.ts (middleware.ts before Next 16)
105
+ import { NextResponse, type NextRequest } from "next/server";
106
+ import { getIronSession, nextProxyCookies } from "iron-session";
107
+
108
+ export async function proxy(request: NextRequest) {
109
+ const response = NextResponse.next();
110
+ const session = await getIronSession(nextProxyCookies(request, response), options);
111
+
112
+ session.lastSeen = Date.now();
113
+ await session.save();
114
+
115
+ return response;
116
+ }
117
+ ```
118
+
119
+ Middleware needs the adapter because Next only merges a cookie into the current
120
+ render when it goes through `response.cookies.set()`. Writing a raw `Set-Cookie`
121
+ header there looks like it works and then has no effect.
122
+
118
123
  ## Examples
119
124
 
120
125
  We have many different patterns and examples on the online demo, have a look: https://get-iron-session.vercel.app/.
121
126
 
127
+ ## Runtimes
128
+
129
+ `getIronSession(req, res, options)` covers Node, Express, Connect and Next.js
130
+ API routes, and `getIronSession(await cookies(), options)` covers the Next.js App
131
+ Router. When you want to be explicit, or when your framework hands you something
132
+ else, pass an adapter instead:
133
+
134
+ | Adapter | For |
135
+ | ---------------------------------------- | -------------------------------------------------------------------------- |
136
+ | `nodeCookies(req, res)` | Node `http`, Express, Connect, Next.js API routes |
137
+ | `webCookies(request, responseOrHeaders)` | Anything web-standard: Hono, Bun, Deno, Cloudflare Workers, Route Handlers |
138
+ | `nextProxyCookies(request, response)` | Next.js `proxy.ts` / `middleware.ts` |
139
+
140
+ Anything with `get(name)` and `set(name, value, options)`, like Next's
141
+ `cookies()`, can be passed directly. If your framework has neither, a cookie jar
142
+ is two functions:
143
+
144
+ ```ts
145
+ const session = await getIronSession(
146
+ {
147
+ read: (name) => myFramework.getCookie(name),
148
+ write: (name, value, options) => myFramework.setCookie(name, value, options),
149
+ },
150
+ options,
151
+ );
152
+ ```
153
+
154
+ ## Session size
155
+
156
+ A browser refuses a cookie over 4096 bytes, and iron-session throws rather than
157
+ letting one be silently dropped. Encryption adds overhead, so plan for roughly
158
+ 3KB of actual data.
159
+
160
+ If you need more, `chunk: true` splits the session across several cookies. Before
161
+ you reach for it, know what the real limit is: every cookie is sent on **every
162
+ request**, and proxies cap the whole `Cookie` header well below what a few
163
+ chunks produce. nginx allows 8KB by default and a CDN in front of it may allow
164
+ less. Going over returns a 400 or 431 at the edge, before your code runs.
165
+ iron-session refuses more than 4 chunks for that reason.
166
+
167
+ The scalable answer is to keep an id in the session and the data in your
168
+ database:
169
+
170
+ ```ts
171
+ session.userId = user.id; // small, stateless
172
+ const user = await db.user.findUnique({ where: { id: session.userId } });
173
+ ```
174
+
175
+ ## Watching for unreadable cookies
176
+
177
+ When a cookie cannot be read, iron-session starts a new empty session instead of
178
+ throwing. It has to: it cannot tell a tampered cookie from a password you
179
+ rotated out or a seal that simply expired, and a 500 on every request would be
180
+ worse. That makes real problems invisible, so log them:
181
+
182
+ ```ts
183
+ const options = {
184
+ cookieName: "session",
185
+ password: process.env.SESSION_PASSWORD,
186
+ onUnsealError: (reason, error) => {
187
+ // "expired" is normal, that is how sessions end.
188
+ if (reason !== "expired") {
189
+ logger.warn({ reason, error }, "session cookie rejected");
190
+ }
191
+ },
192
+ };
193
+ ```
194
+
195
+ A burst of `"unknown-password"` usually means a password rotation went wrong. A
196
+ burst of `"invalid"` can mean someone is probing your cookies.
197
+
198
+ ## Validating session data
199
+
200
+ There is no `validate` option, on purpose. If you change the shape of your
201
+ session, old cookies still decrypt into the old shape, and the place to handle
202
+ that is the wrapper you already have:
203
+
204
+ ```ts
205
+ // lib/session.ts
206
+ export async function getSession() {
207
+ const session = await getIronSession<Session>(await cookies(), options);
208
+
209
+ if (session.user && !SessionSchema.safeParse({ ...session }).success) {
210
+ session.destroy();
211
+ }
212
+
213
+ return session;
214
+ }
215
+ ```
216
+
122
217
  ## Project status
123
218
 
124
219
  ✅ Production ready and maintained.
125
220
 
126
221
  ## Session options
127
222
 
128
- Two options are required: `password` and `cookieName`. Everything else is automatically computed and usually doesn't need to be changed.****
223
+ Two options are required: `password` and `cookieName`. Everything else is automatically computed and usually doesn't need to be changed.
129
224
 
130
- - `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use <https://1password.com/password-generator/> to generate strong passwords. `password` can be either a `string` or an `object` with incrementing keys like this: `{2: "...", 1: "..."}` to allow for password rotation. iron-session will use the highest numbered key for new cookies.
225
+ - `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use <https://1password.com/password-generator/> to generate strong passwords. `password` can be either a `string` or an `object` with incrementing keys like this: `{2: "...", 1: "..."}` to allow for password rotation. iron-session will use the highest numbered key for new cookies.
131
226
  - `cookieName`, **required**: Name of the cookie to be stored
132
- - `ttl`, _optional_: In seconds. Default to the equivalent of 14 days. You can set this to `0` and iron-session will compute the maximum allowed value by cookies.
133
- - `cookieOptions`, _optional_: Any option available from [jshttp/cookie#serialize](https://github.com/jshttp/cookie#cookieserializename-value-options) except for `encode` which is not a Set-Cookie Attribute. See [Mozilla Set-Cookie Attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) and [Chrome Cookie Fields](https://developer.chrome.com/docs/devtools/application/cookies/#fields). Default to:
227
+ - `ttl`, _optional_: In seconds. Default to the equivalent of 14 days. Setting it to `0` means the seal never expires, which also means it can never be revoked: do not use `0` for authentication.
228
+ - `chunk`, _optional_: Split a session that does not fit in one cookie across several cookies. Defaults to `false`. See [Session size](#session-size) before turning it on.
229
+ - `onUnsealError`, _optional_: Called when an existing cookie could not be read, with a reason of `"expired"`, `"invalid"` or `"unknown-password"`. The session is reset to empty either way, so this is for logging. See [Watching for unreadable cookies](#watching-for-unreadable-cookies).
230
+ - `cookieOptions`, _optional_: Any [Set-Cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) supported by [jshttp/cookie](https://github.com/jshttp/cookie). Default to:
134
231
 
135
232
  ```js
136
233
  {
@@ -149,7 +246,7 @@ Two options are required: `password` and `cookieName`. Everything else is automa
149
246
  ```ts
150
247
  type SessionData = {
151
248
  // Your data
152
- }
249
+ };
153
250
 
154
251
  const session = await getIronSession<SessionData>(req, res, sessionOptions);
155
252
  ```
@@ -159,7 +256,7 @@ const session = await getIronSession<SessionData>(req, res, sessionOptions);
159
256
  ```ts
160
257
  type SessionData = {
161
258
  // Your data
162
- }
259
+ };
163
260
 
164
261
  const session = await getIronSession<SessionData>(cookies(), sessionOptions);
165
262
  ```
@@ -169,7 +266,7 @@ const session = await getIronSession<SessionData>(cookies(), sessionOptions);
169
266
  Saves the session. This is an asynchronous operation. It must be done and awaited before headers are sent to the client.
170
267
 
171
268
  ```ts
172
- await session.save()
269
+ await session.save();
173
270
  ```
174
271
 
175
272
  ### `session.destroy(): void`
@@ -177,7 +274,7 @@ await session.save()
177
274
  Destroys the session. This is a synchronous operation as it only removes the cookie. It must be done before headers are sent to the client.
178
275
 
179
276
  ```ts
180
- session.destroy()
277
+ session.destroy();
181
278
  ```
182
279
 
183
280
  ### `session.updateConfig(sessionOptions: SessionOptions): void`