diff --git a/.changeset/red-bikes-beg.md b/.changeset/red-bikes-beg.md new file mode 100644 index 000000000000..38694d88b46c --- /dev/null +++ b/.changeset/red-bikes-beg.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `cookie.setSerialized()` to set a cookie from a string diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 732df205d0ae..13635335c72d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -239,6 +239,17 @@ export interface Cookies { opts: import('cookie').CookieSerializeOptions & { path: string } ) => void; + /** + * Sets a cookie from a string representing the value of the `set-cookie` header. This will add the `set-cookie` header to the response, and also make the cookie available via `cookies.get` or `cookies.getAll` during the current request. + * + * No default values. It will set only properties you specified in a cookie. + * + * If you do not specify name, value and path, it will throw an error. + * @param cookie the serialized cookie + * @since 2.21.0 + */ + setSerialized: (cookie: string) => void; + /** * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past. * diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index 2e683543a534..198b9612ca3b 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -1,6 +1,7 @@ import { parse, serialize } from 'cookie'; import { normalize_path, resolve } from '../../utils/url.js'; import { add_data_suffix } from '../pathname.js'; +import * as set_cookie_parser from 'set-cookie-parser'; // eslint-disable-next-line no-control-regex -- control characters are invalid in cookie names const INVALID_COOKIE_CHARACTER_REGEX = /[\x00-\x1F\x7F()<>@,;:"/[\]?={} \t]/; @@ -129,6 +130,44 @@ export function get_cookies(request, url) { set_internal(name, value, { ...defaults, ...options }); }, + /** + * @param {string} cookie + */ + setSerialized(cookie) { + if (cookie === '') { + throw new Error('Cannot pass empty string'); + } + + const parsed = set_cookie_parser.parseString(cookie); + const { name, value, path, sameSite, secure, httpOnly, ...opts } = parsed; + + if (name === undefined || value === undefined || path === undefined) { + throw new Error('The name, value and path must be provided to create a cookie.'); + } + + /** + * @type {true|false|'lax'|'strict'|'none'|undefined} + */ + const normalized_same_site = (() => { + if (sameSite === undefined || typeof sameSite === 'boolean') { + return sameSite; + } + const lower = sameSite.toLowerCase(); + if (lower === 'lax' || lower === 'strict' || lower === 'none') { + return /** @type {'lax'|'strict'|'none'} */ (lower); + } + return undefined; + })(); + + this.set(name, value, { + ...opts, + path, + sameSite: normalized_same_site, + secure: secure ?? false, + httpOnly: httpOnly ?? false + }); + }, + /** * @param {string} name * @param {import('./page/types.js').Cookie['options']} options diff --git a/packages/kit/src/runtime/server/cookie.spec.js b/packages/kit/src/runtime/server/cookie.spec.js index e8f2cb08e623..e8e376bc4efa 100644 --- a/packages/kit/src/runtime/server/cookie.spec.js +++ b/packages/kit/src/runtime/server/cookie.spec.js @@ -213,3 +213,43 @@ test("set_internal isn't affected by defaults", () => { expect(cookies.get('test')).toEqual('foo'); expect(new_cookies['test']?.options).toEqual(options); }); + +test('no default values when setSerialized is called', () => { + const { cookies, new_cookies } = cookies_setup(); + const cookie_string = 'a=b; Path=/;'; + cookies.setSerialized(cookie_string); + const opts = new_cookies['a']?.options; + assert.equal(opts?.path, '/'); + assert.equal(opts?.secure, false); + assert.equal(opts?.httpOnly, false); + assert.equal(opts?.sameSite, undefined); +}); + +test('set all parameters when setSerialized is called', () => { + const { cookies, new_cookies } = cookies_setup(); + const cookie_string = + 'a=b; Path=/; Max-Age=3600; Expires=Thu, 03 Apr 2025 00:41:07 GMT; Secure; HttpOnly; SameSite=Strict; domain=example.com'; + cookies.setSerialized(cookie_string); + const opts = new_cookies['a']?.options; + assert.equal(opts?.path, '/'); + assert.equal(opts?.secure, true); + assert.equal(opts?.httpOnly, true); + assert.equal(opts?.sameSite, 'strict'); + assert.equal(opts?.domain, 'example.com'); + assert.equal(opts?.maxAge, 3600); + assert.isNotNull(opts.expires); +}); + +test('throw error when setSerialized is called with empty string', () => { + const { cookies } = cookies_setup(); + assert.throws(() => cookies.setSerialized(''), 'Cannot pass empty string'); +}); + +test('throw error when setSerialized is called without name, value and path', () => { + const { cookies } = cookies_setup(); + const cookie_string = 'Max-Age=3600; Expires=Thu, 03 Apr 2025 00:41:07 GMT; Secure; HttpOnly;'; + assert.throws( + () => cookies.setSerialized(cookie_string), + 'The name, value and path must be provided to create a cookie.' + ); +}); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3ac640b17196..9de19d6c064e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -221,6 +221,17 @@ declare module '@sveltejs/kit' { opts: import('cookie').CookieSerializeOptions & { path: string } ) => void; + /** + * Sets a cookie from a string representing the value of the `set-cookie` header. This will add the `set-cookie` header to the response, and also make the cookie available via `cookies.get` or `cookies.getAll` during the current request. + * + * No default values. It will set only properties you specified in a cookie. + * + * If you do not specify name, value and path, it will throw an error. + * @param cookie the serialized cookie + * @since 2.21.0 + */ + setSerialized: (cookie: string) => void; + /** * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past. *