@fastify/cookie 8.3.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,11 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
+ branches:
6
+ - main
7
+ - master
8
+ - next
9
+ - 'v*'
5
10
  paths-ignore:
6
11
  - 'docs/**'
7
12
  - '*.md'
package/.taprc CHANGED
@@ -1,3 +1,2 @@
1
- 100: true
2
- check-coverage: true
3
- coverage: true
1
+ files:
2
+ - test/**/*.test.js
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const Fastify = require('fastify')
4
+ const plugin = require('../')
5
+
6
+ const secret = 'testsecret'
7
+
8
+ const fastify = Fastify()
9
+ fastify.register(plugin, { secret })
10
+
11
+ fastify.get('/', (req, reply) => {
12
+ reply
13
+ .setCookie('foo', 'foo')
14
+ .setCookie('foo', 'foo', { path: '/1' })
15
+ .setCookie('boo', 'boo', { path: '/' })
16
+ .setCookie('foo', 'foo-different', { path: '/' })
17
+ .setCookie('foo', 'foo', { path: '/2' })
18
+ .send({ hello: 'world' })
19
+ })
20
+
21
+ fastify.listen({ host: '127.0.0.1', port: 5001 }, (err, address) => {
22
+ if (err) throw err
23
+ console.log(address)
24
+ })
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const Fastify = require('fastify')
4
+ const plugin = require('../')
5
+
6
+ const secret = 'testsecret'
7
+
8
+ const fastify = Fastify()
9
+ fastify.register(plugin, { secret })
10
+
11
+ fastify.get('/', (req, reply) => {
12
+ reply
13
+ .setCookie('foo', 'foo', { path: '/' })
14
+ .send({ hello: 'world' })
15
+ })
16
+
17
+ fastify.listen({ host: '127.0.0.1', port: 5001 }, (err, address) => {
18
+ if (err) throw err
19
+ console.log(address)
20
+ })
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@fastify/cookie",
3
- "version": "8.3.0",
3
+ "version": "9.0.0",
4
4
  "description": "Plugin for fastify to add support for cookies",
5
5
  "main": "plugin.js",
6
6
  "types": "types/plugin.d.ts",
7
7
  "scripts": {
8
+ "coverage": "npm run test:unit -- --coverage-report=html",
8
9
  "lint": "standard | snazzy",
9
10
  "lint:ci": "standard",
10
11
  "lint:fix": "standard --fix",
11
- "test": "npm run unit && npm run typescript",
12
- "typescript": "tsd",
13
- "unit": "tap -J \"test/*.test.js\"",
14
- "unit:report": "npm run unit -- --coverage-report=html",
15
- "unit:verbose": "npm run unit -- -Rspec"
12
+ "test": "npm run test:unit && npm run test:typescript",
13
+ "test:typescript": "tsd",
14
+ "test:unit": "tap",
15
+ "test:unit:verbose": "npm run test:unit -- -Rspec"
16
16
  },
