Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: add cookie chunking as default to ssr package #669

Merged
merged 4 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-plums-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@supabase/ssr': patch
---

Implement cookie chunking
108 changes: 71 additions & 37 deletions packages/ssr/src/createBrowserClient.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
});
}
}
}
);
}
}
}
Expand Down
61 changes: 48 additions & 13 deletions packages/ssr/src/createServerClient.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
});
}
}
);
}
}
}
Expand Down
41 changes: 17 additions & 24 deletions packages/ssr/src/utils/chunker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ function createChunkRegExp(chunkSize: number) {
return new RegExp('.{1,' + chunkSize + '}', 'g');
}

const MAX_CHUNK_SIZE = 3600;
const MAX_CHUNK_SIZE = 1600;
dijonmusters marked this conversation as resolved.
Show resolved Hide resolved
const MAX_CHUNK_REGEXP = createChunkRegExp(MAX_CHUNK_SIZE);

/**
* create chunks from a string and return an array of object
*/
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));

Expand All @@ -27,35 +26,29 @@ 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 });
});

return chunks;
}

// 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> | 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;
Expand All @@ -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> | string | null | undefined,
removeChunk: (name: string) => Promise<void> | 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);
}
}
Loading