From fbba797aaec8696a2eba1bb2ee327dc1430dbe62 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Thu, 14 Nov 2024 21:08:18 -0500 Subject: [PATCH 1/2] add specific error messages to serialzie validation --- src/index.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index af222fc..95c121e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -150,6 +150,14 @@ function endIndex(str: string, index: number, min: number) { return min; } +const VALID_PRIORITIES = ["low", "medium", "high"] as const; +const VALID_PRIORITIES_VALUES_STRING = VALID_PRIORITIES.join(", "); +type PriorityValues = (typeof VALID_PRIORITIES)[number]; + +const VALID_SAMESITE = ["lax", "strict", "none"] as const; +const VALID_SAMESITE_VALUE_STRING = VALID_SAMESITE.join(", "); +type SameSiteValues = (typeof VALID_SAMESITE)[number]; + /** * Serialize options. */ @@ -217,7 +225,7 @@ export interface SerializeOptions { * * More information about priority levels can be found in [the specification](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1). */ - priority?: "low" | "medium" | "high"; + priority?: PriorityValues; /** * Specifies the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). * @@ -228,7 +236,7 @@ export interface SerializeOptions { * * More information about enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). */ - sameSite?: boolean | "lax" | "strict" | "none"; + sameSite?: boolean | SameSiteValues; } /** @@ -323,7 +331,9 @@ export function serialize( str += "; Priority=High"; break; default: - throw new TypeError(`option priority is invalid: ${options.priority}`); + throw new TypeError( + `option priority is invalid: ${options.priority}. Must be one of ${VALID_PRIORITIES_VALUES_STRING}`, + ); } } @@ -344,7 +354,9 @@ export function serialize( str += "; SameSite=None"; break; default: - throw new TypeError(`option sameSite is invalid: ${options.sameSite}`); + throw new TypeError( + `option sameSite is invalid: ${options.sameSite}. Must be boolean or one of ${VALID_SAMESITE_VALUE_STRING}`, + ); } } From acc0d399c08baafa3377e146aaa510bb1c17b1b2 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Thu, 14 Nov 2024 21:58:58 -0500 Subject: [PATCH 2/2] feat: throw CookieError w/ code and valid options when available --- src/index.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 95c121e..ba80550 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,6 +84,31 @@ export interface ParseOptions { decode?: (str: string) => string | undefined; } +export const CookieErrorCode = { + INVALID_NAME: "INVALID_NAME", + INVALID_VALUE: "INVALID_VALUE", + INVALID_MAXAGE: "INVALID_MAXAGE", + INVALID_DOMAIN: "INVALID_DOMAIN", + INVALID_PATH: "INVALID_PATH", + INVALID_EXPIRES: "INVALID_EXPIRES", + INVALID_PRIORITY: "INVALID_PRIORITY", + INVALID_SAMESITE: "INVALID_SAMESITE", +} as const; + +export type CookieErrorCode = + (typeof CookieErrorCode)[keyof typeof CookieErrorCode]; + +class CookieError extends Error { + code: CookieErrorCode; + + constructor(message: string, code: CookieErrorCode) { + super(message); + this.code = code; + this.name = "CookieError"; + Object.setPrototypeOf(this, CookieError.prototype); // Ensures correct prototype chain for TypeScript + } +} + /** * Parse a cookie header. * @@ -256,13 +281,19 @@ export function serialize( const enc = options?.encode || encodeURIComponent; if (!cookieNameRegExp.test(name)) { - throw new TypeError(`argument name is invalid: ${name}`); + throw new CookieError( + `argument name is invalid: ${name}`, + CookieErrorCode.INVALID_NAME, + ); } const value = enc(val); if (!cookieValueRegExp.test(value)) { - throw new TypeError(`argument val is invalid: ${val}`); + throw new CookieError( + `argument val is invalid: ${val}`, + CookieErrorCode.INVALID_VALUE, + ); } let str = name + "=" + value; @@ -270,7 +301,10 @@ export function serialize( if (options.maxAge !== undefined) { if (!Number.isInteger(options.maxAge)) { - throw new TypeError(`option maxAge is invalid: ${options.maxAge}`); + throw new CookieError( + `option maxAge is invalid: ${options.maxAge}. Must be an integer`, + CookieErrorCode.INVALID_MAXAGE, + ); } str += "; Max-Age=" + options.maxAge; @@ -278,7 +312,10 @@ export function serialize( if (options.domain) { if (!domainValueRegExp.test(options.domain)) { - throw new TypeError(`option domain is invalid: ${options.domain}`); + throw new CookieError( + `option domain is invalid: ${options.domain}`, + CookieErrorCode.INVALID_DOMAIN, + ); } str += "; Domain=" + options.domain; @@ -286,7 +323,10 @@ export function serialize( if (options.path) { if (!pathValueRegExp.test(options.path)) { - throw new TypeError(`option path is invalid: ${options.path}`); + throw new CookieError( + `option path is invalid: ${options.path}`, + CookieErrorCode.INVALID_PATH, + ); } str += "; Path=" + options.path; @@ -297,7 +337,10 @@ export function serialize( !isDate(options.expires) || !Number.isFinite(options.expires.valueOf()) ) { - throw new TypeError(`option expires is invalid: ${options.expires}`); + throw new CookieError( + `option expires is invalid: ${options.expires}. Must be a Date object`, + CookieErrorCode.INVALID_EXPIRES, + ); } str += "; Expires=" + options.expires.toUTCString(); @@ -331,8 +374,9 @@ export function serialize( str += "; Priority=High"; break; default: - throw new TypeError( + throw new CookieError( `option priority is invalid: ${options.priority}. Must be one of ${VALID_PRIORITIES_VALUES_STRING}`, + CookieErrorCode.INVALID_PRIORITY, ); } } @@ -354,8 +398,9 @@ export function serialize( str += "; SameSite=None"; break; default: - throw new TypeError( + throw new CookieError( `option sameSite is invalid: ${options.sameSite}. Must be boolean or one of ${VALID_SAMESITE_VALUE_STRING}`, + CookieErrorCode.INVALID_SAMESITE, ); } }