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

add specific error messages, error codes, to serialize validation #208

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Changes from all 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
77 changes: 67 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -150,6 +175,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.
*/
Expand Down Expand Up @@ -217,7 +250,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).
*
Expand All @@ -228,7 +261,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;
}

/**
Expand All @@ -248,37 +281,52 @@ 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;
if (!options) return str;

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;
}

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;
}

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;
Expand All @@ -289,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();
Expand Down Expand Up @@ -323,7 +374,10 @@ export function serialize(
str += "; Priority=High";
break;
default:
throw new TypeError(`option priority is invalid: ${options.priority}`);
throw new CookieError(
`option priority is invalid: ${options.priority}. Must be one of ${VALID_PRIORITIES_VALUES_STRING}`,
CookieErrorCode.INVALID_PRIORITY,
);
}
}

Expand All @@ -344,7 +398,10 @@ export function serialize(
str += "; SameSite=None";
break;
default:
throw new TypeError(`option sameSite is invalid: ${options.sameSite}`);
throw new CookieError(
`option sameSite is invalid: ${options.sameSite}. Must be boolean or one of ${VALID_SAMESITE_VALUE_STRING}`,
CookieErrorCode.INVALID_SAMESITE,
);
}
}

Expand Down
Loading