From 9e14a78cb05667af9821948c630786f74680090d Mon Sep 17 00:00:00 2001 From: Farzad Date: Wed, 10 Apr 2024 10:18:54 -0400 Subject: [PATCH] Accept common cookie attributes when deleting a cookie (#10671) * Accept common cookie attributes when deleting a cookie * Fix AstroCookieSetOptions IDE annotations * Use AstroCookieSetOptions to construct AstroCookieDeleteOptions * Update .changeset/shaggy-cats-film.md Co-authored-by: Florian Lefebvre --------- Co-authored-by: Florian Lefebvre --- .changeset/shaggy-cats-film.md | 5 +++ packages/astro/src/core/cookies/cookies.ts | 35 ++++++++--------- .../astro/test/units/cookies/delete.test.js | 39 +++++++++++++++++-- 3 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 .changeset/shaggy-cats-film.md diff --git a/.changeset/shaggy-cats-film.md b/.changeset/shaggy-cats-film.md new file mode 100644 index 0000000000..4aaecea6c0 --- /dev/null +++ b/.changeset/shaggy-cats-film.md @@ -0,0 +1,5 @@ +--- +"astro": minor +--- + +Adds the `httpOnly`, `sameSite`, and `secure` options when deleting a cookie diff --git a/packages/astro/src/core/cookies/cookies.ts b/packages/astro/src/core/cookies/cookies.ts index 862d86ff03..069afc796e 100644 --- a/packages/astro/src/core/cookies/cookies.ts +++ b/packages/astro/src/core/cookies/cookies.ts @@ -2,22 +2,16 @@ import type { CookieSerializeOptions } from 'cookie'; import { parse, serialize } from 'cookie'; import { AstroError, AstroErrorData } from '../errors/index.js'; -export interface AstroCookieSetOptions { - domain?: string; - expires?: Date; - httpOnly?: boolean; - maxAge?: number; - path?: string; - sameSite?: boolean | 'lax' | 'none' | 'strict'; - secure?: boolean; - encode?: (value: string) => string; -} +export type AstroCookieSetOptions = Pick< + CookieSerializeOptions, + 'domain' | 'path' | 'expires' | 'maxAge' | 'httpOnly' | 'sameSite' | 'secure' | 'encode' +>; export interface AstroCookieGetOptions { decode?: (value: string) => string; } -type AstroCookieDeleteOptions = Pick; +type AstroCookieDeleteOptions = Omit; interface AstroCookieInterface { value: string; @@ -78,17 +72,22 @@ class AstroCookies implements AstroCookiesInterface { * @param options Options related to this deletion, such as the path of the cookie. */ delete(key: string, options?: AstroCookieDeleteOptions): void { + /** + * The `@ts-expect-error` is necessary because `maxAge` and `expires` properties + * must not appear in the AstroCookieDeleteOptions type. + */ + const { + // @ts-expect-error + maxAge: _ignoredMaxAge, + // @ts-expect-error + expires: _ignoredExpires, + ...sanitizedOptions + } = options || {}; const serializeOptions: CookieSerializeOptions = { expires: DELETED_EXPIRATION, + ...sanitizedOptions, }; - if (options?.domain) { - serializeOptions.domain = options.domain; - } - if (options?.path) { - serializeOptions.path = options.path; - } - // Set-Cookie: token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT this.#ensureOutgoingMap().set(key, [ DELETED_VALUE, diff --git a/packages/astro/test/units/cookies/delete.test.js b/packages/astro/test/units/cookies/delete.test.js index 4b5f188e1a..39a5d7daed 100644 --- a/packages/astro/test/units/cookies/delete.test.js +++ b/packages/astro/test/units/cookies/delete.test.js @@ -47,26 +47,57 @@ describe('astro/src/core/cookies', () => { assert.equal(cookies.has('foo'), false); }); - it('can provide a path', () => { + it('deletes a cookie with attributes', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); + cookies.delete('foo', { + domain: 'example.com', path: '/subpath/', + priority: 'high', + secure: true, + httpOnly: true, + sameSite: 'strict', }); + let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); + assert.equal(/foo=deleted/.test(headers[0]), true); + assert.equal(/Expires=Thu, 01 Jan 1970 00:00:00 GMT/.test(headers[0]), true); + assert.equal(/Domain=example.com/.test(headers[0]), true); assert.equal(/Path=\/subpath\//.test(headers[0]), true); + assert.equal(/Priority=High/.test(headers[0]), true); + assert.equal(/Secure/.test(headers[0]), true); + assert.equal(/HttpOnly/.test(headers[0]), true); + assert.equal(/SameSite=Strict/.test(headers[0]), true); }); - it('can provide a domain', () => { + it('ignores expires option', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); + cookies.delete('foo', { - domain: '.example.com', + expires: new Date(), }); + let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); - assert.equal(/Domain=\.example\.com/.test(headers[0]), true); + assert.equal(/foo=deleted/.test(headers[0]), true); + assert.equal(/Expires=Thu, 01 Jan 1970 00:00:00 GMT/.test(headers[0]), true); + }); + + it('ignores maxAge option', () => { + let req = new Request('http://example.com/'); + let cookies = new AstroCookies(req); + + cookies.delete('foo', { + maxAge: 60, + }); + + let headers = Array.from(cookies.headers()); + assert.equal(headers.length, 1); + assert.equal(/foo=deleted/.test(headers[0]), true); + assert.equal(/Expires=Thu, 01 Jan 1970 00:00:00 GMT/.test(headers[0]), true); }); }); });