UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

1 lines • 13.8 kB
{"version":3,"file":"session-store.mjs","names":["cookies: Record<string, string>","chunks: Chunks","cookies: Cookie[]","cleanedChunks: Record<string, Cookie>","cookies: Record<string, Cookie>","chunks: Array<{ index: number; value: string }>"],"sources":["../../src/cookies/session-store.ts"],"sourcesContent":["import type { GenericEndpointContext } from \"@better-auth/core\";\nimport type { Account } from \"@better-auth/core/db\";\nimport type { InternalLogger } from \"@better-auth/core/env\";\nimport { safeJSONParse } from \"@better-auth/core/utils\";\nimport type { CookieOptions } from \"better-call\";\nimport * as z from \"zod\";\nimport { symmetricDecodeJWT, symmetricEncodeJWT } from \"../crypto\";\n\n// Cookie size constants based on browser limits\nconst ALLOWED_COOKIE_SIZE = 4096;\n// Estimated size of an empty cookie with all attributes\n// (name, path, domain, secure, httpOnly, sameSite, expires/maxAge)\nconst ESTIMATED_EMPTY_COOKIE_SIZE = 200;\nconst CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;\n\ninterface Cookie {\n\tname: string;\n\tvalue: string;\n\toptions: CookieOptions;\n}\n\ntype Chunks = Record<string, string>;\n\n/**\n * Parse cookies from the request headers\n */\nfunction parseCookiesFromContext(\n\tctx: GenericEndpointContext,\n): Record<string, string> {\n\tconst cookieHeader = ctx.headers?.get(\"cookie\");\n\tif (!cookieHeader) {\n\t\treturn {};\n\t}\n\n\tconst cookies: Record<string, string> = {};\n\tconst pairs = cookieHeader.split(\"; \");\n\n\tfor (const pair of pairs) {\n\t\tconst [name, ...valueParts] = pair.split(\"=\");\n\t\tif (name && valueParts.length > 0) {\n\t\t\tcookies[name] = valueParts.join(\"=\");\n\t\t}\n\t}\n\n\treturn cookies;\n}\n\n/**\n * Extract the chunk index from a cookie name\n */\nfunction getChunkIndex(cookieName: string): number {\n\tconst parts = cookieName.split(\".\");\n\tconst lastPart = parts[parts.length - 1];\n\tconst index = parseInt(lastPart || \"0\", 10);\n\treturn isNaN(index) ? 0 : index;\n}\n\n/**\n * Read all existing chunks from cookies\n */\nfunction readExistingChunks(\n\tcookieName: string,\n\tctx: GenericEndpointContext,\n): Chunks {\n\tconst chunks: Chunks = {};\n\tconst cookies = parseCookiesFromContext(ctx);\n\n\tfor (const [name, value] of Object.entries(cookies)) {\n\t\tif (name.startsWith(cookieName)) {\n\t\t\tchunks[name] = value;\n\t\t}\n\t}\n\n\treturn chunks;\n}\n\n/**\n * Get the full session data by joining all chunks\n */\nfunction joinChunks(chunks: Chunks): string {\n\tconst sortedKeys = Object.keys(chunks).sort((a, b) => {\n\t\tconst aIndex = getChunkIndex(a);\n\t\tconst bIndex = getChunkIndex(b);\n\t\treturn aIndex - bIndex;\n\t});\n\n\treturn sortedKeys.map((key) => chunks[key]).join(\"\");\n}\n\n/**\n * Split a cookie value into chunks if needed\n */\nfunction chunkCookie(\n\tstoreName: string,\n\tcookie: Cookie,\n\tchunks: Chunks,\n\tlogger: InternalLogger,\n): Cookie[] {\n\tconst chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE);\n\n\tif (chunkCount === 1) {\n\t\tchunks[cookie.name] = cookie.value;\n\t\treturn [cookie];\n\t}\n\n\tconst cookies: Cookie[] = [];\n\tfor (let i = 0; i < chunkCount; i++) {\n\t\tconst name = `${cookie.name}.${i}`;\n\t\tconst start = i * CHUNK_SIZE;\n\t\tconst value = cookie.value.substring(start, start + CHUNK_SIZE);\n\t\tcookies.push({ ...cookie, name, value });\n\t\tchunks[name] = value;\n\t}\n\n\tlogger.debug(`CHUNKING_${storeName.toUpperCase()}_COOKIE`, {\n\t\tmessage: `${storeName} cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,\n\t\temptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,\n\t\tvalueSize: cookie.value.length,\n\t\tchunkCount,\n\t\tchunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),\n\t});\n\n\treturn cookies;\n}\n\n/**\n * Get all cookies that should be cleaned (removed)\n */\nfunction getCleanCookies(\n\tchunks: Chunks,\n\tcookieOptions: CookieOptions,\n): Record<string, Cookie> {\n\tconst cleanedChunks: Record<string, Cookie> = {};\n\tfor (const name in chunks) {\n\t\tcleanedChunks[name] = {\n\t\t\tname,\n\t\t\tvalue: \"\",\n\t\t\toptions: { ...cookieOptions, maxAge: 0 },\n\t\t};\n\t}\n\treturn cleanedChunks;\n}\n\n/**\n * Create a session store for handling cookie chunking.\n * When session data exceeds 4KB, it automatically splits it into multiple cookies.\n *\n * Based on next-auth's SessionStore implementation.\n * @see https://github.com/nextauthjs/next-auth/blob/27b2519b84b8eb9cf053775dea29d577d2aa0098/packages/next-auth/src/core/lib/cookie.ts\n */\nconst storeFactory =\n\t(storeName: string) =>\n\t(\n\t\tcookieName: string,\n\t\tcookieOptions: CookieOptions,\n\t\tctx: GenericEndpointContext,\n\t) => {\n\t\tconst chunks = readExistingChunks(cookieName, ctx);\n\t\tconst logger = ctx.context.logger;\n\n\t\treturn {\n\t\t\t/**\n\t\t\t * Get the full session data by joining all chunks\n\t\t\t */\n\t\t\tgetValue(): string {\n\t\t\t\treturn joinChunks(chunks);\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Check if there are existing chunks\n\t\t\t */\n\t\t\thasChunks(): boolean {\n\t\t\t\treturn Object.keys(chunks).length > 0;\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Chunk a cookie value and return all cookies to set (including cleanup cookies)\n\t\t\t */\n\t\t\tchunk(value: string, options?: Partial<CookieOptions>): Cookie[] {\n\t\t\t\t// Start by cleaning all existing chunks\n\t\t\t\tconst cleanedChunks = getCleanCookies(chunks, cookieOptions);\n\t\t\t\t// Clear the chunks object\n\t\t\t\tfor (const name in chunks) {\n\t\t\t\t\tdelete chunks[name];\n\t\t\t\t}\n\t\t\t\tconst cookies: Record<string, Cookie> = cleanedChunks;\n\n\t\t\t\t// Create new chunks\n\t\t\t\tconst chunked = chunkCookie(\n\t\t\t\t\tstoreName,\n\t\t\t\t\t{\n\t\t\t\t\t\tname: cookieName,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\toptions: { ...cookieOptions, ...options },\n\t\t\t\t\t},\n\t\t\t\t\tchunks,\n\t\t\t\t\tlogger,\n\t\t\t\t);\n\n\t\t\t\t// Update with new chunks\n\t\t\t\tfor (const chunk of chunked) {\n\t\t\t\t\tcookies[chunk.name] = chunk;\n\t\t\t\t}\n\n\t\t\t\treturn Object.values(cookies);\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Get cookies to clean up all chunks\n\t\t\t */\n\t\t\tclean(): Cookie[] {\n\t\t\t\tconst cleanedChunks = getCleanCookies(chunks, cookieOptions);\n\t\t\t\t// Clear the chunks object\n\t\t\t\tfor (const name in chunks) {\n\t\t\t\t\tdelete chunks[name];\n\t\t\t\t}\n\t\t\t\treturn Object.values(cleanedChunks);\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Set all cookies in the context\n\t\t\t */\n\t\t\tsetCookies(cookies: Cookie[]): void {\n\t\t\t\tfor (const cookie of cookies) {\n\t\t\t\t\tctx.setCookie(cookie.name, cookie.value, cookie.options);\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t};\n\nexport const createSessionStore = storeFactory(\"Session\");\nexport const createAccountStore = storeFactory(\"Account\");\n\nexport function getChunkedCookie(\n\tctx: GenericEndpointContext,\n\tcookieName: string,\n): string | null {\n\tconst value = ctx.getCookie(cookieName);\n\tif (value) {\n\t\treturn value;\n\t}\n\n\tconst chunks: Array<{ index: number; value: string }> = [];\n\n\tconst cookieHeader = ctx.headers?.get(\"cookie\");\n\tif (!cookieHeader) {\n\t\treturn null;\n\t}\n\n\tconst cookies: Record<string, string> = {};\n\tconst pairs = cookieHeader.split(\"; \");\n\tfor (const pair of pairs) {\n\t\tconst [name, ...valueParts] = pair.split(\"=\");\n\t\tif (name && valueParts.length > 0) {\n\t\t\tcookies[name] = valueParts.join(\"=\");\n\t\t}\n\t}\n\n\tfor (const [name, val] of Object.entries(cookies)) {\n\t\tif (name.startsWith(cookieName + \".\")) {\n\t\t\tconst parts = name.split(\".\");\n\t\t\tconst indexStr = parts.at(-1);\n\t\t\tconst index = parseInt(indexStr || \"0\", 10);\n\t\t\tif (!isNaN(index)) {\n\t\t\t\tchunks.push({ index, value: val });\n\t\t\t}\n\t\t}\n\t}\n\n\tif (chunks.length > 0) {\n\t\tchunks.sort((a, b) => a.index - b.index);\n\t\treturn chunks.map((c) => c.value).join(\"\");\n\t}\n\n\treturn null;\n}\n\nexport async function setAccountCookie(\n\tc: GenericEndpointContext,\n\taccountData: Record<string, any>,\n) {\n\tconst accountDataCookie = c.context.authCookies.accountData;\n\tconst options = {\n\t\tmaxAge: 60 * 5,\n\t\t...accountDataCookie.options,\n\t};\n\tconst data = await symmetricEncodeJWT(\n\t\taccountData,\n\t\tc.context.secret,\n\t\t\"better-auth-account\",\n\t\toptions.maxAge,\n\t);\n\n\tif (data.length > ALLOWED_COOKIE_SIZE) {\n\t\tconst accountStore = createAccountStore(accountDataCookie.name, options, c);\n\n\t\tconst cookies = accountStore.chunk(data, options);\n\t\taccountStore.setCookies(cookies);\n\t} else {\n\t\tconst accountStore = createAccountStore(accountDataCookie.name, options, c);\n\t\tif (accountStore.hasChunks()) {\n\t\t\tconst cleanCookies = accountStore.clean();\n\t\t\taccountStore.setCookies(cleanCookies);\n\t\t}\n\t\tc.setCookie(accountDataCookie.name, data, options);\n\t}\n}\n\nexport async function getAccountCookie(c: GenericEndpointContext) {\n\tconst accountCookie = getChunkedCookie(\n\t\tc,\n\t\tc.context.authCookies.accountData.name,\n\t);\n\tif (accountCookie) {\n\t\tconst accountData = safeJSONParse<Account>(\n\t\t\tawait symmetricDecodeJWT(\n\t\t\t\taccountCookie,\n\t\t\t\tc.context.secret,\n\t\t\t\t\"better-auth-account\",\n\t\t\t),\n\t\t);\n\t\tif (accountData) {\n\t\t\treturn accountData;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport const getSessionQuerySchema = z.optional(\n\tz.object({\n\t\t/**\n\t\t * If cookie cache is enabled, it will disable the cache\n\t\t * and fetch the session from the database\n\t\t */\n\t\tdisableCookieCache: z.coerce\n\t\t\t.boolean()\n\t\t\t.meta({\n\t\t\t\tdescription: \"Disable cookie cache and fetch session from database\",\n\t\t\t})\n\t\t\t.optional(),\n\t\tdisableRefresh: z.coerce\n\t\t\t.boolean()\n\t\t\t.meta({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Disable session refresh. Useful for checking session status, without updating the session\",\n\t\t\t})\n\t\t\t.optional(),\n\t}),\n);\n"],"mappings":"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"}