mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 16:36:34 -05:00
feat: session store chunking (#5645)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -14,37 +14,17 @@ import { APIError } from "better-call";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
deleteSessionCookie,
|
||||
getChunkedCookie,
|
||||
setCookieCache,
|
||||
setSessionCookie,
|
||||
} from "../../cookies";
|
||||
import { symmetricDecodeJWT } from "../../crypto/jwt";
|
||||
import { getSessionQuerySchema } from "../../cookies/session-store";
|
||||
import { symmetricDecodeJWT } from "../../crypto";
|
||||
import type { InferSession, InferUser, Session, User } from "../../types";
|
||||
import type { Prettify } from "../../types/helper";
|
||||
import { getDate } from "../../utils/date";
|
||||
import { safeJSONParse } from "../../utils/json";
|
||||
|
||||
export const getSessionQuerySchema = z.optional(
|
||||
z.object({
|
||||
/**
|
||||
* If cookie cache is enabled, it will disable the cache
|
||||
* and fetch the session from the database
|
||||
*/
|
||||
disableCookieCache: z.coerce
|
||||
.boolean()
|
||||
.meta({
|
||||
description: "Disable cookie cache and fetch session from database",
|
||||
})
|
||||
.optional(),
|
||||
disableRefresh: z.coerce
|
||||
.boolean()
|
||||
.meta({
|
||||
description:
|
||||
"Disable session refresh. Useful for checking session status, without updating the session",
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const getSession = <Option extends BetterAuthOptions>() =>
|
||||
createAuthEndpoint(
|
||||
"/get-session",
|
||||
@@ -95,7 +75,8 @@ export const getSession = <Option extends BetterAuthOptions>() =>
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionDataCookie = ctx.getCookie(
|
||||
const sessionDataCookie = getChunkedCookie(
|
||||
ctx,
|
||||
ctx.context.authCookies.sessionData.name,
|
||||
);
|
||||
|
||||
|
||||
@@ -369,17 +369,8 @@ describe("getSessionCookie", async () => {
|
||||
await expect(getCookieCache(request)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should log error and skip setting cookie when data exceeds size limit", async () => {
|
||||
const loggerErrors: string[] = [];
|
||||
const mockLogger = {
|
||||
log: (level: string, message: string) => {
|
||||
if (level === "error") {
|
||||
loggerErrors.push(message);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const { auth } = await getTestInstance({
|
||||
it("should chunk large cookies instead of logging error", async () => {
|
||||
const { client, testUser } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
user: {
|
||||
additionalFields: {
|
||||
@@ -402,35 +393,46 @@ describe("getSessionCookie", async () => {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
logger: mockLogger,
|
||||
});
|
||||
|
||||
// Create a very large string that will exceed the cookie size limit when combined with session data
|
||||
// The limit is 4093 bytes, so we create data that will definitely exceed it
|
||||
const largeString = "x".repeat(2000);
|
||||
|
||||
const headers = new Headers();
|
||||
let hasCookieChunks = false;
|
||||
|
||||
// Sign up with large user data using the server API
|
||||
const result = await auth.api.signUpEmail({
|
||||
body: {
|
||||
await client.signUp.email(
|
||||
{
|
||||
name: "Test User",
|
||||
email: "large-data-test@example.com",
|
||||
password: "password123",
|
||||
customField1: largeString,
|
||||
customField2: largeString,
|
||||
customField3: largeString,
|
||||
} as any,
|
||||
{
|
||||
onSuccess(context) {
|
||||
const setCookie = context.response.headers.get("set-cookie");
|
||||
if (setCookie) {
|
||||
const parsed = parseSetCookieHeader(setCookie);
|
||||
parsed.forEach((value, name) => {
|
||||
if (
|
||||
name.includes("session_data.0") ||
|
||||
name.includes("session_data.1")
|
||||
) {
|
||||
hasCookieChunks = true;
|
||||
}
|
||||
headers.append("cookie", `${name}=${value.value}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that logger recorded an error about exceeding size limit
|
||||
const sizeError = loggerErrors.find((msg) =>
|
||||
msg.includes("Session data exceeds cookie size limit"),
|
||||
);
|
||||
expect(sizeError).toBeDefined();
|
||||
expect(sizeError).toContain("4093 bytes");
|
||||
|
||||
// The sign up should still succeed
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.user).toBeDefined();
|
||||
// Verify that chunking happened (instead of logging an error and not caching)
|
||||
expect(hasCookieChunks).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -827,3 +829,265 @@ describe("Cookie Cache Field Filtering", () => {
|
||||
expect(cache?.session?.token).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cookie Chunking", () => {
|
||||
it("should chunk cookies when they exceed 4KB", async () => {
|
||||
// Create a large string that will exceed the cookie size limit
|
||||
const largeString = "x".repeat(2000);
|
||||
|
||||
const { client, cookieSetter } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
user: {
|
||||
additionalFields: {
|
||||
field1: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
field2: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
// Sign up with large user data
|
||||
await client.signUp.email(
|
||||
{
|
||||
name: "Test User",
|
||||
email: "chunk-test@example.com",
|
||||
password: "password123",
|
||||
field1: largeString,
|
||||
field2: largeString,
|
||||
} as any,
|
||||
{
|
||||
onSuccess(context) {
|
||||
const setCookie = context.response.headers.get("set-cookie");
|
||||
expect(setCookie).toBeDefined();
|
||||
|
||||
// Parse set-cookie header to check for chunks
|
||||
const parsed = parseSetCookieHeader(setCookie!);
|
||||
let hasChunks = false;
|
||||
|
||||
// Check if we have chunked cookies
|
||||
parsed.forEach((value, name) => {
|
||||
if (
|
||||
name.includes("session_data.0") ||
|
||||
name.includes("session_data.1")
|
||||
) {
|
||||
hasChunks = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasChunks).toBe(true);
|
||||
|
||||
// Set cookies in headers for next request
|
||||
parsed.forEach((value, name) => {
|
||||
headers.append("cookie", `${name}=${value.value}`);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Now verify we can read it back
|
||||
const request = new Request("https://example.com/api/auth/session", {
|
||||
headers,
|
||||
});
|
||||
|
||||
const cache = await getCookieCache(request, {
|
||||
secret: "better-auth.secret",
|
||||
});
|
||||
|
||||
expect(cache).not.toBeNull();
|
||||
expect(cache?.user?.email).toEqual("chunk-test@example.com");
|
||||
expect(cache?.session?.token).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("should reconstruct chunked cookies correctly", async () => {
|
||||
const largeString = "y".repeat(2500);
|
||||
|
||||
const { client, cookieSetter } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
user: {
|
||||
additionalFields: {
|
||||
largeField: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
await client.signUp.email(
|
||||
{
|
||||
name: "Large Data User",
|
||||
email: "large-chunk-test@example.com",
|
||||
password: "password123",
|
||||
largeField: largeString,
|
||||
} as any,
|
||||
{
|
||||
onSuccess: cookieSetter(headers),
|
||||
},
|
||||
);
|
||||
|
||||
const request = new Request("https://example.com/api/auth/session", {
|
||||
headers,
|
||||
});
|
||||
|
||||
const cache = await getCookieCache(request, {
|
||||
secret: "better-auth.secret",
|
||||
});
|
||||
|
||||
expect(cache).not.toBeNull();
|
||||
expect(cache?.user?.email).toEqual("large-chunk-test@example.com");
|
||||
expect(cache?.user?.largeField).toEqual(largeString);
|
||||
});
|
||||
|
||||
it("should clean up all chunks when deleting session", async () => {
|
||||
const largeString = "z".repeat(2000);
|
||||
|
||||
const { client } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
user: {
|
||||
additionalFields: {
|
||||
field1: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
field2: {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
// Sign up with large data to create chunks
|
||||
await client.signUp.email(
|
||||
{
|
||||
name: "Delete Test User",
|
||||
email: "delete-chunk-test@example.com",
|
||||
password: "password123",
|
||||
field1: largeString,
|
||||
field2: largeString,
|
||||
} as any,
|
||||
{
|
||||
onSuccess(context) {
|
||||
const setCookie = context.response.headers.get("set-cookie");
|
||||
if (setCookie) {
|
||||
const parsed = parseSetCookieHeader(setCookie);
|
||||
parsed.forEach((value, name) => {
|
||||
headers.append("cookie", `${name}=${value.value}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Sign out
|
||||
await client.signOut({
|
||||
fetchOptions: {
|
||||
headers,
|
||||
onSuccess(context) {
|
||||
const setCookie = context.response.headers.get("set-cookie");
|
||||
expect(setCookie).toBeDefined();
|
||||
|
||||
// Should have maxAge=0 for all chunks
|
||||
const parsed = parseSetCookieHeader(setCookie!);
|
||||
let hasCleanupChunks = false;
|
||||
|
||||
parsed.forEach((value, name) => {
|
||||
if (name.includes("session_data")) {
|
||||
expect(value["max-age"]).toBe(0);
|
||||
hasCleanupChunks = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasCleanupChunks).toBe(true);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT chunk cookies when they are under 4KB", async () => {
|
||||
const { client, testUser, cookieSetter } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onSuccess(context) {
|
||||
const setCookie = context.response.headers.get("set-cookie");
|
||||
expect(setCookie).toBeDefined();
|
||||
|
||||
const parsed = parseSetCookieHeader(setCookie!);
|
||||
let hasChunks = false;
|
||||
let hasSingleSessionData = false;
|
||||
|
||||
parsed.forEach((value, name) => {
|
||||
if (
|
||||
name.includes("session_data.0") ||
|
||||
name.includes("session_data.1")
|
||||
) {
|
||||
hasChunks = true;
|
||||
}
|
||||
if (name.endsWith("session_data")) {
|
||||
hasSingleSessionData = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(hasChunks).toBe(false);
|
||||
expect(hasSingleSessionData).toBe(true);
|
||||
|
||||
parsed.forEach((value, name) => {
|
||||
headers.append("cookie", `${name}=${value.value}`);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Verify we can read it back
|
||||
const request = new Request("https://example.com/api/auth/session", {
|
||||
headers,
|
||||
});
|
||||
|
||||
const cache = await getCookieCache(request, {
|
||||
secret: "better-auth.secret",
|
||||
});
|
||||
|
||||
expect(cache).not.toBeNull();
|
||||
expect(cache?.user?.email).toEqual(testUser.email);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { Session, User } from "../types";
|
||||
import { getDate } from "../utils/date";
|
||||
import { safeJSONParse } from "../utils/json";
|
||||
import { getBaseURL } from "../utils/url";
|
||||
import { createSessionStore } from "./session-store";
|
||||
|
||||
export function createCookieGetter(options: BetterAuthOptions) {
|
||||
const secure =
|
||||
@@ -170,13 +171,30 @@ export async function setCookieCache(
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we need to chunk the cookie (only if it exceeds 4093 bytes)
|
||||
if (data.length > 4093) {
|
||||
ctx.context?.logger?.error(
|
||||
`Session data exceeds cookie size limit (${data.length} bytes > 4093 bytes). Consider reducing session data size or disabling cookie cache. Session will not be cached in cookie.`,
|
||||
const sessionStore = createSessionStore(
|
||||
ctx.context.authCookies.sessionData.name,
|
||||
options,
|
||||
ctx,
|
||||
);
|
||||
return;
|
||||
|
||||
const cookies = sessionStore.chunk(data, options);
|
||||
sessionStore.setCookies(cookies);
|
||||
} else {
|
||||
const sessionStore = createSessionStore(
|
||||
ctx.context.authCookies.sessionData.name,
|
||||
options,
|
||||
ctx,
|
||||
);
|
||||
|
||||
if (sessionStore.hasChunks()) {
|
||||
const cleanCookies = sessionStore.clean();
|
||||
sessionStore.setCookies(cleanCookies);
|
||||
}
|
||||
|
||||
ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options);
|
||||
}
|
||||
ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +267,16 @@ export function deleteSessionCookie(
|
||||
...ctx.context.authCookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
});
|
||||
ctx.setCookie(ctx.context.authCookies.sessionData.name, "", {
|
||||
...ctx.context.authCookies.sessionData.options,
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
// Use createSessionStore to clean up all session data chunks
|
||||
const sessionStore = createSessionStore(
|
||||
ctx.context.authCookies.sessionData.name,
|
||||
ctx.context.authCookies.sessionData.options,
|
||||
ctx,
|
||||
);
|
||||
const cleanCookies = sessionStore.clean();
|
||||
sessionStore.setCookies(cleanCookies);
|
||||
|
||||
if (!skipDontRememberMe) {
|
||||
ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", {
|
||||
...ctx.context.authCookies.dontRememberToken.options,
|
||||
@@ -346,7 +370,30 @@ export const getCookieCache = async <
|
||||
? `__Secure-${cookiePrefix}.${cookieName}`
|
||||
: `${cookiePrefix}.${cookieName}`;
|
||||
const parsedCookie = parseCookies(cookies);
|
||||
const sessionData = parsedCookie.get(name);
|
||||
|
||||
// Check for chunked cookies
|
||||
let sessionData = parsedCookie.get(name);
|
||||
if (!sessionData) {
|
||||
// Try to reconstruct from chunks
|
||||
const chunks: Array<{ index: number; value: string }> = [];
|
||||
for (const [cookieName, value] of parsedCookie.entries()) {
|
||||
if (cookieName.startsWith(name + ".")) {
|
||||
const parts = cookieName.split(".");
|
||||
const indexStr = parts[parts.length - 1];
|
||||
const index = parseInt(indexStr || "0", 10);
|
||||
if (!isNaN(index)) {
|
||||
chunks.push({ index, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.length > 0) {
|
||||
// Sort by index and join
|
||||
chunks.sort((a, b) => a.index - b.index);
|
||||
sessionData = chunks.map((c) => c.value).join("");
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionData) {
|
||||
const secret = config?.secret || env.BETTER_AUTH_SECRET;
|
||||
if (!secret) {
|
||||
@@ -397,3 +444,4 @@ export const getCookieCache = async <
|
||||
};
|
||||
|
||||
export * from "./cookie-utils";
|
||||
export { createSessionStore, getChunkedCookie } from "./session-store";
|
||||
|
||||
288
packages/better-auth/src/cookies/session-store.ts
Normal file
288
packages/better-auth/src/cookies/session-store.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import type { GenericEndpointContext } from "@better-auth/core";
|
||||
import type { InternalLogger } from "@better-auth/core/env";
|
||||
import type { CookieOptions } from "better-call";
|
||||
import * as z from "zod";
|
||||
|
||||
// Cookie size constants based on browser limits
|
||||
const ALLOWED_COOKIE_SIZE = 4096;
|
||||
// Estimated size of an empty cookie with all attributes
|
||||
// (name, path, domain, secure, httpOnly, sameSite, expires/maxAge)
|
||||
const ESTIMATED_EMPTY_COOKIE_SIZE = 200;
|
||||
const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
||||
|
||||
interface Cookie {
|
||||
name: string;
|
||||
value: string;
|
||||
options: CookieOptions;
|
||||
}
|
||||
|
||||
type Chunks = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Parse cookies from the request headers
|
||||
*/
|
||||
function parseCookiesFromContext(
|
||||
ctx: GenericEndpointContext,
|
||||
): Record<string, string> {
|
||||
const cookieHeader = ctx.headers?.get("cookie");
|
||||
if (!cookieHeader) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cookies: Record<string, string> = {};
|
||||
const pairs = cookieHeader.split("; ");
|
||||
|
||||
for (const pair of pairs) {
|
||||
const [name, ...valueParts] = pair.split("=");
|
||||
if (name && valueParts.length > 0) {
|
||||
cookies[name] = valueParts.join("=");
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the chunk index from a cookie name
|
||||
*/
|
||||
function getChunkIndex(cookieName: string): number {
|
||||
const parts = cookieName.split(".");
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const index = parseInt(lastPart || "0", 10);
|
||||
return isNaN(index) ? 0 : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all existing chunks from cookies
|
||||
*/
|
||||
function readExistingChunks(
|
||||
cookieName: string,
|
||||
ctx: GenericEndpointContext,
|
||||
): Chunks {
|
||||
const chunks: Chunks = {};
|
||||
const cookies = parseCookiesFromContext(ctx);
|
||||
|
||||
for (const [name, value] of Object.entries(cookies)) {
|
||||
if (name.startsWith(cookieName)) {
|
||||
chunks[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full session data by joining all chunks
|
||||
*/
|
||||
function joinChunks(chunks: Chunks): string {
|
||||
const sortedKeys = Object.keys(chunks).sort((a, b) => {
|
||||
const aIndex = getChunkIndex(a);
|
||||
const bIndex = getChunkIndex(b);
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
return sortedKeys.map((key) => chunks[key]).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a cookie value into chunks if needed
|
||||
*/
|
||||
function chunkCookie(
|
||||
cookie: Cookie,
|
||||
chunks: Chunks,
|
||||
logger: InternalLogger,
|
||||
): Cookie[] {
|
||||
const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE);
|
||||
|
||||
if (chunkCount === 1) {
|
||||
chunks[cookie.name] = cookie.value;
|
||||
return [cookie];
|
||||
}
|
||||
|
||||
const cookies: Cookie[] = [];
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const name = `${cookie.name}.${i}`;
|
||||
const start = i * CHUNK_SIZE;
|
||||
const value = cookie.value.substring(start, start + CHUNK_SIZE);
|
||||
cookies.push({ ...cookie, name, value });
|
||||
chunks[name] = value;
|
||||
}
|
||||
|
||||
logger.debug("CHUNKING_SESSION_COOKIE", {
|
||||
message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
|
||||
emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
|
||||
valueSize: cookie.value.length,
|
||||
chunkCount,
|
||||
chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
|
||||
});
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cookies that should be cleaned (removed)
|
||||
*/
|
||||
function getCleanCookies(
|
||||
chunks: Chunks,
|
||||
cookieOptions: CookieOptions,
|
||||
): Record<string, Cookie> {
|
||||
const cleanedChunks: Record<string, Cookie> = {};
|
||||
for (const name in chunks) {
|
||||
cleanedChunks[name] = {
|
||||
name,
|
||||
value: "",
|
||||
options: { ...cookieOptions, maxAge: 0 },
|
||||
};
|
||||
}
|
||||
return cleanedChunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a session store for handling cookie chunking.
|
||||
* When session data exceeds 4KB, it automatically splits it into multiple cookies.
|
||||
*
|
||||
* Based on next-auth's SessionStore implementation.
|
||||
* @see https://github.com/nextauthjs/next-auth/blob/27b2519b84b8eb9cf053775dea29d577d2aa0098/packages/next-auth/src/core/lib/cookie.ts
|
||||
*/
|
||||
export function createSessionStore(
|
||||
cookieName: string,
|
||||
cookieOptions: CookieOptions,
|
||||
ctx: GenericEndpointContext,
|
||||
) {
|
||||
const chunks = readExistingChunks(cookieName, ctx);
|
||||
const logger = ctx.context.logger;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the full session data by joining all chunks
|
||||
*/
|
||||
getValue(): string {
|
||||
return joinChunks(chunks);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if there are existing chunks
|
||||
*/
|
||||
hasChunks(): boolean {
|
||||
return Object.keys(chunks).length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Chunk a cookie value and return all cookies to set (including cleanup cookies)
|
||||
*/
|
||||
chunk(value: string, options?: Partial<CookieOptions>): Cookie[] {
|
||||
// Start by cleaning all existing chunks
|
||||
const cleanedChunks = getCleanCookies(chunks, cookieOptions);
|
||||
// Clear the chunks object
|
||||
for (const name in chunks) {
|
||||
delete chunks[name];
|
||||
}
|
||||
const cookies: Record<string, Cookie> = cleanedChunks;
|
||||
|
||||
// Create new chunks
|
||||
const chunked = chunkCookie(
|
||||
{
|
||||
name: cookieName,
|
||||
value,
|
||||
options: { ...cookieOptions, ...options },
|
||||
},
|
||||
chunks,
|
||||
logger,
|
||||
);
|
||||
|
||||
// Update with new chunks
|
||||
for (const chunk of chunked) {
|
||||
cookies[chunk.name] = chunk;
|
||||
}
|
||||
|
||||
return Object.values(cookies);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cookies to clean up all chunks
|
||||
*/
|
||||
clean(): Cookie[] {
|
||||
const cleanedChunks = getCleanCookies(chunks, cookieOptions);
|
||||
// Clear the chunks object
|
||||
for (const name in chunks) {
|
||||
delete chunks[name];
|
||||
}
|
||||
return Object.values(cleanedChunks);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set all cookies in the context
|
||||
*/
|
||||
setCookies(cookies: Cookie[]): void {
|
||||
for (const cookie of cookies) {
|
||||
ctx.setCookie(cookie.name, cookie.value, cookie.options);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getChunkedCookie(
|
||||
ctx: GenericEndpointContext,
|
||||
cookieName: string,
|
||||
): string | null {
|
||||
const value = ctx.getCookie(cookieName);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const chunks: Array<{ index: number; value: string }> = [];
|
||||
|
||||
const cookieHeader = ctx.headers?.get("cookie");
|
||||
if (!cookieHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookies: Record<string, string> = {};
|
||||
const pairs = cookieHeader.split("; ");
|
||||
for (const pair of pairs) {
|
||||
const [name, ...valueParts] = pair.split("=");
|
||||
if (name && valueParts.length > 0) {
|
||||
cookies[name] = valueParts.join("=");
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, val] of Object.entries(cookies)) {
|
||||
if (name.startsWith(cookieName + ".")) {
|
||||
const parts = name.split(".");
|
||||
const indexStr = parts.at(-1);
|
||||
const index = parseInt(indexStr || "0", 10);
|
||||
if (!isNaN(index)) {
|
||||
chunks.push({ index, value: val });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.length > 0) {
|
||||
chunks.sort((a, b) => a.index - b.index);
|
||||
return chunks.map((c) => c.value).join("");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getSessionQuerySchema = z.optional(
|
||||
z.object({
|
||||
/**
|
||||
* If cookie cache is enabled, it will disable the cache
|
||||
* and fetch the session from the database
|
||||
*/
|
||||
disableCookieCache: z.coerce
|
||||
.boolean()
|
||||
.meta({
|
||||
description: "Disable cookie cache and fetch session from database",
|
||||
})
|
||||
.optional(),
|
||||
disableRefresh: z.coerce
|
||||
.boolean()
|
||||
.meta({
|
||||
description:
|
||||
"Disable session refresh. Useful for checking session status, without updating the session",
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user