@fastify/cookie
Advanced tools
Comparing version 8.3.0 to 9.0.0
{ | ||
"name": "@fastify/cookie", | ||
"version": "8.3.0", | ||
"version": "9.0.0", | ||
"description": "Plugin for fastify to add support for cookies", | ||
@@ -8,10 +8,10 @@ "main": "plugin.js", | ||
"scripts": { | ||
"coverage": "npm run test:unit -- --coverage-report=html", | ||
"lint": "standard | snazzy", | ||
"lint:ci": "standard", | ||
"lint:fix": "standard --fix", | ||
"test": "npm run unit && npm run typescript", | ||
"typescript": "tsd", | ||
"unit": "tap -J \"test/*.test.js\"", | ||
"unit:report": "npm run unit -- --coverage-report=html", | ||
"unit:verbose": "npm run unit -- -Rspec" | ||
"test": "npm run test:unit && npm run test:typescript", | ||
"test:typescript": "tsd", | ||
"test:unit": "tap", | ||
"test:unit:verbose": "npm run test:unit -- -Rspec" | ||
}, | ||
@@ -44,10 +44,10 @@ "precommit": [ | ||
"@fastify/pre-commit": "^2.0.2", | ||
"@types/node": "^18.0.0", | ||
"@types/node": "^20.1.0", | ||
"benchmark": "^2.1.4", | ||
"fastify": "^4.0.0", | ||
"sinon": "^14.0.0", | ||
"sinon": "^15.0.0", | ||
"snazzy": "^9.0.0", | ||
"standard": "^17.0.0", | ||
"tap": "^16.0.0", | ||
"tsd": "^0.24.1" | ||
"tsd": "^0.28.0" | ||
}, | ||
@@ -54,0 +54,0 @@ "dependencies": { |
@@ -8,4 +8,7 @@ 'use strict' | ||
function fastifyCookieSetCookie (reply, name, value, options, signer) { | ||
const kReplySetCookies = Symbol('fastify.reply.setCookies') | ||
function fastifyCookieSetCookie (reply, name, value, options) { | ||
const opts = Object.assign({}, options) | ||
if (opts.expires && Number.isInteger(opts.expires)) { | ||
@@ -16,3 +19,3 @@ opts.expires = new Date(opts.expires) | ||
if (opts.signed) { | ||
value = signer.sign(value) | ||
value = reply.signCookie(value) | ||
} | ||
@@ -29,16 +32,4 @@ | ||
const serialized = cookie.serialize(name, value, opts) | ||
let setCookie = reply.getHeader('Set-Cookie') | ||
if (!setCookie) { | ||
reply.header('Set-Cookie', serialized) | ||
return reply | ||
} | ||
reply[kReplySetCookies].set(`${name};${opts.domain};${opts.path || '/'}`, { name, value, opts }) | ||
if (typeof setCookie === 'string') { | ||
setCookie = [setCookie] | ||
} | ||
setCookie.push(serialized) | ||
reply.removeHeader('Set-Cookie') | ||
reply.header('Set-Cookie', setCookie) | ||
return reply | ||
@@ -64,2 +55,3 @@ } | ||
} | ||
fastifyRes[kReplySetCookies] = new Map() | ||
done() | ||
@@ -73,2 +65,3 @@ } | ||
} | ||
fastifyRes[kReplySetCookies] = new Map() | ||
done() | ||
@@ -78,2 +71,32 @@ } | ||
function fastifyCookieOnSendHandler (fastifyReq, fastifyRes, payload, done) { | ||
if (fastifyRes[kReplySetCookies].size) { | ||
let setCookie = fastifyRes.getHeader('Set-Cookie') | ||
/* istanbul ignore else */ | ||
if (setCookie === undefined) { | ||
if (fastifyRes[kReplySetCookies].size === 1) { | ||
for (const c of fastifyRes[kReplySetCookies].values()) { | ||
fastifyRes.header('Set-Cookie', cookie.serialize(c.name, c.value, c.opts)) | ||
} | ||
return done() | ||
} | ||
setCookie = [] | ||
} else if (typeof setCookie === 'string') { | ||
setCookie = [setCookie] | ||
} | ||
for (const c of fastifyRes[kReplySetCookies].values()) { | ||
setCookie.push(cookie.serialize(c.name, c.value, c.opts)) | ||
} | ||
fastifyRes.removeHeader('Set-Cookie') | ||
fastifyRes.header('Set-Cookie', setCookie) | ||
} | ||
done() | ||
} | ||
function getHook (hook = 'onRequest') { | ||
@@ -98,5 +121,5 @@ const hooks = { | ||
const isSigner = !secret || (typeof secret.sign === 'function' && typeof secret.unsign === 'function') | ||
const algorithm = options.algorithm || 'sha256' | ||
const signer = isSigner ? secret : new Signer(secret, algorithm) | ||
const signer = isSigner ? secret : new Signer(secret, options.algorithm || 'sha256') | ||
fastify.decorate('serializeCookie', cookie.serialize) | ||
fastify.decorate('parseCookie', parseCookie) | ||
@@ -116,4 +139,5 @@ | ||
fastify.decorateRequest('cookies', null) | ||
fastify.decorateReply(kReplySetCookies, null) | ||
fastify.decorateReply('cookie', setCookie) | ||
fastify.decorateReply('setCookie', setCookie) | ||
@@ -124,2 +148,3 @@ fastify.decorateReply('clearCookie', clearCookie) | ||
fastify.addHook(hook, onReqHandlerWrapper(fastify, hook)) | ||
fastify.addHook('onSend', fastifyCookieOnSendHandler) | ||
} | ||
@@ -144,3 +169,3 @@ | ||
const opts = Object.assign({}, options.parseOptions, cookieOptions) | ||
return fastifyCookieSetCookie(this, name, value, opts, signer) | ||
return fastifyCookieSetCookie(this, name, value, opts) | ||
} | ||
@@ -147,0 +172,0 @@ |
@@ -126,3 +126,3 @@ 'use strict' | ||
test('share options for setCookie and clearCookie', (t) => { | ||
t.plan(11) | ||
t.plan(8) | ||
const fastify = Fastify() | ||
@@ -153,11 +153,8 @@ const secret = 'testsecret' | ||
const cookies = res.cookies | ||
t.equal(cookies.length, 2) | ||
t.equal(cookies.length, 1) | ||
t.equal(cookies[0].name, 'foo') | ||
t.equal(cookies[0].value, sign('foo', secret)) | ||
t.equal(cookies[0].maxAge, 36000) | ||
t.equal(cookies[0].value, '') | ||
t.equal(cookies[0].maxAge, undefined) | ||
t.equal(cookies[1].name, 'foo') | ||
t.equal(cookies[1].value, '') | ||
t.equal(cookies[1].path, '/') | ||
t.ok(new Date(cookies[1].expires) < new Date()) | ||
t.ok(new Date(cookies[0].expires) < new Date()) | ||
}) | ||
@@ -167,3 +164,3 @@ }) | ||
test('expires should not be overridden in clearCookie', (t) => { | ||
t.plan(11) | ||
t.plan(7) | ||
const fastify = Fastify() | ||
@@ -194,12 +191,7 @@ const secret = 'testsecret' | ||
const cookies = res.cookies | ||
t.equal(cookies.length, 2) | ||
t.equal(cookies.length, 1) | ||
t.equal(cookies[0].name, 'foo') | ||
t.equal(cookies[0].value, sign('foo', secret)) | ||
t.equal(cookies[0].value, '') | ||
const expires = new Date(cookies[0].expires) | ||
t.ok(expires < new Date(Date.now() + 5000)) | ||
t.equal(cookies[1].name, 'foo') | ||
t.equal(cookies[1].value, '') | ||
t.equal(cookies[1].path, '/') | ||
t.equal(Number(cookies[1].expires), 0) | ||
}) | ||
@@ -718,2 +710,14 @@ }) | ||
test('serialize cookie manually using decorator', (t) => { | ||
t.plan(2) | ||
const fastify = Fastify() | ||
fastify.register(plugin) | ||
fastify.ready(() => { | ||
t.ok(fastify.serializeCookie) | ||
t.same(fastify.serializeCookie('foo', 'bar', {}), 'foo=bar') | ||
t.end() | ||
}) | ||
}) | ||
test('parse cookie manually using decorator', (t) => { | ||
@@ -952,3 +956,3 @@ t.plan(2) | ||
test('clearCookie should include parseOptions', (t) => { | ||
t.plan(14) | ||
t.plan(10) | ||
const fastify = Fastify() | ||
@@ -984,16 +988,178 @@ fastify.register(plugin, { | ||
t.equal(cookies.length, 2) | ||
t.equal(cookies.length, 1) | ||
t.equal(cookies[0].name, 'foo') | ||
t.equal(cookies[0].value, 'foo') | ||
t.equal(cookies[0].maxAge, 36000) | ||
t.equal(cookies[0].value, '') | ||
t.equal(cookies[0].maxAge, undefined) | ||
t.equal(cookies[0].path, '/test') | ||
t.equal(cookies[0].domain, 'example.com') | ||
t.ok(new Date(cookies[0].expires) < new Date()) | ||
}) | ||
}) | ||
test('should update a cookie value when setCookie is called multiple times', (t) => { | ||
t.plan(15) | ||
const fastify = Fastify() | ||
const secret = 'testsecret' | ||
fastify.register(plugin, { secret }) | ||
const cookieOptions = { | ||
signed: true, | ||
path: '/foo', | ||
maxAge: 36000 | ||
} | ||
const cookieOptions2 = { | ||
signed: true, | ||
maxAge: 36000 | ||
} | ||
fastify.get('/test1', (req, reply) => { | ||
reply | ||
.setCookie('foo', 'foo', cookieOptions) | ||
.clearCookie('foo', cookieOptions) | ||
.setCookie('foo', 'foo', cookieOptions2) | ||
.setCookie('foos', 'foos', cookieOptions) | ||
.setCookie('foos', 'foosy', cookieOptions) | ||
.send({ hello: 'world' }) | ||
}) | ||
fastify.inject({ | ||
method: 'GET', | ||
url: '/test1' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.equal(res.statusCode, 200) | ||
t.same(JSON.parse(res.body), { hello: 'world' }) | ||
const cookies = res.cookies | ||
t.equal(cookies.length, 3) | ||
t.equal(cookies[0].name, 'foo') | ||
t.equal(cookies[0].value, '') | ||
t.equal(cookies[0].path, '/foo') | ||
t.equal(cookies[1].name, 'foo') | ||
t.equal(cookies[1].value, sign('foo', secret)) | ||
t.equal(cookies[1].maxAge, 36000) | ||
t.equal(cookies[2].name, 'foos') | ||
t.equal(cookies[2].value, sign('foosy', secret)) | ||
t.equal(cookies[2].path, '/foo') | ||
t.equal(cookies[2].maxAge, 36000) | ||
t.ok(new Date(cookies[0].expires) < new Date()) | ||
}) | ||
}) | ||
test('should update a cookie value when setCookie is called multiple times (empty header)', (t) => { | ||
t.plan(15) | ||
const fastify = Fastify() | ||
const secret = 'testsecret' | ||
fastify.register(plugin, { secret }) | ||
const cookieOptions = { | ||
signed: true, | ||
path: '/foo', | ||
maxAge: 36000 | ||
} | ||
const cookieOptions2 = { | ||
signed: true, | ||
maxAge: 36000 | ||
} | ||
fastify.get('/test1', (req, reply) => { | ||
reply | ||
.header('Set-Cookie', '', cookieOptions) | ||
.setCookie('foo', 'foo', cookieOptions) | ||
.clearCookie('foo', cookieOptions) | ||
.setCookie('foo', 'foo', cookieOptions2) | ||
.setCookie('foos', 'foos', cookieOptions) | ||
.setCookie('foos', 'foosy', cookieOptions) | ||
.send({ hello: 'world' }) | ||
}) | ||
fastify.inject({ | ||
method: 'GET', | ||
url: '/test1' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.equal(res.statusCode, 200) | ||
t.same(JSON.parse(res.body), { hello: 'world' }) | ||
const cookies = res.cookies | ||
t.equal(cookies.length, 3) | ||
t.equal(cookies[0].name, 'foo') | ||
t.equal(cookies[0].value, '') | ||
t.equal(cookies[0].path, '/foo') | ||
t.equal(cookies[1].name, 'foo') | ||
t.equal(cookies[1].value, sign('foo', secret)) | ||
t.equal(cookies[1].maxAge, 36000) | ||
t.equal(cookies[2].name, 'foos') | ||
t.equal(cookies[2].value, sign('foosy', secret)) | ||
t.equal(cookies[2].path, '/foo') | ||
t.equal(cookies[2].maxAge, 36000) | ||
t.ok(new Date(cookies[0].expires) < new Date()) | ||
}) | ||
}) | ||
test('should update a cookie value when setCookie is called multiple times (non-empty header)', (t) => { | ||
t.plan(15) | ||
const fastify = Fastify() | ||
const secret = 'testsecret' | ||
fastify.register(plugin, { secret }) | ||
const cookieOptions = { | ||
signed: true, | ||
path: '/foo', | ||
maxAge: 36000 | ||
} | ||
const cookieOptions2 = { | ||
signed: true, | ||
maxAge: 36000 | ||
} | ||
fastify.get('/test1', (req, reply) => { | ||
reply | ||
.header('Set-Cookie', 'manual=manual', cookieOptions) | ||
.setCookie('foo', 'foo', cookieOptions) | ||
.clearCookie('foo', cookieOptions) | ||
.setCookie('foo', 'foo', cookieOptions2) | ||
.setCookie('foos', 'foos', cookieOptions) | ||
.setCookie('foos', 'foosy', cookieOptions) | ||
.send({ hello: 'world' }) | ||
}) | ||
fastify.inject({ | ||
method: 'GET', | ||
url: '/test1' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.equal(res.statusCode, 200) | ||
t.same(JSON.parse(res.body), { hello: 'world' }) | ||
const cookies = res.cookies | ||
t.equal(cookies.length, 4) | ||
t.equal(cookies[1].name, 'foo') | ||
t.equal(cookies[1].value, '') | ||
t.equal(cookies[1].path, '/test') | ||
t.equal(cookies[1].domain, 'example.com') | ||
t.equal(cookies[1].path, '/foo') | ||
t.equal(cookies[2].name, 'foo') | ||
t.equal(cookies[2].value, sign('foo', secret)) | ||
t.equal(cookies[2].maxAge, 36000) | ||
t.equal(cookies[3].name, 'foos') | ||
t.equal(cookies[3].value, sign('foosy', secret)) | ||
t.equal(cookies[3].path, '/foo') | ||
t.equal(cookies[3].maxAge, 36000) | ||
t.ok(new Date(cookies[1].expires) < new Date()) | ||
}) | ||
}) |
@@ -8,2 +8,11 @@ /// <reference types='node' /> | ||
/** | ||
* Serialize a cookie name-value pair into a Set-Cookie header string | ||
* @param name Cookie name | ||
* @param value Cookie value | ||
* @param opts Options | ||
* @throws {TypeError} When maxAge option is invalid | ||
*/ | ||
serializeCookie(name: string, value: string, opts?: fastifyCookie.SerializeOptions): string; | ||
/** | ||
* Manual cookie parsing method | ||
@@ -109,5 +118,6 @@ * @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing | ||
export interface CookieSerializeOptions { | ||
export interface SerializeOptions { | ||
/** The `Domain` attribute. */ | ||
domain?: string; | ||
/** Specifies a function that will be used to encode a cookie's value. Since value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value. */ | ||
encode?(val: string): string; | ||
@@ -118,3 +128,3 @@ /** The expiration `date` used for the `Expires` attribute. If both `expires` and `maxAge` are set, then `expires` is used. */ | ||
httpOnly?: boolean; | ||
/** A `number` in milliseconds that specifies the `Expires` attribute by adding the specified milliseconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */ | ||
/** A `number` in seconds that specifies the `Expires` attribute by adding the specified seconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */ | ||
maxAge?: number; | ||
@@ -124,5 +134,9 @@ /** The `Path` attribute. Defaults to `/` (the root path). */ | ||
priority?: "low" | "medium" | "high"; | ||
/** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `node` or `strict`. */ | ||
/** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `none` or `strict`. */ | ||
sameSite?: 'lax' | 'none' | 'strict' | boolean; | ||
/** The `boolean` value of the `Secure` attribute. Set this option to false when communicating over an unencrypted (HTTP) connection. Value can be set to `auto`; in this case the `Secure` attribute will be set to false for HTTP request, in case of HTTPS it will be set to true. Defaults to true. */ | ||
secure?: boolean; | ||
} | ||
export interface CookieSerializeOptions extends Omit<SerializeOptions, 'secure'> { | ||
secure?: boolean | 'auto'; | ||
@@ -176,2 +190,2 @@ signed?: boolean; | ||
export = fastifyCookie; | ||
export = fastifyCookie; |
@@ -42,2 +42,6 @@ import cookie from '..'; | ||
server.after((_err) => { | ||
expectType< string >( | ||
server.serializeCookie('sessionId', 'aYb4uTIhdBXC') | ||
); | ||
expectType<{ [key: string]: string }>( | ||
@@ -44,0 +48,0 @@ // See https://github.com/fastify/fastify-cookie#manual-cookie-parsing |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
69435
18
1764
0