From 8741f6f7b2637be86d804cd9183443064359f17d Mon Sep 17 00:00:00 2001 From: Sefa Senturk Date: Mon, 1 Dec 2025 12:32:35 +0100 Subject: [PATCH 1/2] Add cookie chunking support for large session data This adds optional cookie chunking to handle session data that exceeds browser cookie size limits (~4KB). When enabled, large cookies are automatically split into multiple smaller cookies and reconstructed transparently when reading. Features: - Opt-in via chunking config: { enabled: true, chunkSize: 3500 } - Automatic chunking when session exceeds chunk size - Transparent reconstruction from chunks on read - Automatic cleanup of old chunks on save/destroy - Works with both Node.js req/res and CookieStore patterns - Backward compatible with existing non-chunked sessions Chunk naming pattern: {cookieName}.0, {cookieName}.1, etc. Includes comprehensive test coverage with 6 new test cases. --- README.md | 44 +++++++ src/core.ts | 301 +++++++++++++++++++++++++++++++++++++++++----- src/index.test.ts | 221 ++++++++++++++++++++++++++++++++++ 3 files changed, 536 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index efe7aded..e0af567a 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,26 @@ async function Profile() { } ``` +**For large session data (>4KB), enable cookie chunking:** + +```ts +import { getIronSession } from 'iron-session'; + +export async function POST(req, res) { + const session = await getIronSession(req, res, { + password: "...", + cookieName: "...", + chunking: { + enabled: true, + chunkSize: 3500 // optional + } + }); + + session.largeData = { /* your large session data */ }; + await session.save(); // Automatically splits into multiple cookies if needed +} +``` + ## Examples We have many different patterns and examples on the online demo, have a look: https://get-iron-session.vercel.app/. @@ -130,6 +150,22 @@ Two options are required: `password` and `cookieName`. Everything else is automa - `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use 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. - `cookieName`, **required**: Name of the cookie to be stored - `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. +- `chunking`, _optional_: Enable cookie chunking for large session data that exceeds browser cookie size limits (~4KB). When enabled, the session cookie is automatically split into multiple smaller cookies. Default to `undefined` (disabled). Options: + - `enabled`: Boolean to enable/disable chunking + - `chunkSize`: Maximum size of each chunk in bytes. Default to `3500` + + Example: + ```js + { + chunking: { + enabled: true, + chunkSize: 3500 // optional, defaults to 3500 + } + } + ``` + + When chunking is enabled and the session data exceeds the chunk size, cookies are stored as `{cookieName}.0`, `{cookieName}.1`, etc. The reconstruction happens automatically on read. Old chunks are cleaned up when the session is saved or destroyed. + - `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: ```js @@ -220,6 +256,14 @@ Not so much: Depending on your own needs and preferences, `iron-session` may or may not fit you. +### What if my session data exceeds cookie size limits? + +Browsers typically limit cookies to around 4KB. If your session data is larger, you have two options: + +1. **Enable cookie chunking** (recommended for moderate sizes): Set `chunking: { enabled: true }` in your session options. This will automatically split large cookies into multiple smaller cookies (e.g., `session.0`, `session.1`, etc.) and reconstruct them transparently when reading. + +2. **Store large data elsewhere**: For very large session data, consider storing it in a database or cache (like Redis) and only keep a session ID in the cookie. + ## Credits - [Eran Hammer and hapi.js contributors](https://github.com/hapijs/iron/graphs/contributors) diff --git a/src/core.ts b/src/core.ts index 91902ec5..142cb77a 100644 --- a/src/core.ts +++ b/src/core.ts @@ -96,6 +96,33 @@ export interface SessionOptions { * @see https://github.com/jshttp/cookie#options-1 */ cookieOptions?: CookieOptions; + + /** + * Configure cookie chunking for large sessions that exceed browser cookie size limits. + * + * When enabled, iron-session will automatically split large cookies into multiple + * smaller chunks (named `{cookieName}.0`, `{cookieName}.1`, etc.) and reassemble + * them when reading. This allows sessions larger than the typical 4096-byte browser + * limit. + * + * @example { enabled: true, chunkSize: 3500 } + */ + chunking?: { + /** + * Enable cookie chunking. When `false` or `undefined`, the traditional 4096-byte + * limit check will be enforced. + * + * @default false + */ + enabled: boolean; + /** + * Maximum size in bytes for each cookie chunk. Should be less than 4096 to allow + * room for cookie name and attributes. + * + * @default 3500 + */ + chunkSize?: number; + }; } export type IronSession = T & { @@ -199,6 +226,89 @@ function setCookie(res: ResponseType, cookieValue: string): void { ]); } +/** + * Splits a large cookie value into multiple chunks for browsers that enforce + * size limits. Each chunk is named `{baseName}.{index}`. + */ +function splitCookieIntoChunks( + value: string, + chunkSize: number, +): string[] { + const chunks: string[] = []; + for (let i = 0; i < value.length; i += chunkSize) { + chunks.push(value.slice(i, i + chunkSize)); + } + return chunks; +} + +/** + * Reconstructs a cookie value from chunks. Tries direct cookie first, + * then attempts to reassemble from indexed chunks if not found. + * Works transparently for both chunked and non-chunked cookies. + */ +function reconstructCookie( + req: RequestType, + cookieName: string, +): string { + // Try direct cookie first (for non-chunked or old sessions) + const directCookie = getCookie(req, cookieName); + if (directCookie) { + return directCookie; + } + + // Try to reconstruct from chunks + const chunks: string[] = []; + let chunkIndex = 0; + + while (true) { + const chunkName = `${cookieName}.${chunkIndex}`; + const chunk = getCookie(req, chunkName); + + if (!chunk) { + break; + } + + chunks.push(chunk); + chunkIndex++; + } + + return chunks.join(""); +} + +/** + * Reconstructs a cookie value from chunks using CookieStore. + * For use with Next.js cookies() and similar APIs. + * Works transparently for both chunked and non-chunked cookies. + */ +function reconstructCookieWithStore( + cookieHandler: CookieStore, + cookieName: string, +): string { + // Try direct cookie first + const directCookie = getServerActionCookie(cookieName, cookieHandler); + if (directCookie) { + return directCookie; + } + + // Try to reconstruct from chunks + const chunks: string[] = []; + let chunkIndex = 0; + + while (true) { + const chunkName = `${cookieName}.${chunkIndex}`; + const chunk = getServerActionCookie(chunkName, cookieHandler); + + if (!chunk) { + break; + } + + chunks.push(chunk); + chunkIndex++; + } + + return chunks.join(""); +} + export function createSealData(_crypto: Crypto) { return async function sealData( data: unknown, @@ -271,7 +381,7 @@ export function createUnsealData(_crypto: Crypto) { function getSessionConfig( sessionOptions: SessionOptions, -): Required { +): Required> & Pick { const options = { ...defaultOptions, ...sessionOptions, @@ -361,7 +471,7 @@ export function createGetIronSession( let sessionConfig = getSessionConfig(sessionOptions); - const sealFromCookies = getCookie(req, sessionConfig.cookieName); + const sealFromCookies = reconstructCookie(req, sessionConfig.cookieName); const session = sealFromCookies ? await unsealData(sealFromCookies, { password: passwordsMap, @@ -387,19 +497,72 @@ export function createGetIronSession( password: passwordsMap, ttl: sessionConfig.ttl, }); - const cookieValue = serialize( - sessionConfig.cookieName, - seal, - sessionConfig.cookieOptions, - ); - if (cookieValue.length > 4096) { - throw new Error( - `iron-session: Cookie length is too big (${cookieValue.length} bytes), browsers will refuse it. Try to remove some data.`, + // Check if chunking is enabled + if (sessionConfig.chunking?.enabled) { + const chunkSize = sessionConfig.chunking.chunkSize ?? 3500; + + // Clean up old chunks first + let oldChunkIndex = 0; + while (true) { + const oldChunkName = `${sessionConfig.cookieName}.${oldChunkIndex}`; + const oldChunk = getCookie(req, oldChunkName); + if (!oldChunk) break; + + const cleanupCookie = serialize(oldChunkName, "", { + ...sessionConfig.cookieOptions, + maxAge: 0, + }); + setCookie(res, cleanupCookie); + oldChunkIndex++; + } + + // If seal is small enough, use single cookie + const testCookieValue = serialize( + sessionConfig.cookieName, + seal, + sessionConfig.cookieOptions, ); - } - setCookie(res, cookieValue); + if (testCookieValue.length <= 4096) { + setCookie(res, testCookieValue); + return; + } + + // Split into chunks + const chunks = splitCookieIntoChunks(seal, chunkSize); + chunks.forEach((chunk, index) => { + const chunkName = `${sessionConfig.cookieName}.${index}`; + const chunkCookieValue = serialize( + chunkName, + chunk, + sessionConfig.cookieOptions, + ); + setCookie(res, chunkCookieValue); + }); + + // Delete the main cookie if it exists (we're using chunks now) + const deleteCookie = serialize(sessionConfig.cookieName, "", { + ...sessionConfig.cookieOptions, + maxAge: 0, + }); + setCookie(res, deleteCookie); + } else { + // Original behavior - no chunking + const cookieValue = serialize( + sessionConfig.cookieName, + seal, + sessionConfig.cookieOptions, + ); + + if (cookieValue.length > 4096) { + throw new Error( + `iron-session: Cookie length is too big (${cookieValue.length} bytes), browsers will refuse it. Try to remove some data.`, + ); + } + + setCookie(res, cookieValue); + } }, }, @@ -408,12 +571,30 @@ export function createGetIronSession( Object.keys(session).forEach((key) => { delete (session as Record)[key]; }); + + // Delete main cookie const cookieValue = serialize(sessionConfig.cookieName, "", { ...sessionConfig.cookieOptions, maxAge: 0, }); - setCookie(res, cookieValue); + + // Also delete chunks if chunking was enabled + if (sessionConfig.chunking?.enabled) { + let chunkIndex = 0; + while (true) { + const chunkName = `${sessionConfig.cookieName}.${chunkIndex}`; + const chunk = getCookie(req, chunkName); + if (!chunk) break; + + const chunkCookieValue = serialize(chunkName, "", { + ...sessionConfig.cookieOptions, + maxAge: 0, + }); + setCookie(res, chunkCookieValue); + chunkIndex++; + } + } }, }, }); @@ -445,10 +626,8 @@ async function getIronSessionFromCookieStore( } let sessionConfig = getSessionConfig(sessionOptions); - const sealFromCookies = getServerActionCookie( - sessionConfig.cookieName, - cookieStore, - ); + + const sealFromCookies = reconstructCookieWithStore(cookieStore, sessionConfig.cookieName); const session = sealFromCookies ? await unsealData(sealFromCookies, { password: passwordsMap, @@ -469,22 +648,70 @@ async function getIronSessionFromCookieStore( ttl: sessionConfig.ttl, }); - const cookieLength = - sessionConfig.cookieName.length + - seal.length + - JSON.stringify(sessionConfig.cookieOptions).length; + // Check if chunking is enabled + if (sessionConfig.chunking?.enabled) { + const chunkSize = sessionConfig.chunking.chunkSize ?? 3500; + + // Clean up old chunks first + let oldChunkIndex = 0; + while (true) { + const oldChunkName = `${sessionConfig.cookieName}.${oldChunkIndex}`; + const oldChunk = getServerActionCookie(oldChunkName, cookieStore); + if (!oldChunk) break; + + cookieStore.set(oldChunkName, "", { + ...sessionConfig.cookieOptions, + maxAge: 0, + }); + oldChunkIndex++; + } - if (cookieLength > 4096) { - throw new Error( - `iron-session: Cookie length is too big (${cookieLength} bytes), browsers will refuse it. Try to remove some data.`, + // If seal is small enough, use single cookie + const cookieLength = + sessionConfig.cookieName.length + + seal.length + + JSON.stringify(sessionConfig.cookieOptions).length; + + if (cookieLength <= 4096) { + cookieStore.set( + sessionConfig.cookieName, + seal, + sessionConfig.cookieOptions, + ); + return; + } + + // Split into chunks + const chunks = splitCookieIntoChunks(seal, chunkSize); + chunks.forEach((chunk, index) => { + const chunkName = `${sessionConfig.cookieName}.${index}`; + cookieStore.set(chunkName, chunk, sessionConfig.cookieOptions); + }); + + // Delete the main cookie if it exists (we're using chunks now) + cookieStore.set(sessionConfig.cookieName, "", { + ...sessionConfig.cookieOptions, + maxAge: 0, + }); + } else { + // Original behavior - no chunking + const cookieLength = + sessionConfig.cookieName.length + + seal.length + + JSON.stringify(sessionConfig.cookieOptions).length; + + if (cookieLength > 4096) { + throw new Error( + `iron-session: Cookie length is too big (${cookieLength} bytes), browsers will refuse it. Try to remove some data.`, + ); + } + + cookieStore.set( + sessionConfig.cookieName, + seal, + sessionConfig.cookieOptions, ); } - - cookieStore.set( - sessionConfig.cookieName, - seal, - sessionConfig.cookieOptions, - ); }, }, @@ -494,8 +721,22 @@ async function getIronSessionFromCookieStore( delete (session as Record)[key]; }); + // Delete main cookie const cookieOptions = { ...sessionConfig.cookieOptions, maxAge: 0 }; cookieStore.set(sessionConfig.cookieName, "", cookieOptions); + + // Also delete chunks if chunking was enabled + if (sessionConfig.chunking?.enabled) { + let chunkIndex = 0; + while (true) { + const chunkName = `${sessionConfig.cookieName}.${chunkIndex}`; + const chunk = getServerActionCookie(chunkName, cookieStore); + if (!chunk) break; + + cookieStore.set(chunkName, "", cookieOptions); + chunkIndex++; + } + } }, }, }); diff --git a/src/index.test.ts b/src/index.test.ts index a91baccd..6ffd7f5c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -500,3 +500,224 @@ await test("should work with standard web Request/Response APIs", async () => { session = await getSession(req, res, { cookieName, password }); deepEqual(session, { user: { id: 1 } }); }); + +const collectAllCookies = (res: { setHeader: { mock: { calls: Array<{ arguments: [string, string[]] }> } } }) => { + const allCookies: string[] = []; + for (const call of res.setHeader.mock.calls) { + const [, cookies] = call.arguments; + if (Array.isArray(cookies)) { + allCookies.push(...cookies); + } + } + return allCookies; +}; + +await test("should enable chunking when configured", async () => { + const res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + // Create a large session (> 4096 bytes) to trigger chunking + const largeData = "x".repeat(5000); + const session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true }, + }, + ); + session.user = { id: 1, meta: largeData }; + await session.save(); + + const allCookies = collectAllCookies(res); + + // Should create multiple chunked cookies (at least 2 chunks + 1 delete for main cookie) + equal(allCookies.length >= 2, true); + // Check chunk naming pattern + const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); + equal(chunkCookies.length >= 2, true); + match(chunkCookies[0], /^test\.0=/); + match(chunkCookies[1], /^test\.1=/); + + mock.reset(); +}); + +await test("should reconstruct session from chunked cookies", async () => { + const res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + const largeData = "y".repeat(5000); + let session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true }, + }, + ); + session.user = { id: 2, meta: largeData }; + await session.save(); + + const allCookies = collectAllCookies(res); + const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); + + // Build cookie header with all chunks + const cookieHeader = chunkCookies.map((c: string) => c.split(";")[0]).join("; "); + + // Read session back - should reconstruct from chunks + const req = { headers: { cookie: cookieHeader } } as IncomingMessage; + session = await getSession(req, res as unknown as ServerResponse, { + cookieName, + password, + chunking: { enabled: true }, + }); + + equal(session.user?.id, 2); + equal(session.user?.meta, largeData); + + mock.reset(); +}); + +await test("should use single cookie when data is small with chunking enabled", async () => { + const res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + const session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true }, + }, + ); + session.user = { id: 3 }; // Small data + await session.save(); + + const allCookies = collectAllCookies(res); + // Should use single cookie for small data + equal(allCookies.length, 1); + match(allCookies[0], /^test=/); // Not chunked + doesNotMatch(allCookies[0], /^test\.0=/); // No chunk suffix + + mock.reset(); +}); + +await test("should clean up old chunks when updating session", async () => { + let res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + // First save with large data (creates chunks) + const largeData = "z".repeat(5000); + let session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true }, + }, + ); + session.user = { id: 4, meta: largeData }; + await session.save(); + + const firstCookies = collectAllCookies(res); + const firstChunkCookies = firstCookies.filter(c => /^test\.\d+=/.test(c)); + const firstChunkCount = firstChunkCookies.length; + + // Build cookie header + const cookieHeader = firstChunkCookies.map((c: string) => c.split(";")[0]).join("; "); + + // Reset mock and update getHeader to return existing cookies + res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + // Update with smaller data + const req = { headers: { cookie: cookieHeader } } as IncomingMessage; + session = await getSession(req, res as unknown as ServerResponse, { + cookieName, + password, + chunking: { enabled: true }, + }); + session.user = { id: 5 }; // Small data now + await session.save(); + + const secondCookies = collectAllCookies(res); + + // Should have cleanup cookies (maxAge=0) for old chunks plus the new single cookie + equal(secondCookies.length, firstChunkCount + 1); + // Old chunks should be deleted (maxAge=0) + for (let i = 0; i < firstChunkCount; i++) { + match(secondCookies[i], /Max-Age=0/); + } + + mock.reset(); +}); + +await test("should destroy all chunks on session destroy", async () => { + let res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + // Create chunked session + const largeData = "a".repeat(5000); + let session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true }, + }, + ); + session.user = { id: 6, meta: largeData }; + await session.save(); + + const saveCookies = collectAllCookies(res); + const saveChunkCookies = saveCookies.filter(c => /^test\.\d+=/.test(c)); + const chunkCount = saveChunkCookies.length; + + // Build cookie header + const cookieHeader = saveChunkCookies.map((c: string) => c.split(";")[0]).join("; "); + + // Reset mock + res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + // Destroy session + const req = { headers: { cookie: cookieHeader } } as IncomingMessage; + session = await getSession(req, res as unknown as ServerResponse, { + cookieName, + password, + chunking: { enabled: true }, + }); + session.destroy(); + + const destroyCookies = collectAllCookies(res); + + // Should delete main cookie + all chunks + equal(destroyCookies.length, chunkCount + 1); + // All should have maxAge=0 + for (const cookie of destroyCookies) { + match(cookie, /Max-Age=0/); + } + + mock.reset(); +}); + +await test("should respect custom chunk size", async () => { + const res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; + + const mediumData = "b".repeat(3000); + const session = await getSession( + { headers: {} } as Request, + res as unknown as ServerResponse, + { + cookieName, + password, + chunking: { enabled: true, chunkSize: 2000 }, // Custom smaller chunk size + }, + ); + session.user = { id: 7, meta: mediumData }; + await session.save(); + + const allCookies = collectAllCookies(res); + const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); + // With 2000 byte chunks, 3000 bytes of data should create multiple chunks + equal(chunkCookies.length > 1, true); + + mock.reset(); +}); From 247968d4b48774a2cfe0bf8d5ecbd4fd32fcc64e Mon Sep 17 00:00:00 2001 From: Sefa Senturk Date: Mon, 1 Dec 2025 20:56:56 +0100 Subject: [PATCH 2/2] Fix TypeScript errors in test file - Add type assertions (as any) to collectAllCookies calls to fix mock type mismatch - Add non-null assertions for array access in test assertions - Fixes prepare script errors during npm install --- src/index.test.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 6ffd7f5c..7795f45f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -529,15 +529,15 @@ await test("should enable chunking when configured", async () => { session.user = { id: 1, meta: largeData }; await session.save(); - const allCookies = collectAllCookies(res); + const allCookies = collectAllCookies(res as any); // Should create multiple chunked cookies (at least 2 chunks + 1 delete for main cookie) equal(allCookies.length >= 2, true); // Check chunk naming pattern const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); equal(chunkCookies.length >= 2, true); - match(chunkCookies[0], /^test\.0=/); - match(chunkCookies[1], /^test\.1=/); + match(chunkCookies[0]!, /^test\.0=/); + match(chunkCookies[1]!, /^test\.1=/); mock.reset(); }); @@ -557,11 +557,11 @@ await test("should reconstruct session from chunked cookies", async () => { session.user = { id: 2, meta: largeData }; await session.save(); - const allCookies = collectAllCookies(res); + const allCookies = collectAllCookies(res as any); const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); // Build cookie header with all chunks - const cookieHeader = chunkCookies.map((c: string) => c.split(";")[0]).join("; "); + const cookieHeader = chunkCookies.map((c: string) => c.split(";")[0]!).join("; "); // Read session back - should reconstruct from chunks const req = { headers: { cookie: cookieHeader } } as IncomingMessage; @@ -592,11 +592,11 @@ await test("should use single cookie when data is small with chunking enabled", session.user = { id: 3 }; // Small data await session.save(); - const allCookies = collectAllCookies(res); + const allCookies = collectAllCookies(res as any); // Should use single cookie for small data equal(allCookies.length, 1); - match(allCookies[0], /^test=/); // Not chunked - doesNotMatch(allCookies[0], /^test\.0=/); // No chunk suffix + match(allCookies[0]!, /^test=/); // Not chunked + doesNotMatch(allCookies[0]!, /^test\.0=/); // No chunk suffix mock.reset(); }); @@ -618,12 +618,12 @@ await test("should clean up old chunks when updating session", async () => { session.user = { id: 4, meta: largeData }; await session.save(); - const firstCookies = collectAllCookies(res); + const firstCookies = collectAllCookies(res as any); const firstChunkCookies = firstCookies.filter(c => /^test\.\d+=/.test(c)); const firstChunkCount = firstChunkCookies.length; // Build cookie header - const cookieHeader = firstChunkCookies.map((c: string) => c.split(";")[0]).join("; "); + const cookieHeader = firstChunkCookies.map((c: string) => c.split(";")[0]!).join("; "); // Reset mock and update getHeader to return existing cookies res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; @@ -638,13 +638,13 @@ await test("should clean up old chunks when updating session", async () => { session.user = { id: 5 }; // Small data now await session.save(); - const secondCookies = collectAllCookies(res); + const secondCookies = collectAllCookies(res as any); // Should have cleanup cookies (maxAge=0) for old chunks plus the new single cookie equal(secondCookies.length, firstChunkCount + 1); // Old chunks should be deleted (maxAge=0) for (let i = 0; i < firstChunkCount; i++) { - match(secondCookies[i], /Max-Age=0/); + match(secondCookies[i]!, /Max-Age=0/); } mock.reset(); @@ -667,12 +667,12 @@ await test("should destroy all chunks on session destroy", async () => { session.user = { id: 6, meta: largeData }; await session.save(); - const saveCookies = collectAllCookies(res); + const saveCookies = collectAllCookies(res as any); const saveChunkCookies = saveCookies.filter(c => /^test\.\d+=/.test(c)); const chunkCount = saveChunkCookies.length; // Build cookie header - const cookieHeader = saveChunkCookies.map((c: string) => c.split(";")[0]).join("; "); + const cookieHeader = saveChunkCookies.map((c: string) => c.split(";")[0]!).join("; "); // Reset mock res = { getHeader: mock.fn(() => []), setHeader: mock.fn() }; @@ -686,13 +686,13 @@ await test("should destroy all chunks on session destroy", async () => { }); session.destroy(); - const destroyCookies = collectAllCookies(res); + const destroyCookies = collectAllCookies(res as any); // Should delete main cookie + all chunks equal(destroyCookies.length, chunkCount + 1); // All should have maxAge=0 for (const cookie of destroyCookies) { - match(cookie, /Max-Age=0/); + match(cookie!, /Max-Age=0/); } mock.reset(); @@ -714,7 +714,7 @@ await test("should respect custom chunk size", async () => { session.user = { id: 7, meta: mediumData }; await session.save(); - const allCookies = collectAllCookies(res); + const allCookies = collectAllCookies(res as any); const chunkCookies = allCookies.filter(c => /^test\.\d+=/.test(c)); // With 2000 byte chunks, 3000 bytes of data should create multiple chunks equal(chunkCookies.length > 1, true);