diff --git a/contributors.yml b/contributors.yml index 57082791773..1bd9de0a4b2 100644 --- a/contributors.yml +++ b/contributors.yml @@ -192,6 +192,7 @@ - eric-burel - ericchernuka - esamattis +- Evanion - evanwinter - everdimension - exegeteio diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index 9cce2903978..e1a4e3a0c4e 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -193,6 +193,38 @@ describe("cookies", () => { ); }); }); + + describe("encode/decode options", () => { + it("Will not Base64 encode/decode the cookie if `encode` and `decode` are set to custom functions", async () => { + let testValue = { hello: "world" }; + let cookie = createCookie("my-cookie", { + encode: (value) => JSON.stringify(value), + decode: (value) => JSON.parse(value), + }); + let setCookie = await cookie.serialize(testValue); + + expect(setCookie).toBe("my-cookie=" + JSON.stringify(testValue)); + }); + + it("Will not allow `encode` and `decode` to be set at the same time as `secrets`", async () => { + let secrets = ["foo"]; + let encode = (value) => JSON.stringify(value); + + expect(() => { + createCookie("my-cookie", { secrets, encode }); + }).toThrowErrorMatchingInlineSnapshot( + "sign is not supported with encode option" + ); + + let decode = (value) => JSON.parse(value); + + expect(() => { + createCookie("my-cookie", { secrets, decode }); + }).toThrowErrorMatchingInlineSnapshot( + "unsign is not supported with decode option" + ); + }); + }); }); function spyConsole() { diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index 80884fa8fc2..a48a6b03e45 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -109,17 +109,30 @@ export const createCookieFactory = }, async parse(cookieHeader, parseOptions) { if (!cookieHeader) return null; + if (secrets.length > 0 && !!parseOptions?.decode) + throw new Error("unsign is not supported with decode option"); + let shouldEncode = !parseOptions?.decode; let cookies = parse(cookieHeader, { ...options, ...parseOptions }); return name in cookies ? cookies[name] === "" ? "" - : await decodeCookieValue(unsign, cookies[name], secrets) + : await decodeCookieValue( + unsign, + cookies[name], + secrets, + shouldEncode + ) : null; }, async serialize(value, serializeOptions) { + if (secrets.length > 0 && !!serializeOptions?.encode) + throw new Error("sign is not supported with encode option"); + let shouldEncode = !serializeOptions?.encode; return serialize( name, - value === "" ? "" : await encodeCookieValue(sign, value, secrets), + value === "" + ? "" + : await encodeCookieValue(sign, value, secrets, shouldEncode), { ...options, ...serializeOptions, @@ -149,9 +162,10 @@ export const isCookie: IsCookieFunction = (object): object is Cookie => { async function encodeCookieValue( sign: SignFunction, value: any, - secrets: string[] + secrets: string[], + shouldEncode: boolean ): Promise { - let encoded = encodeData(value); + let encoded = shouldEncode ? encodeData(value) : value; if (secrets.length > 0) { encoded = await sign(encoded, secrets[0]); @@ -163,7 +177,8 @@ async function encodeCookieValue( async function decodeCookieValue( unsign: UnsignFunction, value: string, - secrets: string[] + secrets: string[], + shouldDecode: boolean ): Promise { if (secrets.length > 0) { for (let secret of secrets) { @@ -176,7 +191,7 @@ async function decodeCookieValue( return null; } - return decodeData(value); + return shouldDecode ? decodeData(value) : value; } function encodeData(value: any): string {