From 1c7f7e89484a721515732911e5ddb6f0a68aa4b5 Mon Sep 17 00:00:00 2001 From: Jon Meyers Date: Sat, 4 Nov 2023 00:15:00 +1100 Subject: [PATCH] Fix: add cookie chunking as default to ssr package (#669) * Add cookie chunking as default for ssr * fix client-side chunking * add changeset * remove testing chunk size --------- Co-authored-by: Andrew Smith --- .changeset/olive-plums-flash.md | 5 ++ packages/ssr/src/createBrowserClient.ts | 108 ++++++++++++++++-------- packages/ssr/src/createServerClient.ts | 61 ++++++++++--- packages/ssr/src/utils/chunker.ts | 39 ++++----- 4 files changed, 140 insertions(+), 73 deletions(-) create mode 100644 .changeset/olive-plums-flash.md diff --git a/.changeset/olive-plums-flash.md b/.changeset/olive-plums-flash.md new file mode 100644 index 00000000..07477ca2 --- /dev/null +++ b/.changeset/olive-plums-flash.md @@ -0,0 +1,5 @@ +--- +'@supabase/ssr': patch +--- + +Implement cookie chunking diff --git a/packages/ssr/src/createBrowserClient.ts b/packages/ssr/src/createBrowserClient.ts index 368a325e..49c79495 100644 --- a/packages/ssr/src/createBrowserClient.ts +++ b/packages/ssr/src/createBrowserClient.ts @@ -1,6 +1,12 @@ import { createClient } from '@supabase/supabase-js'; import { mergeDeepRight } from 'ramda'; -import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils'; +import { + DEFAULT_COOKIE_OPTIONS, + combineChunks, + createChunks, + deleteChunks, + isBrowser +} from './utils'; import { parse, serialize } from 'cookie'; import type { SupabaseClient } from '@supabase/supabase-js'; @@ -57,48 +63,76 @@ export function createBrowserClient< persistSession: true, storage: { getItem: async (key: string) => { - if (typeof cookies.get === 'function') { - return await cookies.get(key); - } - - if (isBrowser()) { - const cookie = parse(document.cookie); - return cookie[key]; - } + const chunkedCookie = await combineChunks(key, async (chunkName) => { + if (typeof cookies.get === 'function') { + return await cookies.get(chunkName); + } + if (isBrowser()) { + const cookie = parse(document.cookie); + return cookie[chunkName]; + } + }); + return chunkedCookie; }, setItem: async (key: string, value: string) => { - if (typeof cookies.set === 'function') { - return await cookies.set(key, value, { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: DEFAULT_COOKIE_OPTIONS.maxAge - }); - } - - if (isBrowser()) { - document.cookie = serialize(key, value, { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: DEFAULT_COOKIE_OPTIONS.maxAge - }); - } + const chunks = await createChunks(key, value); + await Promise.all( + chunks.map(async (chunk) => { + if (typeof cookies.set === 'function') { + await cookies.set(chunk.name, chunk.value, { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: DEFAULT_COOKIE_OPTIONS.maxAge + }); + } else { + if (isBrowser()) { + document.cookie = serialize(chunk.name, chunk.value, { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: DEFAULT_COOKIE_OPTIONS.maxAge + }); + } + } + }) + ); }, removeItem: async (key: string) => { - if (typeof cookies.remove === 'function') { - return await cookies.remove(key, { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: 0 - }); + if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') { + console.log( + 'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createBrowserClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client' + ); + return; } - if (isBrowser()) { - document.cookie = serialize(key, '', { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: 0 - }); - } + await deleteChunks( + key, + async (chunkName) => { + if (typeof cookies.get === 'function') { + return await cookies.get(chunkName); + } + if (isBrowser()) { + const documentCookies = parse(document.cookie); + return documentCookies[chunkName]; + } + }, + async (chunkName) => { + if (typeof cookies.remove === 'function') { + await cookies.remove(chunkName, { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: 0 + }); + } else { + if (isBrowser()) { + document.cookie = serialize(chunkName, '', { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: 0 + }); + } + } + } + ); } } } diff --git a/packages/ssr/src/createServerClient.ts b/packages/ssr/src/createServerClient.ts index 76a124d2..413bf7b4 100644 --- a/packages/ssr/src/createServerClient.ts +++ b/packages/ssr/src/createServerClient.ts @@ -1,6 +1,12 @@ import { createClient } from '@supabase/supabase-js'; import { mergeDeepRight } from 'ramda'; -import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils'; +import { + DEFAULT_COOKIE_OPTIONS, + combineChunks, + createChunks, + deleteChunks, + isBrowser +} from './utils'; import type { GenericSchema, @@ -45,23 +51,52 @@ export function createServerClient< persistSession: true, storage: { getItem: async (key: string) => { - if (typeof cookies.get === 'function') { - return await cookies.get(key); - } + const chunkedCookie = await combineChunks(key, async (chunkName: string) => { + if (typeof cookies.get === 'function') { + return await cookies.get(chunkName); + } + }); + return chunkedCookie; }, setItem: async (key: string, value: string) => { - if (typeof cookies.set === 'function') { - await cookies.set(key, value, { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: DEFAULT_COOKIE_OPTIONS.maxAge - }); - } + const chunks = createChunks(key, value); + await Promise.all( + chunks.map(async (chunk) => { + if (typeof cookies.set === 'function') { + await cookies.set(chunk.name, chunk.value, { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: DEFAULT_COOKIE_OPTIONS.maxAge + }); + } + }) + ); }, removeItem: async (key: string) => { - if (typeof cookies.remove === 'function') { - await cookies.remove(key, { ...DEFAULT_COOKIE_OPTIONS, ...cookieOptions, maxAge: 0 }); + if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') { + console.log( + 'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client' + ); + return; } + + deleteChunks( + key, + async (chunkName) => { + if (typeof cookies.get === 'function') { + return await cookies.get(chunkName); + } + }, + async (chunkName) => { + if (typeof cookies.remove === 'function') { + return await cookies.remove(chunkName, { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: 0 + }); + } + } + ); } } } diff --git a/packages/ssr/src/utils/chunker.ts b/packages/ssr/src/utils/chunker.ts index c72f2eb4..ce48fbc9 100644 --- a/packages/ssr/src/utils/chunker.ts +++ b/packages/ssr/src/utils/chunker.ts @@ -15,7 +15,6 @@ const MAX_CHUNK_REGEXP = createChunkRegExp(MAX_CHUNK_SIZE); */ export function createChunks(key: string, value: string, chunkSize?: number): Chunk[] { const re = chunkSize !== undefined ? createChunkRegExp(chunkSize) : MAX_CHUNK_REGEXP; - // check the length of the string to work out if it should be returned or chunked const chunkCount = Math.ceil(value.length / (chunkSize ?? MAX_CHUNK_SIZE)); @@ -27,7 +26,7 @@ export function createChunks(key: string, value: string, chunkSize?: number): Ch // split string into a array based on the regex const values = value.match(re); values?.forEach((value, i) => { - const name: string = `${key}.${i}`; + const name = `${key}.${i}`; chunks.push({ name, value }); }); @@ -35,27 +34,21 @@ export function createChunks(key: string, value: string, chunkSize?: number): Ch } // Get fully constructed chunks -export function combineChunks( +export async function combineChunks( key: string, - retrieveChunk: (name: string) => string | null | undefined = () => { - return null; - } + retrieveChunk: (name: string) => Promise | string | null | undefined ) { - const value = retrieveChunk(key); - - // pkce code verifier - if (key.endsWith('-code-verifier') && value) { - return value; - } + const value = await retrieveChunk(key); if (value) { return value; } let values: string[] = []; + for (let i = 0; ; i++) { const chunkName = `${key}.${i}`; - const chunk = retrieveChunk(chunkName); + const chunk = await retrieveChunk(chunkName); if (!chunk) { break; @@ -64,31 +57,31 @@ export function combineChunks( values.push(chunk); } - return values.length ? values.join('') : null; + if (values.length > 0) { + return values.join(''); + } } -export function deleteChunks( +export async function deleteChunks( key: string, - retrieveChunk: (name: string) => string | null | undefined = () => { - return null; - }, - removeChunk: (name: string) => void = () => {} + retrieveChunk: (name: string) => Promise | string | null | undefined, + removeChunk: (name: string) => Promise | void ) { - const value = retrieveChunk(key); + const value = await retrieveChunk(key); if (value) { - removeChunk(key); + await removeChunk(key); return; } for (let i = 0; ; i++) { const chunkName = `${key}.${i}`; - const chunk = retrieveChunk(chunkName); + const chunk = await retrieveChunk(chunkName); if (!chunk) { break; } - removeChunk(chunkName); + await removeChunk(chunkName); } }