17
17
  "precommit": [
18
18
  "lint",
@@ -40,14 +40,14 @@
40
40
  "homepage": "https://github.com/fastify/fastify-cookie#readme",
41
41
  "devDependencies": {
42
42
  "@fastify/pre-commit": "^2.0.2",
43
- "@types/node": "^18.0.0",
43
+ "@types/node": "^20.1.0",
44
44
  "benchmark": "^2.1.4",
45
45
  "fastify": "^4.0.0",
46
- "sinon": "^14.0.0",
46
+ "sinon": "^15.0.0",
47
47
  "snazzy": "^9.0.0",
48
48
  "standard": "^17.0.0",
49
49
  "tap": "^16.0.0",
50
- "tsd": "^0.24.1"
50
+ "tsd": "^0.28.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "cookie": "^0.5.0",
package/plugin.js CHANGED
@@ -5,14 +5,17 @@ const cookie = require('cookie')
5
5
 
6
6
  const { Signer, sign, unsign } = require('./signer')
7
7
 
8
- function fastifyCookieSetCookie (reply, name, value, options, signer) {
8
+ const kReplySetCookies = Symbol('fastify.reply.setCookies')
9
+
10
+ function fastifyCookieSetCookie (reply, name, value, options) {
9
11
  const opts = Object.assign({}, options)
12
+
10
13
  if (opts.expires && Number.isInteger(opts.expires)) {
11
14
  opts.expires = new Date(opts.expires)
12
15
  }
13
16
 
14
17
  if (opts.signed) {
15
- value = signer.sign(value)
18
+ value = reply.signCookie(value)
16
19
  }
17
20
 
18
21
  if (opts.secure === 'auto') {
@@ -24,20 +27,8 @@ function fastifyCookieSetCookie (reply, name, value, options, signer) {
24
27
  }
25
28
  }
26
29
 
27
- const serialized = cookie.serialize(name, value, opts)
28
- let setCookie = reply.getHeader('Set-Cookie')
29
- if (!setCookie) {
30
- reply.header('Set-Cookie', serialized)
31
- return reply
32
- }
33
-
34
- if (typeof setCookie === 'string') {
35
- setCookie = [setCookie]
36
- }
30
+ reply[kReplySetCookies].set(`${name};${opts.domain};${opts.path || '/'}`, { name, value, opts })
37
31
 
38
- setCookie.push(serialized)
39
- reply.removeHeader('Set-Cookie')
40
- reply.header('Set-Cookie', setCookie)
41
32
  return reply
42
33
  }
43
34
 
@@ -58,6 +49,7 @@ function onReqHandlerWrapper (fastify, hook) {
58
49
  if (cookieHeader) {
59
50
  fastifyReq.cookies = fastify.parseCookie(cookieHeader)
60
51
  }
52
+ fastifyRes[kReplySetCookies] = new Map()
61
53
  done()
62
54
  }
63
55
  : function fastifyCookieHandler (fastifyReq, fastifyRes, done) {
@@ -66,10 +58,41 @@ function onReqHandlerWrapper (fastify, hook) {
66
58
  if (cookieHeader) {
67
59
  fastifyReq.cookies = fastify.parseCookie(cookieHeader)
68
60
  }
61
+ fastifyRes[kReplySetCookies] = new Map()
69
62
  done()
70
63
  }
71
64
  }
72
65
 
66
+ function fastifyCookieOnSendHandler (fastifyReq, fastifyRes, payload, done) {
67
+ if (fastifyRes[kReplySetCookies].size) {
68
+ let setCookie = fastifyRes.getHeader('Set-Cookie')
69
+
70
+ /* istanbul ignore else */
71
+ if (setCookie === undefined) {
72
+ if (fastifyRes[kReplySetCookies].size === 1) {
73
+ for (const c of fastifyRes[kReplySetCookies].values()) {
74
+ fastifyRes.header('Set-Cookie', cookie.serialize(c.name, c.value, c.opts))
75
+ }
76
+
77
+ return done()
78
+ }
79
+
80
+ setCookie = []
81
+ } else if (typeof setCookie === 'string') {
82
+ setCookie = [setCookie]
83
+ }
84
+
85
+ for (const c of fastifyRes[kReplySetCookies].values()) {
86
+ setCookie.push(cookie.serialize(c.name, c.value, c.opts))
87
+ }
88
+
89
+ fastifyRes.removeHeader('Set-Cookie')
90
+ fastifyRes.header('Set-Cookie', setCookie)
91
+ }
92
+
93
+ done()
94
+ }
95
+
73
96
  function getHook (hook = 'onRequest') {
74
97
  const hooks = {
75
98
  onRequest: 'onRequest',
@@ -89,9 +112,9 @@ function plugin (fastify, options, next) {
89
112
  return next(new Error('@fastify/cookie: Invalid value provided for the hook-option. You can set the hook-option only to false, \'onRequest\' , \'preParsing\' , \'preValidation\' or \'preHandler\''))
90
113
  }
91
114
  const isSigner = !secret || (typeof secret.sign === 'function' && typeof secret.unsign === 'function')
92
- const algorithm = options.algorithm || 'sha256'
93
- const signer = isSigner ? secret : new Signer(secret, algorithm)
115
+ const signer = isSigner ? secret : new Signer(secret, options.algorithm || 'sha256')
94
116
 
117
+ fastify.decorate('serializeCookie', cookie.serialize)
95
118
  fastify.decorate('parseCookie', parseCookie)
96
119
 
97
120
  if (typeof secret !== 'undefined') {
@@ -106,13 +129,15 @@ function plugin (fastify, options, next) {
106
129
  }
107
130
 
108
131
  fastify.decorateRequest('cookies', null)
109
- fastify.decorateReply('cookie', setCookie)
132
+ fastify.decorateReply(kReplySetCookies, null)
110
133
 
134
+ fastify.decorateReply('cookie', setCookie)
111
135
  fastify.decorateReply('setCookie', setCookie)
112
136
  fastify.decorateReply('clearCookie', clearCookie)
113
137
 
114
138
  if (hook) {
115
139
  fastify.addHook(hook, onReqHandlerWrapper(fastify, hook))
140
+ fastify.addHook('onSend', fastifyCookieOnSendHandler)
116
141
  }
117
142
 
118
143
  next()
@@ -132,7 +157,7 @@ function plugin (fastify, options, next) {
132
157
 
133
158
  function setCookie (name, value, cookieOptions) {
134
159
  const opts = Object.assign({}, options.parseOptions, cookieOptions)
135
- return fastifyCookieSetCookie(this, name, value, opts, signer)
160
+ return fastifyCookieSetCookie(this, name, value, opts)
136
161
  }
137
162
 
138
163
  function clearCookie (name, cookieOptions) {
@@ -123,7 +123,7 @@ test('cookies get set correctly with millisecond dates', (t) => {
123
123
  })
124
124
 
125
125
  test('share options for setCookie and clearCookie', (t) => {
126
- t.plan(11)
126
+ t.plan(8)
127
127
  const fastify = Fastify()
128
128
  const secret = 'testsecret'
129
129
  fastify.register(plugin, { secret })
@@ -149,20 +149,17 @@ test('share options for setCookie and clearCookie', (t) => {
149
149
  t.same(JSON.parse(res.body), { hello: 'world' })
150
150
 
151
151
  const cookies = res.cookies
152
- t.equal(cookies.length, 2)
152
+ t.equal(cookies.length, 1)
153
153
  t.equal(cookies[0].name, 'foo')
154
- t.equal(cookies[0].value, sign('foo', secret))
155
- t.equal(cookies[0].maxAge, 36000)
154
+ t.equal(cookies[0].value, '')
155
+ t.equal(cookies[0].maxAge, undefined)
156
156
 
157
- t.equal(cookies[1].name, 'foo')
158
- t.equal(cookies[1].value, '')
159
- t.equal(cookies[1].path, '/')
160
- t.ok(new Date(cookies[1].expires) < new Date())
157
+ t.ok(new Date(cookies[0].expires) < new Date())
161
158
  })
162
159
  })
163
160
 
164
161
  test('expires should not be overridden in clearCookie', (t) => {
165
- t.plan(11)
162
+ t.plan(7)
166
163
  const fastify = Fastify()
167
164
  const secret = 'testsecret'
168
165
  fastify.register(plugin, { secret })
@@ -188,16 +185,11 @@ test('expires should not be overridden in clearCookie', (t) => {
188
185
  t.same(JSON.parse(res.body), { hello: 'world' })
189
186
 
190
187
  const cookies = res.cookies
191
- t.equal(cookies.length, 2)
188
+ t.equal(cookies.length, 1)
192
189
  t.equal(cookies[0].name, 'foo')
193
- t.equal(cookies[0].value, sign('foo', secret))
190
+ t.equal(cookies[0].value, '')
194
191
  const expires = new Date(cookies[0].expires)
195
192
  t.ok(expires < new Date(Date.now() + 5000))
196
-
197
- t.equal(cookies[1].name, 'foo')
198
- t.equal(cookies[1].value, '')
199
- t.equal(cookies[1].path, '/')
200
- t.equal(Number(cookies[1].expires), 0)
201
193
  })
202
194
  })
203
195
 
@@ -711,6 +703,18 @@ test('issue 53', (t) => {
711
703
  })
712
704
  })
713
705
 
706
+ test('serialize cookie manually using decorator', (t) => {
707
+ t.plan(2)
708
+ const fastify = Fastify()
709
+ fastify.register(plugin)
710
+
711
+ fastify.ready(() => {
712
+ t.ok(fastify.serializeCookie)
713
+ t.same(fastify.serializeCookie('foo', 'bar', {}), 'foo=bar')
714
+ t.end()
715
+ })
716
+ })
717
+
714
718
  test('parse cookie manually using decorator', (t) => {
715
719
  t.plan(2)
716
720
  const fastify = Fastify()
@@ -944,7 +948,7 @@ test('if cookies are not set, then the handler creates an empty req.cookies obje
944
948
  })
945
949
 
946
950
  test('clearCookie should include parseOptions', (t) => {
947
- t.plan(14)
951
+ t.plan(10)
948
952
  const fastify = Fastify()
949
953
  fastify.register(plugin, {
950
954
  parseOptions: {
@@ -975,17 +979,179 @@ test('clearCookie should include parseOptions', (t) => {
975
979
 
976
980
  const cookies = res.cookies
977
981
 
978
- t.equal(cookies.length, 2)
982
+ t.equal(cookies.length, 1)
979
983
  t.equal(cookies[0].name, 'foo')
980
- t.equal(cookies[0].value, 'foo')
981
- t.equal(cookies[0].maxAge, 36000)
984
+ t.equal(cookies[0].value, '')
985
+ t.equal(cookies[0].maxAge, undefined)
982
986
  t.equal(cookies[0].path, '/test')
983
987
  t.equal(cookies[0].domain, 'example.com')
984
988
 
989
+ t.ok(new Date(cookies[0].expires) < new Date())
990
+ })
991
+ })
992
+
993
+ test('should update a cookie value when setCookie is called multiple times', (t) => {
994
+ t.plan(15)
995
+ const fastify = Fastify()
996
+ const secret = 'testsecret'
997
+ fastify.register(plugin, { secret })
998
+
999
+ const cookieOptions = {
1000
+ signed: true,
1001
+ path: '/foo',
1002
+ maxAge: 36000
1003
+ }
1004
+
1005
+ const cookieOptions2 = {
1006
+ signed: true,
1007
+ maxAge: 36000
1008
+ }
1009
+
1010
+ fastify.get('/test1', (req, reply) => {
1011
+ reply
1012
+ .setCookie('foo', 'foo', cookieOptions)
1013
+ .clearCookie('foo', cookieOptions)
1014
+ .setCookie('foo', 'foo', cookieOptions2)
1015
+ .setCookie('foos', 'foos', cookieOptions)
1016
+ .setCookie('foos', 'foosy', cookieOptions)
1017
+ .send({ hello: 'world' })
1018
+ })
1019
+
1020
+ fastify.inject({
1021
+ method: 'GET',
1022
+ url: '/test1'
1023
+ }, (err, res) => {
1024
+ t.error(err)
1025
+ t.equal(res.statusCode, 200)
1026
+ t.same(JSON.parse(res.body), { hello: 'world' })
1027
+
1028
+ const cookies = res.cookies
1029
+ t.equal(cookies.length, 3)
1030
+
1031
+ t.equal(cookies[0].name, 'foo')
1032
+ t.equal(cookies[0].value, '')
1033
+ t.equal(cookies[0].path, '/foo')
1034
+
1035
+ t.equal(cookies[1].name, 'foo')
1036
+ t.equal(cookies[1].value, sign('foo', secret))
1037
+ t.equal(cookies[1].maxAge, 36000)
1038
+
1039
+ t.equal(cookies[2].name, 'foos')
1040
+ t.equal(cookies[2].value, sign('foosy', secret))
1041
+ t.equal(cookies[2].path, '/foo')
1042
+ t.equal(cookies[2].maxAge, 36000)
1043
+
1044
+ t.ok(new Date(cookies[0].expires) < new Date())
1045
+ })
1046
+ })
1047
+
1048
+ test('should update a cookie value when setCookie is called multiple times (empty header)', (t) => {
1049
+ t.plan(15)
1050
+ const fastify = Fastify()
1051
+ const secret = 'testsecret'
1052
+ fastify.register(plugin, { secret })
1053
+
1054
+ const cookieOptions = {
1055
+ signed: true,
1056
+ path: '/foo',
1057
+ maxAge: 36000
1058
+ }
1059
+
1060
+ const cookieOptions2 = {
1061
+ signed: true,
1062
+ maxAge: 36000
1063
+ }
1064
+
1065
+ fastify.get('/test1', (req, reply) => {
1066
+ reply
1067
+ .header('Set-Cookie', '', cookieOptions)
1068
+ .setCookie('foo', 'foo', cookieOptions)
1069
+ .clearCookie('foo', cookieOptions)
1070
+ .setCookie('foo', 'foo', cookieOptions2)
1071
+ .setCookie('foos', 'foos', cookieOptions)
1072
+ .setCookie('foos', 'foosy', cookieOptions)
1073
+ .send({ hello: 'world' })
1074
+ })
1075
+
1076
+ fastify.inject({
1077
+ method: 'GET',
1078
+ url: '/test1'
1079
+ }, (err, res) => {
1080
+ t.error(err)
1081
+ t.equal(res.statusCode, 200)
1082
+ t.same(JSON.parse(res.body), { hello: 'world' })
1083
+
1084
+ const cookies = res.cookies
1085
+ t.equal(cookies.length, 3)
1086
+
1087
+ t.equal(cookies[0].name, 'foo')
1088
+ t.equal(cookies[0].value, '')
1089
+ t.equal(cookies[0].path, '/foo')
1090
+
1091
+ t.equal(cookies[1].name, 'foo')
1092
+ t.equal(cookies[1].value, sign('foo', secret))
1093
+ t.equal(cookies[1].maxAge, 36000)
1094
+
1095
+ t.equal(cookies[2].name, 'foos')
1096
+ t.equal(cookies[2].value, sign('foosy', secret))
1097
+ t.equal(cookies[2].path, '/foo')
1098
+ t.equal(cookies[2].maxAge, 36000)
1099
+
1100
+ t.ok(new Date(cookies[0].expires) < new Date())
1101
+ })
1102
+ })
1103
+
1104
+ test('should update a cookie value when setCookie is called multiple times (non-empty header)', (t) => {
1105
+ t.plan(15)
1106
+ const fastify = Fastify()
1107
+ const secret = 'testsecret'
1108
+ fastify.register(plugin, { secret })
1109
+
1110
+ const cookieOptions = {
1111
+ signed: true,
1112
+ path: '/foo',
1113
+ maxAge: 36000
1114
+ }
1115
+
1116
+ const cookieOptions2 = {
1117
+ signed: true,
1118
+ maxAge: 36000
1119
+ }
1120
+
1121
+ fastify.get('/test1', (req, reply) => {
1122
+ reply
1123
+ .header('Set-Cookie', 'manual=manual', cookieOptions)
1124
+ .setCookie('foo', 'foo', cookieOptions)
1125
+ .clearCookie('foo', cookieOptions)
1126
+ .setCookie('foo', 'foo', cookieOptions2)
1127
+ .setCookie('foos', 'foos', cookieOptions)
1128
+ .setCookie('foos', 'foosy', cookieOptions)
1129
+ .send({ hello: 'world' })
1130
+ })
1131
+
1132
+ fastify.inject({
1133
+ method: 'GET',
1134
+ url: '/test1'
1135
+ }, (err, res) => {
1136
+ t.error(err)
1137
+ t.equal(res.statusCode, 200)
1138
+ t.same(JSON.parse(res.body), { hello: 'world' })
1139
+
1140
+ const cookies = res.cookies
1141
+ t.equal(cookies.length, 4)
1142
+
985
1143
  t.equal(cookies[1].name, 'foo')
986
1144
  t.equal(cookies[1].value, '')
987
- t.equal(cookies[1].path, '/test')
988
- t.equal(cookies[1].domain, 'example.com')
1145
+ t.equal(cookies[1].path, '/foo')
1146
+
1147
+ t.equal(cookies[2].name, 'foo')
1148
+ t.equal(cookies[2].value, sign('foo', secret))
1149
+ t.equal(cookies[2].maxAge, 36000)
1150
+
1151
+ t.equal(cookies[3].name, 'foos')
1152
+ t.equal(cookies[3].value, sign('foosy', secret))
1153
+ t.equal(cookies[3].path, '/foo')
1154
+ t.equal(cookies[3].maxAge, 36000)
989
1155
 
990
1156
  t.ok(new Date(cookies[1].expires) < new Date())
991
1157
  })
package/types/plugin.d.ts CHANGED
@@ -4,6 +4,15 @@ import { FastifyPluginCallback } from "fastify";
4
4
 
5
5
  declare module "fastify" {
6
6
  interface FastifyInstance extends SignerMethods {
7
+ /**
8
+ * Serialize a cookie name-value pair into a Set-Cookie header string
9
+ * @param name Cookie name
10
+ * @param value Cookie value
11
+ * @param opts Options
12
+ * @throws {TypeError} When maxAge option is invalid
13
+ */
14
+ serializeCookie(name: string, value: string, opts?: fastifyCookie.SerializeOptions): string;
15
+
7
16
  /**
8
17
  * Manual cookie parsing method
9
18
  * @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
@@ -105,22 +114,27 @@ declare namespace fastifyCookie {
105
114
  unsign: (input: string) => UnsignResult;
106
115
  }
107
116
 
108
- export interface CookieSerializeOptions {
117
+ export interface SerializeOptions {
109
118
  /** The `Domain` attribute. */
110
119
  domain?: string;
120
+ /** 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. */
111
121
  encode?(val: string): string;
112
122
  /** The expiration `date` used for the `Expires` attribute. If both `expires` and `maxAge` are set, then `expires` is used. */
113
123
  expires?: Date;
114
124
  /** The `boolean` value of the `HttpOnly` attribute. Defaults to true. */
115
125
  httpOnly?: boolean;
116
- /** 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. */
126
+ /** 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. */
117
127
  maxAge?: number;
118
128
  /** The `Path` attribute. Defaults to `/` (the root path). */
119
129
  path?: string;
120
130
  priority?: "low" | "medium" | "high";
121
- /** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `node` or `strict`. */
131
+ /** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `none` or `strict`. */
122
132
  sameSite?: 'lax' | 'none' | 'strict' | boolean;
123
133
  /** 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. */
134
+ secure?: boolean;
135
+ }
136
+
137
+ export interface CookieSerializeOptions extends Omit<SerializeOptions, 'secure'> {
124
138
  secure?: boolean | 'auto';
125
139
  signed?: boolean;
126
140
  }
@@ -169,4 +183,4 @@ declare function fastifyCookie(
169
183
  ...params: Parameters<FastifyCookiePlugin>
170
184
  ): ReturnType<FastifyCookiePlugin>;
171
185
 
172
- export = fastifyCookie;
186
+ export = fastifyCookie;
@@ -39,6 +39,10 @@ const server = fastify();
39
39
  server.register(cookie);
40
40
 
41
41
  server.after((_err) => {
42
+ expectType< string >(
43
+ server.serializeCookie('sessionId', 'aYb4uTIhdBXC')
44
+ );
45
+
42
46
  expectType<{ [key: string]: string }>(
43
47
  // See https://github.com/fastify/fastify-cookie#manual-cookie-parsing
44
48
  server.parseCookie('sessionId=aYb4uTIhdBXC')