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 +123 -0
- package/README.md +135 -38
- package/dist/index.d.ts +271 -92
- package/dist/index.js +358 -254
- package/dist/index.js.map +1 -1
- package/package.json +75 -53
- package/dist/index.cjs +0 -295
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -115
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
|
-
|
|
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
|
|
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
|
|
87
|
-
import { getIronSession } from
|
|
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
|
|
104
|
-
import { getIronSession } from
|
|
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.
|
|
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.
|
|
133
|
-
- `
|
|
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`
|