diff --git a/packages/ssr/src/createBrowserClient.ts b/packages/ssr/src/createBrowserClient.ts index be6243c7..a2ecf4b8 100644 --- a/packages/ssr/src/createBrowserClient.ts +++ b/packages/ssr/src/createBrowserClient.ts @@ -62,7 +62,7 @@ export function createBrowserClient< await deleteChunks( key, async (chunkName) => { - if (typeof cookies.get === 'function') { + if ('get' in cookies && typeof cookies.get === 'function') { return await cookies.get(chunkName); } if (isBrowser()) { @@ -71,7 +71,7 @@ export function createBrowserClient< } }, async (chunkName) => { - if (typeof cookies.remove === 'function') { + if ('remove' in cookies.remove && typeof cookies.remove === 'function') { await cookies.remove(chunkName, { ...DEFAULT_COOKIE_OPTIONS, ...cookieOptions, diff --git a/packages/ssr/src/createServerClient.ts b/packages/ssr/src/createServerClient.ts index 9dfba7e8..2365638d 100644 --- a/packages/ssr/src/createServerClient.ts +++ b/packages/ssr/src/createServerClient.ts @@ -46,25 +46,109 @@ export function createServerClient< }; } - const deleteAllChunks = async (key: string) => { - await deleteChunks( - key, - async (chunkName) => { - if (typeof cookies.get === 'function') { - return await cookies.get(chunkName); + let storage: any; + + if ('get' in cookies) { + const deleteAllChunks = async (key: string) => { + await 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 + }); + } } + ); + }; + + storage = { + // to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted + isServer: true, + getItem: async (key: string) => { + const chunkedCookie = await combineChunks(key, async (chunkName: string) => { + if (typeof cookies.get === 'function') { + return await cookies.get(chunkName); + } + }); + return chunkedCookie; }, - async (chunkName) => { - if (typeof cookies.remove === 'function') { - return await cookies.remove(chunkName, { - ...DEFAULT_COOKIE_OPTIONS, - ...cookieOptions, - maxAge: 0 - }); + setItem: async (key: string, value: string) => { + if (typeof cookies.set === 'function') { + // first delete all chunks so that there would be no overlap + await deleteAllChunks(key); + + const chunks = createChunks(key, value); + + for (let i = 0; i < chunks.length; i += 1) { + const chunk = chunks[i]; + + 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' && 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; + } + + await deleteAllChunks(key); + } + }; + } + + const setItems: { [key: string]: string } = {}; + const removedItems: { [key: string]: boolean } = {}; + if ('getAll' in cookies) { + storage = { + // to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted + isServer: true, + getItem: async (key: string) => { + if (typeof setItems[key] === 'string') { + return setItems[key]; + } + + if (removedItems[key]) { + return null; } + + const allCookies = await cookies.getAll(); + const chunkedCookie = await combineChunks(key, async (chunkName: string) => { + const cookie = allCookies?.find(({ name }) => name === chunkName) || null; + + if (!cookie) { + return null; + } + + return cookie.value; + }); + + return chunkedCookie; + }, + setItem: async (key: string, value: string) => { + setItems[key] = value; + delete removedItems[key]; + }, + removeItem: async (key: string) => { + delete setItems[key]; + removedItems[key] = true; } - ); - }; + }; + } const cookieClientOptions = { global: { @@ -77,46 +161,7 @@ export function createServerClient< autoRefreshToken: isBrowser(), detectSessionInUrl: isBrowser(), persistSession: true, - storage: { - // to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted - isServer: true, - getItem: async (key: string) => { - 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') { - // first delete all chunks so that there would be no overlap - await deleteAllChunks(key); - - const chunks = createChunks(key, value); - - for (let i = 0; i < chunks.length; i += 1) { - const chunk = chunks[i]; - - 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' && 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; - } - - await deleteAllChunks(key); - } - } + storage } }; @@ -126,5 +171,82 @@ export function createServerClient< userDefinedClientOptions ) as SupabaseClientOptions; - return createClient(supabaseUrl, supabaseKey, clientOptions); + const client = createClient( + supabaseUrl, + supabaseKey, + clientOptions + ); + + if ('getAll' in cookies) { + client.auth.onAuthStateChange(async (event) => { + if (event === 'TOKEN_REFRESHED' || event === 'USER_UPDATED' || event === 'SIGNED_OUT') { + if (typeof cookies.setAll !== 'function') { + console.log('You are holding it wrong!!!!!'); + } + + const allCookies = await cookies.getAll(); + const cookieNames = allCookies?.map(({ name }) => name) || []; + + const removeCookies = cookieNames.filter((name) => { + if (removedItems[name]) { + return true; + } + + const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/); + if (chunkLike && removedItems[chunkLike[1]]) { + return true; + } + + return false; + }); + + const setCookies = Object.keys(setItems).flatMap((itemName) => { + const removeExistingCookiesForItem = new Set( + cookieNames.filter((name) => { + if (name === itemName) { + return true; + } + + const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/); + if (chunkLike && chunkLike[1] === itemName) { + return true; + } + + return false; + }) + ); + + const chunks = createChunks(itemName, setItems[itemName]); + + chunks.forEach((chunk) => { + removeExistingCookiesForItem.delete(chunk.name); + }); + + removeCookies.push(...removeExistingCookiesForItem); + + return chunks; + }); + + const removeCookieOptions = { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: 0 + }; + const setCookieOptions = { + ...DEFAULT_COOKIE_OPTIONS, + ...cookieOptions, + maxAge: DEFAULT_COOKIE_OPTIONS.maxAge + }; + + await cookies.setAll( + [].concat( + removeCookies.map((name) => ({ name, value: '', options: removeCookieOptions })), + setCookies.map(({ name, value }) => ({ name, value, options: setCookieOptions })) + ) + ); + } + }); + } + + return client; } diff --git a/packages/ssr/src/types.ts b/packages/ssr/src/types.ts index 9959e98a..bbfb71a5 100644 --- a/packages/ssr/src/types.ts +++ b/packages/ssr/src/types.ts @@ -2,8 +2,21 @@ import type { CookieSerializeOptions } from 'cookie'; export type CookieOptions = Partial; export type CookieOptionsWithName = { name?: string } & CookieOptions; -export type CookieMethods = { - get?: (key: string) => Promise | string | null | undefined; - set?: (key: string, value: string, options: CookieOptions) => Promise | void; - remove?: (key: string, options: CookieOptions) => Promise | void; -}; +export type CookieMethods = + | { + /** @deprecated Move to using `getAll` instead. */ + get: (key: string) => Promise | string | null | undefined; + /** @deprecated Move to using `setAll` instead. */ + set?: (key: string, value: string, options: CookieOptions) => Promise | void; + /** @deprecated Move to using `setAll` instead. */ + remove?: (key: string, options: CookieOptions) => Promise | void; + } + | { + getAll: () => + | Promise<{ name: string; value: string }[] | null> + | { name: string; value: string }[] + | null; + setAll?: ( + cookies: { key: string; value: string; options: CookieOptions }[] + ) => Promise | void; + };