diff --git a/src/index.ts b/src/index.ts index a95a2e9..7add4e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,8 +68,6 @@ const pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/; */ const maxAgeRegExp = /^-?\d+$/; -const __toString = Object.prototype.toString; - const NullObject = /* @__PURE__ */ (() => { const C = function () {}; C.prototype = Object.create(null); @@ -276,28 +274,13 @@ export type SerializeOptions = StringifyOptions & * Serialize a name value pair into a cookie string suitable for * http headers. An optional options object specifies cookie parameters. * - * serialize('foo', 'bar', { httpOnly: true }) - * => "foo=bar; httpOnly" + * stringifySetCookie({ name: 'foo', value: 'bar', httpOnly: true }) + * => "foo=bar; HttpOnly" */ export function stringifySetCookie( cookie: SetCookie, options?: StringifyOptions, -): string; -export function stringifySetCookie( - name: string, - val: string, - options?: SerializeOptions, -): string; -export function stringifySetCookie( - _name: string | SetCookie, - _val?: string | StringifyOptions, - _opts?: SerializeOptions, ): string { - const cookie = - typeof _name === "object" - ? _name - : { ..._opts, name: _name, value: String(_val) }; - const options = typeof _val === "object" ? _val : _opts; const enc = options?.encode || encodeURIComponent; if (!cookieNameRegExp.test(cookie.name)) { @@ -337,7 +320,7 @@ export function stringifySetCookie( } if (cookie.expires) { - if (!isDate(cookie.expires) || !Number.isFinite(cookie.expires.valueOf())) { + if (!Number.isFinite(cookie.expires.valueOf())) { throw new TypeError(`option expires is invalid: ${cookie.expires}`); } @@ -403,7 +386,7 @@ export function stringifySetCookie( /** * Deserialize a `Set-Cookie` header into an object. * - * deserialize('foo=bar; httpOnly') + * parseSetCookie('foo=bar; HttpOnly') * => { name: 'foo', value: 'bar', httpOnly: true } */ export function parseSetCookie(str: string, options?: ParseOptions): SetCookie { @@ -533,15 +516,3 @@ function decode(str: string): string { return str; } } - -/** - * Determine if value is a Date. - */ -function isDate(val: any): val is Date { - return __toString.call(val) === "[object Date]"; -} - -/** - * Backward compatibility exports. - */ -export { stringifySetCookie as serialize, parseCookie as parse }; diff --git a/src/parse-cookie.spec.ts b/src/parse-cookie.spec.ts index 2444909..82a042a 100644 --- a/src/parse-cookie.spec.ts +++ b/src/parse-cookie.spec.ts @@ -11,10 +11,6 @@ describe("cookie.parseCookie", function () { }); }); - it("should have backward compatible export", function () { - expect(cookie.parse).toBe(cookie.parseCookie); - }); - it("should parse cookie string to object", function () { expect(cookie.parseCookie("foo=bar")).toEqual({ foo: "bar" }); expect(cookie.parseCookie("foo=123")).toEqual({ foo: "123" }); diff --git a/src/stringify-set-cookie.bench.ts b/src/stringify-set-cookie.bench.ts index 281656e..3d983b9 100644 --- a/src/stringify-set-cookie.bench.ts +++ b/src/stringify-set-cookie.bench.ts @@ -3,16 +3,24 @@ import * as cookie from "./index.js"; describe("cookie.stringifySetCookie", () => { bench("simple", () => { - cookie.stringifySetCookie("foo", "bar"); + cookie.stringifySetCookie({ + name: "foo", + value: "bar", + }); }); bench("encode", () => { - cookie.stringifySetCookie("foo", "hello there!"); + cookie.stringifySetCookie({ + name: "foo", + value: "hello there!", + }); }); const expires = new Date("Wed, 21 Oct 2015 07:28:00 GMT"); - bench("attributes", () => { - cookie.stringifySetCookie("foo", "bar", { + bench("all attributes", () => { + cookie.stringifySetCookie({ + name: "foo", + value: "bar", path: "/", domain: "example.com", maxAge: 3600, @@ -25,7 +33,7 @@ describe("cookie.stringifySetCookie", () => { }); }); - bench("object input", () => { + bench("typical object", () => { cookie.stringifySetCookie({ name: "foo", value: "bar", @@ -36,36 +44,4 @@ describe("cookie.stringifySetCookie", () => { sameSite: "strict", }); }); - - const setCookies10 = genSetCookies(10); - bench("10 set-cookies", () => { - for (const setCookie of setCookies10) { - cookie.stringifySetCookie(setCookie); - } - }); - - const setCookies100 = genSetCookies(100); - bench("100 set-cookies", () => { - for (const setCookie of setCookies100) { - cookie.stringifySetCookie(setCookie); - } - }); }); - -function genSetCookies(num: number): cookie.SetCookie[] { - const cookies: cookie.SetCookie[] = []; - - for (let i = 0; i < num; i++) { - cookies.push({ - name: "foo" + i, - value: "bar " + i, - path: "/foo" + i, - maxAge: 3600, - httpOnly: true, - secure: true, - sameSite: "lax", - }); - } - - return cookies; -} diff --git a/src/stringify-set-cookie.spec.ts b/src/stringify-set-cookie.spec.ts index 92a56ea..d944e1d 100644 --- a/src/stringify-set-cookie.spec.ts +++ b/src/stringify-set-cookie.spec.ts @@ -1,46 +1,32 @@ -import { describe, it, expect } from "vitest"; -import * as cookie from "./index.js"; +import { describe, expect, it } from "vitest"; +import { stringifySetCookie } from "./index.js"; -describe("cookie.stringifySetCookie", function () { - it("should have backward compatible export", function () { - expect(cookie.serialize).toBe(cookie.stringifySetCookie); - }); - - it("should serialize name and value", function () { - expect(cookie.stringifySetCookie("foo", "bar")).toEqual("foo=bar"); +describe("cookie.stringifySetCookie", () => { + it("should serialize name and value", () => { + expect(stringifySetCookie({ name: "foo", value: "bar" })).toEqual( + "foo=bar", + ); }); - it("should URL-encode value", function () { - expect(cookie.stringifySetCookie("foo", "bar +baz")).toEqual( + it("should URL-encode value", () => { + expect(stringifySetCookie({ name: "foo", value: "bar +baz" })).toEqual( "foo=bar%20%2Bbaz", ); }); - it("should serialize empty value", function () { - expect(cookie.stringifySetCookie("foo", "")).toEqual("foo="); - }); - - it("should serialize an object", function () { - expect( - cookie.stringifySetCookie({ name: "foo", value: "bar +baz" }), - ).toEqual("foo=bar%20%2Bbaz"); + it("should serialize empty value", () => { + expect(stringifySetCookie({ name: "foo", value: "" })).toEqual("foo="); }); - it("should serialize an object with options", function () { + it("should serialize with options", () => { expect( - cookie.stringifySetCookie( + stringifySetCookie( { name: "foo", value: "bar+baz" }, { encode: (x) => x }, ), ).toEqual("foo=bar+baz"); }); - it("should support three argument mode with value in options", function () { - expect( - cookie.stringifySetCookie("foo", "test", { value: "ignored" } as any), - ).toEqual("foo=test"); - }); - it.each([ ["foo"], ["foo,bar"], @@ -71,7 +57,7 @@ describe("cookie.stringifySetCookie", function () { ["foo?bar"], ["foo\\bar"], ])("should serialize name: %s", (name) => { - expect(cookie.stringifySetCookie(name, "baz")).toEqual(`${name}=baz`); + expect(stringifySetCookie({ name, value: "baz" })).toEqual(`${name}=baz`); }); it.each([ @@ -82,12 +68,12 @@ describe("cookie.stringifySetCookie", function () { ["foo bar"], ["foo\tbar"], ])("should throw for invalid name: %s", (name) => { - expect(() => cookie.stringifySetCookie(name, "bar")).toThrow( + expect(() => stringifySetCookie({ name, value: "bar" })).toThrow( /argument name is invalid/, ); }); - describe('with "domain" option', function () { + describe('with "domain" option', () => { it.each([ ["example.com"], ["sub.example.com"], @@ -95,9 +81,8 @@ describe("cookie.stringifySetCookie", function () { ["localhost"], [".localhost"], ["my-site.org"], - ["localhost"], ])("should serialize domain: %s", (domain) => { - expect(cookie.stringifySetCookie("foo", "bar", { domain })).toEqual( + expect(stringifySetCookie({ name: "foo", value: "bar", domain })).toEqual( `foo=bar; Domain=${domain}`, ); }); @@ -110,20 +95,21 @@ describe("cookie.stringifySetCookie", function () { ["example.com; Path=/"], ["example.com /* inject a comment */"], ])("should throw for invalid domain: %s", (domain) => { - expect(() => cookie.stringifySetCookie("foo", "bar", { domain })).toThrow( - /option domain is invalid/, - ); + expect(() => + stringifySetCookie({ name: "foo", value: "bar", domain }), + ).toThrow(/option domain is invalid/); }); }); - describe('with "encode" option', function () { - it("should specify alternative value encoder", function () { + describe('with "encode" option', () => { + it("should specify alternative value encoder", () => { expect( - cookie.stringifySetCookie("foo", "bar", { - encode: function (v) { - return Buffer.from(v, "utf8").toString("base64"); + stringifySetCookie( + { name: "foo", value: "bar" }, + { + encode: (v) => Buffer.from(v, "utf8").toString("base64"), }, - }), + ), ).toEqual("foo=YmFy"); }); @@ -131,7 +117,7 @@ describe("cookie.stringifySetCookie", function () { "should serialize value: %s", (value) => { expect( - cookie.stringifySetCookie("foo", value, { encode: (x) => x }), + stringifySetCookie({ name: "foo", value }, { encode: (x) => x }), ).toEqual(`foo=${value}`); }, ); @@ -140,228 +126,146 @@ describe("cookie.stringifySetCookie", function () { "should throw for invalid value: %s", (value) => { expect(() => - cookie.stringifySetCookie("foo", value, { encode: (x) => x }), + stringifySetCookie({ name: "foo", value }, { encode: (x) => x }), ).toThrow(/argument val is invalid/); }, ); }); - describe('with "expires" option', function () { - it("should throw on invalid date", function () { - expect( - cookie.stringifySetCookie.bind(cookie, "foo", "bar", { + describe('with "expires" option', () => { + it("should throw on invalid date", () => { + expect(() => + stringifySetCookie({ + name: "foo", + value: "bar", expires: new Date(NaN), }), ).toThrow(/option expires is invalid/); }); - it("should set expires to given date", function () { + it("should set expires to given date", () => { expect( - cookie.stringifySetCookie("foo", "bar", { + stringifySetCookie({ + name: "foo", + value: "bar", expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)), }), ).toEqual("foo=bar; Expires=Sun, 24 Dec 2000 10:30:59 GMT"); }); }); - describe('with "httpOnly" option', function () { - it("should include httpOnly flag when true", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { httpOnly: true }), - ).toEqual("foo=bar; HttpOnly"); - }); - - it("should not include httpOnly flag when false", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { httpOnly: false }), - ).toEqual("foo=bar"); - }); - }); - - describe('with "maxAge" option', function () { - it("should throw when not a number", function () { - expect(function () { - cookie.stringifySetCookie("foo", "bar", { maxAge: "buzz" as any }); - }).toThrow(/option maxAge is invalid/); - }); - - it("should throw when Infinity", function () { - expect(function () { - cookie.stringifySetCookie("foo", "bar", { maxAge: Infinity }); - }).toThrow(/option maxAge is invalid/); - }); - - it("should throw when max-age is not an integer", function () { - expect(function () { - cookie.stringifySetCookie("foo", "bar", { maxAge: 3.14 }); - }).toThrow(/option maxAge is invalid/); - }); - - it("should set max-age to value", function () { - expect(cookie.stringifySetCookie("foo", "bar", { maxAge: 1000 })).toEqual( - "foo=bar; Max-Age=1000", - ); - expect(cookie.stringifySetCookie("foo", "bar", { maxAge: 0 })).toEqual( - "foo=bar; Max-Age=0", - ); - }); - - it("should not set when undefined", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { maxAge: undefined }), - ).toEqual("foo=bar"); + describe('with "maxAge" option', () => { + it.each([ + ["non-number", "buzz"], + ["Infinity", Infinity], + ["non-integer", 3.14], + ])("should throw when maxAge is %s", (_label, maxAge) => { + expect(() => + stringifySetCookie({ name: "foo", value: "bar", maxAge } as any), + ).toThrow(/option maxAge is invalid/); }); - }); - describe('with "partitioned" option', function () { - it("should include partitioned flag when true", function () { + it("should set max-age to value", () => { expect( - cookie.stringifySetCookie("foo", "bar", { partitioned: true }), - ).toEqual("foo=bar; Partitioned"); - }); - - it("should not include partitioned flag when false", function () { + stringifySetCookie({ name: "foo", value: "bar", maxAge: 1000 }), + ).toEqual("foo=bar; Max-Age=1000"); expect( - cookie.stringifySetCookie("foo", "bar", { partitioned: false }), - ).toEqual("foo=bar"); - }); - - it("should not include partitioned flag when not defined", function () { - expect(cookie.stringifySetCookie("foo", "bar", {})).toEqual("foo=bar"); + stringifySetCookie({ name: "foo", value: "bar", maxAge: 0 }), + ).toEqual("foo=bar; Max-Age=0"); }); }); - describe('with "path" option', function () { - it("should serialize path", function () { - var validPaths = [ - "/", - "/login", - "/foo.bar/baz", - "/foo-bar", - "/foo=bar?baz", - '/foo"bar"', - "/../foo/bar", - "../foo/", - "./", - ]; - - validPaths.forEach(function (path) { - expect(cookie.stringifySetCookie("foo", "bar", { path: path })).toEqual( - "foo=bar; Path=" + path, - ); - }); + describe('with "path" option', () => { + it.each([ + ["/"], + ["/login"], + ["/foo.bar/baz"], + ["/foo-bar"], + ["/foo=bar?baz"], + ['/foo"bar"'], + ["/../foo/bar"], + ["../foo/"], + ["./"], + ])("should serialize path: %s", (path) => { + expect(stringifySetCookie({ name: "foo", value: "bar", path })).toEqual( + `foo=bar; Path=${path}`, + ); }); - it("should throw for invalid value", function () { - var invalidPaths = [ - "/\n", - "/foo\u0000", - "/path/with\rnewline", - "/; Path=/sensitive-data", - '/login">', - ]; - - invalidPaths.forEach(function (path) { - expect( - cookie.stringifySetCookie.bind(cookie, "foo", "bar", { path: path }), - ).toThrow(/option path is invalid/); - }); + it.each([ + ["/\n"], + ["/foo\u0000"], + ["/path/with\rnewline"], + ["/; Path=/sensitive-data"], + ['/login">'], + ])("should throw for invalid path: %s", (path) => { + expect(() => + stringifySetCookie({ name: "foo", value: "bar", path }), + ).toThrow(/option path is invalid/); }); }); - describe('with "priority" option', function () { - it("should throw on invalid priority", function () { - expect(function () { - cookie.stringifySetCookie("foo", "bar", { priority: "foo" as any }); - }).toThrow(/option priority is invalid/); - }); - - it("should throw on non-string", function () { - expect(function () { - cookie.stringifySetCookie("foo", "bar", { priority: 42 as any }); - }).toThrow(/option priority is invalid/); - }); - - it("should set priority low", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { priority: "low" }), - ).toEqual("foo=bar; Priority=Low"); - }); - - it("should set priority medium", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { priority: "medium" }), - ).toEqual("foo=bar; Priority=Medium"); - }); - - it("should set priority high", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { priority: "high" }), - ).toEqual("foo=bar; Priority=High"); - }); - - it("should set priority case insensitive", function () { + describe('with "boolean attributes"', () => { + it("should include enabled flags", () => { expect( - cookie.stringifySetCookie("foo", "bar", { priority: "High" as any }), - ).toEqual("foo=bar; Priority=High"); + stringifySetCookie({ + name: "foo", + value: "bar", + httpOnly: true, + secure: true, + partitioned: true, + }), + ).toEqual("foo=bar; HttpOnly; Secure; Partitioned"); }); }); - describe('with "sameSite" option', function () { - it("should throw on invalid sameSite", function () { - expect(() => { - cookie.stringifySetCookie("foo", "bar", { sameSite: "foo" as any }); - }).toThrow(/option sameSite is invalid/); - }); - - it("should set sameSite strict", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: "strict" }), - ).toEqual("foo=bar; SameSite=Strict"); - }); - - it("should set sameSite lax", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: "lax" }), - ).toEqual("foo=bar; SameSite=Lax"); - }); - - it("should set sameSite none", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: "none" }), - ).toEqual("foo=bar; SameSite=None"); - }); - - it("should set sameSite strict when true", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: true }), - ).toEqual("foo=bar; SameSite=Strict"); - }); - - it("should not set sameSite when false", function () { - expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: false }), - ).toEqual("foo=bar"); + describe('with "priority" option', () => { + it.each([ + ["invalid priority", "foo"], + ["non-string", 42], + ])("should throw on %s", (_label, priority) => { + expect(() => + stringifySetCookie({ name: "foo", value: "bar", priority } as any), + ).toThrow(/option priority is invalid/); }); - it("should set sameSite case insensitive", function () { + it.each([ + ["low", "Low"], + ["medium", "Medium"], + ["high", "High"], + ["High", "High"], + ])("should set priority %s", (priority, expected) => { expect( - cookie.stringifySetCookie("foo", "bar", { sameSite: "Lax" as any }), - ).toEqual("foo=bar; SameSite=Lax"); + stringifySetCookie({ + name: "foo", + value: "bar", + priority: priority as any, + }), + ).toEqual(`foo=bar; Priority=${expected}`); }); }); - describe('with "secure" option', function () { - it("should include secure flag when true", function () { - expect(cookie.stringifySetCookie("foo", "bar", { secure: true })).toEqual( - "foo=bar; Secure", - ); + describe('with "sameSite" option', () => { + it("should throw on invalid sameSite", () => { + expect(() => + stringifySetCookie({ + name: "foo", + value: "bar", + sameSite: "foo" as any, + }), + ).toThrow(/option sameSite is invalid/); }); - it("should not include secure flag when false", function () { + it.each([ + ["strict", "Strict"], + ["lax", "Lax"], + ["none", "None"], + [true, "Strict"], + ["Lax", "Lax"], + ])("should set sameSite %s", (sameSite, expected) => { expect( - cookie.stringifySetCookie("foo", "bar", { secure: false }), - ).toEqual("foo=bar"); + stringifySetCookie({ name: "foo", value: "bar", sameSite } as any), + ).toEqual(`foo=bar; SameSite=${expected}`); }); }); });