@fastify/cookie 7.3.0 → 8.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.
- package/.github/workflows/ci.yml +1 -0
- package/README.md +6 -6
- package/benchmark/signer-multi.js +18 -0
- package/benchmark/signer.js +17 -0
- package/package.json +6 -7
- package/plugin.js +22 -11
- package/signer.js +108 -24
- package/test/cookie.test.js +45 -12
- package/test/signer.test.js +72 -17
- package/types/plugin.d.ts +169 -0
- package/{test → types}/plugin.test-d.ts +21 -1
- package/plugin.d.ts +0 -133
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -192,9 +192,9 @@ fastify.register(require('@fastify/cookie'), {
|
|
|
192
192
|
|
|
193
193
|
### Manual cookie unsigning
|
|
194
194
|
|
|
195
|
-
The method `unsignCookie(value)` is added to the `fastify` instance and the `reply` object
|
|
196
|
-
via the Fastify `decorate`
|
|
197
|
-
the provided signer's (or the default signer if no custom implementation is provided) `unsign` method on the cookie.
|
|
195
|
+
The method `unsignCookie(value)` is added to the `fastify` instance, to the `request` and the `reply` object
|
|
196
|
+
via the Fastify `decorate`, `decorateRequest` and `decorateReply` APIs, if a secret was provided as option.
|
|
197
|
+
Using it on a signed cookie will call the the provided signer's (or the default signer if no custom implementation is provided) `unsign` method on the cookie.
|
|
198
198
|
|
|
199
199
|
**Example:**
|
|
200
200
|
|
|
@@ -236,12 +236,12 @@ test('Request requires signed cookie', async () => {
|
|
|
236
236
|
|
|
237
237
|
### Manual signing/unsigning with low level utilities
|
|
238
238
|
|
|
239
|
-
with
|
|
239
|
+
with Signer
|
|
240
240
|
|
|
241
241
|
```js
|
|
242
|
-
const {
|
|
242
|
+
const { Signer } = require('@fastify/cookie');
|
|
243
243
|
|
|
244
|
-
const signer =
|
|
244
|
+
const signer = new Signer('secret');
|
|
245
245
|
const signedValue = signer.sign('test');
|
|
246
246
|
const {valid, renew, value } = signer.unsign(signedValue);
|
|
247
247
|
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const Benchmark = require('benchmark')
|
|
2
|
+
const Signer = require('../signer')
|
|
3
|
+
const suite = new Benchmark.Suite()
|
|
4
|
+
|
|
5
|
+
const signer = Signer(['abcd', 'asdf', 'vadfa'])
|
|
6
|
+
const signedValue = signer.sign('test', 'vadfa')
|
|
7
|
+
|
|
8
|
+
suite
|
|
9
|
+
.add('sign', function () {
|
|
10
|
+
signer.sign('test')
|
|
11
|
+
})
|
|
12
|
+
.add('unsign', function () {
|
|
13
|
+
signer.unsign(signedValue)
|
|
14
|
+
})
|
|
15
|
+
.on('cycle', function (event) {
|
|
16
|
+
console.log(String(event.target))
|
|
17
|
+
})
|
|
18
|
+
.run({ delay: 0 })
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const Benchmark = require('benchmark')
|
|
2
|
+
const Signer = require('../signer')
|
|
3
|
+
const suite = new Benchmark.Suite()
|
|
4
|
+
|
|
5
|
+
const signer = Signer('abcd')
|
|
6
|
+
const signedValue = signer.sign('test')
|
|
7
|
+
suite
|
|
8
|
+
.add('sign', function () {
|
|
9
|
+
signer.sign('test')
|
|
10
|
+
})
|
|
11
|
+
.add('unsign', function () {
|
|
12
|
+
signer.unsign(signedValue)
|
|
13
|
+
})
|
|
14
|
+
.on('cycle', function (event) {
|
|
15
|
+
console.log(String(event.target))
|
|
16
|
+
})
|
|
17
|
+
.run({ delay: 0 })
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/cookie",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Plugin for fastify to add support for cookies",
|
|
5
5
|
"main": "plugin.js",
|
|
6
|
-
"types": "plugin.d.ts",
|
|
6
|
+
"types": "types/plugin.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"lint": "standard | snazzy",
|
|
9
9
|
"lint:ci": "standard",
|
|
@@ -39,19 +39,18 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/fastify/fastify-cookie#readme",
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@fastify/pre-commit": "^2.0.2",
|
|
42
43
|
"@types/node": "^18.0.0",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
44
|
+
"benchmark": "^2.1.4",
|
|
45
|
+
"fastify": "^4.0.0",
|
|
45
46
|
"sinon": "^14.0.0",
|
|
46
47
|
"snazzy": "^9.0.0",
|
|
47
48
|
"standard": "^17.0.0",
|
|
48
49
|
"tap": "^16.0.0",
|
|
49
|
-
"tsd": "^0.22.0"
|
|
50
|
-
"typescript": "^4.5.5"
|
|
50
|
+
"tsd": "^0.22.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"cookie": "^0.5.0",
|
|
54
|
-
"cookie-signature": "^1.1.0",
|
|
55
54
|
"fastify-plugin": "^4.0.0"
|
|
56
55
|
},
|
|
57
56
|
"tsd": {
|
package/plugin.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { sign, unsign } = require('cookie-signature')
|
|
4
3
|
const fp = require('fastify-plugin')
|
|
5
4
|
const cookie = require('cookie')
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const { Signer, sign, unsign } = require('./signer')
|
|
8
7
|
|
|
9
8
|
function fastifyCookieSetCookie (reply, name, value, options, signer) {
|
|
10
9
|
const opts = Object.assign({}, options)
|
|
@@ -63,20 +62,29 @@ function onReqHandlerWrapper (fastify) {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
function plugin (fastify, options, next) {
|
|
66
|
-
const secret = options.secret
|
|
65
|
+
const secret = options.secret
|
|
67
66
|
const enableRotation = Array.isArray(secret)
|
|
68
|
-
const
|
|
67
|
+
const algorithm = options.algorithm || 'sha256'
|
|
68
|
+
const signer = typeof secret === 'string' || enableRotation ? new Signer(secret, algorithm) : secret
|
|
69
69
|
|
|
70
70
|
fastify.decorate('parseCookie', parseCookie)
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
|
|
72
|
+
if (typeof secret !== 'undefined') {
|
|
73
|
+
fastify.decorate('signCookie', signCookie)
|
|
74
|
+
fastify.decorate('unsignCookie', unsignCookie)
|
|
75
|
+
|
|
76
|
+
fastify.decorateRequest('signCookie', signCookie)
|
|
77
|
+
fastify.decorateRequest('unsignCookie', unsignCookie)
|
|
78
|
+
|
|
79
|
+
fastify.decorateReply('signCookie', signCookie)
|
|
80
|
+
fastify.decorateReply('unsignCookie', unsignCookie)
|
|
81
|
+
}
|
|
73
82
|
|
|
74
83
|
fastify.decorateRequest('cookies', null)
|
|
75
|
-
fastify.decorateRequest('unsignCookie', unsignCookie)
|
|
76
|
-
fastify.decorateReply('setCookie', setCookie)
|
|
77
84
|
fastify.decorateReply('cookie', setCookie)
|
|
85
|
+
|
|
86
|
+
fastify.decorateReply('setCookie', setCookie)
|
|
78
87
|
fastify.decorateReply('clearCookie', clearCookie)
|
|
79
|
-
fastify.decorateReply('unsignCookie', unsignCookie)
|
|
80
88
|
|
|
81
89
|
fastify.addHook('onRequest', onReqHandlerWrapper(fastify))
|
|
82
90
|
|
|
@@ -127,14 +135,17 @@ const fastifyCookie = fp(plugin, {
|
|
|
127
135
|
* - `import { fastifyCookie } from 'fastify-cookie'`
|
|
128
136
|
* - `import fastifyCookie from 'fastify-cookie'`
|
|
129
137
|
*/
|
|
138
|
+
fastifyCookie.signerFactory = Signer
|
|
130
139
|
fastifyCookie.fastifyCookie = fastifyCookie
|
|
131
140
|
fastifyCookie.default = fastifyCookie
|
|
132
141
|
module.exports = fastifyCookie
|
|
133
142
|
|
|
134
|
-
fastifyCookie.fastifyCookie.signerFactory =
|
|
143
|
+
fastifyCookie.fastifyCookie.signerFactory = Signer
|
|
144
|
+
fastifyCookie.fastifyCookie.Signer = Signer
|
|
135
145
|
fastifyCookie.fastifyCookie.sign = sign
|
|
136
146
|
fastifyCookie.fastifyCookie.unsign = unsign
|
|
137
147
|
|
|
138
|
-
module.exports.signerFactory =
|
|
148
|
+
module.exports.signerFactory = Signer
|
|
149
|
+
module.exports.Signer = Signer
|
|
139
150
|
module.exports.sign = sign
|
|
140
151
|
module.exports.unsign = unsign
|
package/signer.js
CHANGED
|
@@ -1,32 +1,116 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Inspired by node-cookie-signature
|
|
4
|
+
// https://github.com/tj/node-cookie-signature
|
|
5
|
+
//
|
|
6
|
+
// The MIT License
|
|
7
|
+
// Copyright (c) 2012–2022 LearnBoost <tj@learnboost.com> and other contributors;
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
const secrets = Array.isArray(secret) ? secret : [secret]
|
|
7
|
-
const [signingKey] = secrets
|
|
9
|
+
const crypto = require('crypto')
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
let renew = false
|
|
16
|
-
let value = null
|
|
17
|
-
|
|
18
|
-
for (const key of secrets) {
|
|
19
|
-
const result = cookieSignature.unsign(signedValue, key)
|
|
20
|
-
|
|
21
|
-
if (result !== false) {
|
|
22
|
-
valid = true
|
|
23
|
-
renew = key !== signingKey
|
|
24
|
-
value = result
|
|
25
|
-
break
|
|
26
|
-
}
|
|
27
|
-
}
|
|
11
|
+
const base64PaddingRE = /=/g
|
|
12
|
+
|
|
13
|
+
function Signer (secrets, algorithm = 'sha256') {
|
|
14
|
+
if (!(this instanceof Signer)) {
|
|
15
|
+
return new Signer(secrets, algorithm)
|
|
16
|
+
}
|
|
28
17
|
|
|
29
|
-
|
|
18
|
+
this.secrets = Array.isArray(secrets) ? secrets : [secrets]
|
|
19
|
+
this.signingKey = this.secrets[0]
|
|
20
|
+
this.algorithm = algorithm
|
|
21
|
+
|
|
22
|
+
validateSecrets(this.secrets)
|
|
23
|
+
validateAlgorithm(this.algorithm)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateSecrets (secrets) {
|
|
27
|
+
for (const secret of secrets) {
|
|
28
|
+
if (typeof secret !== 'string') {
|
|
29
|
+
throw new TypeError('Secret key must be a string.')
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
function validateAlgorithm (algorithm) {
|
|
35
|
+
// validate that the algorithm is supported by the node runtime
|
|
36
|
+
try {
|
|
37
|
+
crypto.createHmac(algorithm, 'dummyHmac')
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new TypeError(`Algorithm ${algorithm} not supported.`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _sign (value, secret, algorithm) {
|
|
44
|
+
if (typeof value !== 'string') {
|
|
45
|
+
throw new TypeError('Cookie value must be provided as a string.')
|
|
46
|
+
}
|
|
47
|
+
return value + '.' + crypto
|
|
48
|
+
.createHmac(algorithm, secret)
|
|
49
|
+
.update(value)
|
|
50
|
+
.digest('base64')
|
|
51
|
+
// remove base64 padding (=) as it has special meaning in cookies
|
|
52
|
+
.replace(base64PaddingRE, '')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _unsign (signedValue, secrets, algorithm) {
|
|
56
|
+
if (typeof signedValue !== 'string') {
|
|
57
|
+
throw new TypeError('Signed cookie string must be provided.')
|
|
58
|
+
}
|
|
59
|
+
const value = signedValue.slice(0, signedValue.lastIndexOf('.'))
|
|
60
|
+
const actual = Buffer.from(signedValue.slice(signedValue.lastIndexOf('.') + 1))
|
|
61
|
+
|
|
62
|
+
for (const secret of secrets) {
|
|
63
|
+
const expected = Buffer.from(crypto
|
|
64
|
+
.createHmac(algorithm, secret)
|
|
65
|
+
.update(value)
|
|
66
|
+
.digest('base64')
|
|
67
|
+
// remove base64 padding (=) as it has special meaning in cookies
|
|
68
|
+
.replace(base64PaddingRE, ''))
|
|
69
|
+
if (
|
|
70
|
+
expected.length === actual.length &&
|
|
71
|
+
crypto.timingSafeEqual(expected, actual)
|
|
72
|
+
) {
|
|
73
|
+
return {
|
|
74
|
+
valid: true,
|
|
75
|
+
renew: secret !== secrets[0],
|
|
76
|
+
value
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
renew: false,
|
|
84
|
+
value: null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Signer.prototype.sign = function (value) {
|
|
89
|
+
return _sign(value, this.signingKey, this.algorithm)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Signer.prototype.unsign = function (signedValue) {
|
|
93
|
+
return _unsign(signedValue, this.secrets, this.algorithm)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sign (value, secret, algorithm = 'sha256') {
|
|
97
|
+
const secrets = Array.isArray(secret) ? secret : [secret]
|
|
98
|
+
|
|
99
|
+
validateSecrets(secrets)
|
|
100
|
+
|
|
101
|
+
return _sign(value, secrets[0], algorithm)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function unsign (signedValue, secret, algorithm = 'sha256') {
|
|
105
|
+
const secrets = Array.isArray(secret) ? secret : [secret]
|
|
106
|
+
|
|
107
|
+
validateSecrets(secrets)
|
|
108
|
+
|
|
109
|
+
return _unsign(signedValue, secrets, algorithm)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = Signer
|
|
113
|
+
module.exports.signerFactory = Signer
|
|
114
|
+
module.exports.Signer = Signer
|
|
115
|
+
module.exports.sign = sign
|
|
116
|
+
module.exports.unsign = unsign
|
package/test/cookie.test.js
CHANGED
|
@@ -4,7 +4,7 @@ const tap = require('tap')
|
|
|
4
4
|
const test = tap.test
|
|
5
5
|
const Fastify = require('fastify')
|
|
6
6
|
const sinon = require('sinon')
|
|
7
|
-
const
|
|
7
|
+
const { sign, unsign } = require('../signer')
|
|
8
8
|
const plugin = require('../')
|
|
9
9
|
|
|
10
10
|
test('cookies get set correctly', (t) => {
|
|
@@ -151,7 +151,7 @@ test('share options for setCookie and clearCookie', (t) => {
|
|
|
151
151
|
const cookies = res.cookies
|
|
152
152
|
t.equal(cookies.length, 2)
|
|
153
153
|
t.equal(cookies[0].name, 'foo')
|
|
154
|
-
t.equal(cookies[0].value,
|
|
154
|
+
t.equal(cookies[0].value, sign('foo', secret))
|
|
155
155
|
t.equal(cookies[0].maxAge, 36000)
|
|
156
156
|
|
|
157
157
|
t.equal(cookies[1].name, 'foo')
|
|
@@ -190,7 +190,7 @@ test('expires should not be overridden in clearCookie', (t) => {
|
|
|
190
190
|
const cookies = res.cookies
|
|
191
191
|
t.equal(cookies.length, 2)
|
|
192
192
|
t.equal(cookies[0].name, 'foo')
|
|
193
|
-
t.equal(cookies[0].value,
|
|
193
|
+
t.equal(cookies[0].value, sign('foo', secret))
|
|
194
194
|
const expires = new Date(cookies[0].expires)
|
|
195
195
|
t.ok(expires < new Date(Date.now() + 5000))
|
|
196
196
|
|
|
@@ -378,7 +378,7 @@ test('cookies signature', (t) => {
|
|
|
378
378
|
const cookies = res.cookies
|
|
379
379
|
t.equal(cookies.length, 1)
|
|
380
380
|
t.equal(cookies[0].name, 'foo')
|
|
381
|
-
t.
|
|
381
|
+
t.same(unsign(cookies[0].value, secret), { valid: true, renew: false, value: 'foo' })
|
|
382
382
|
})
|
|
383
383
|
})
|
|
384
384
|
|
|
@@ -406,7 +406,7 @@ test('cookies signature', (t) => {
|
|
|
406
406
|
const cookies = res.cookies
|
|
407
407
|
t.equal(cookies.length, 1)
|
|
408
408
|
t.equal(cookies[0].name, 'foo')
|
|
409
|
-
t.
|
|
409
|
+
t.same(unsign(cookies[0].value, secret1), { valid: true, renew: false, value: 'cookieVal' }) // decode using first key
|
|
410
410
|
})
|
|
411
411
|
})
|
|
412
412
|
|
|
@@ -427,7 +427,7 @@ test('cookies signature', (t) => {
|
|
|
427
427
|
method: 'GET',
|
|
428
428
|
url: '/test1',
|
|
429
429
|
headers: {
|
|
430
|
-
cookie: `foo=${
|
|
430
|
+
cookie: `foo=${sign('foo', secret)}`
|
|
431
431
|
}
|
|
432
432
|
}, (err, res) => {
|
|
433
433
|
t.error(err)
|
|
@@ -452,7 +452,7 @@ test('cookies signature', (t) => {
|
|
|
452
452
|
method: 'GET',
|
|
453
453
|
url: '/test1',
|
|
454
454
|
headers: {
|
|
455
|
-
cookie: `foo=${
|
|
455
|
+
cookie: `foo=${sign('foo', secret)}`
|
|
456
456
|
}
|
|
457
457
|
}, (err, res) => {
|
|
458
458
|
t.error(err)
|
|
@@ -477,7 +477,7 @@ test('cookies signature', (t) => {
|
|
|
477
477
|
method: 'GET',
|
|
478
478
|
url: '/test1',
|
|
479
479
|
headers: {
|
|
480
|
-
cookie: `foo=${
|
|
480
|
+
cookie: `foo=${sign('foo', secret)}`
|
|
481
481
|
}
|
|
482
482
|
}, (err, res) => {
|
|
483
483
|
t.error(err)
|
|
@@ -503,7 +503,7 @@ test('cookies signature', (t) => {
|
|
|
503
503
|
method: 'GET',
|
|
504
504
|
url: '/test1',
|
|
505
505
|
headers: {
|
|
506
|
-
cookie: `foo=${
|
|
506
|
+
cookie: `foo=${sign('foo', secret2)}`
|
|
507
507
|
}
|
|
508
508
|
}, (err, res) => {
|
|
509
509
|
t.error(err)
|
|
@@ -529,7 +529,7 @@ test('cookies signature', (t) => {
|
|
|
529
529
|
method: 'GET',
|
|
530
530
|
url: '/test1',
|
|
531
531
|
headers: {
|
|
532
|
-
cookie: `foo=${
|
|
532
|
+
cookie: `foo=${sign('foo', secret2)}`
|
|
533
533
|
}
|
|
534
534
|
}, (err, res) => {
|
|
535
535
|
t.error(err)
|
|
@@ -555,7 +555,7 @@ test('cookies signature', (t) => {
|
|
|
555
555
|
method: 'GET',
|
|
556
556
|
url: '/test1',
|
|
557
557
|
headers: {
|
|
558
|
-
cookie: `foo=${
|
|
558
|
+
cookie: `foo=${sign('foo', 'invalid-secret')}`
|
|
559
559
|
}
|
|
560
560
|
}, (err, res) => {
|
|
561
561
|
t.error(err)
|
|
@@ -581,7 +581,7 @@ test('cookies signature', (t) => {
|
|
|
581
581
|
method: 'GET',
|
|
582
582
|
url: '/test1',
|
|
583
583
|
headers: {
|
|
584
|
-
cookie: `foo=${
|
|
584
|
+
cookie: `foo=${sign('foo', 'invalid-secret')}`
|
|
585
585
|
}
|
|
586
586
|
}, (err, res) => {
|
|
587
587
|
t.error(err)
|
|
@@ -814,3 +814,36 @@ test('handle secure:auto of cookieOptions', async (t) => {
|
|
|
814
814
|
t.same(cookies2[0].secure, null)
|
|
815
815
|
t.equal(cookies2[0].path, '/')
|
|
816
816
|
})
|
|
817
|
+
|
|
818
|
+
test('should not decorate fastify, request and reply if no secret was provided', async (t) => {
|
|
819
|
+
t.plan(8)
|
|
820
|
+
const fastify = Fastify()
|
|
821
|
+
|
|
822
|
+
await fastify.register(plugin)
|
|
823
|
+
|
|
824
|
+
t.notOk(fastify.signCookie)
|
|
825
|
+
t.notOk(fastify.unsignCookie)
|
|
826
|
+
|
|
827
|
+
fastify.get('/testDecorators', (req, reply) => {
|
|
828
|
+
t.notOk(req.signCookie)
|
|
829
|
+
t.notOk(reply.signCookie)
|
|
830
|
+
t.notOk(req.unsignCookie)
|
|
831
|
+
t.notOk(reply.unsignCookie)
|
|
832
|
+
|
|
833
|
+
reply.send({
|
|
834
|
+
unsigned: req.unsignCookie(req.cookies.foo)
|
|
835
|
+
})
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
const res = await fastify.inject({
|
|
839
|
+
method: 'GET',
|
|
840
|
+
url: '/testDecorators'
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
t.equal(res.statusCode, 500)
|
|
844
|
+
t.same(JSON.parse(res.body), {
|
|
845
|
+
statusCode: 500,
|
|
846
|
+
error: 'Internal Server Error',
|
|
847
|
+
message: 'req.unsignCookie is not a function'
|
|
848
|
+
})
|
|
849
|
+
})
|
package/test/signer.test.js
CHANGED
|
@@ -2,33 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
const { test } = require('tap')
|
|
4
4
|
const sinon = require('sinon')
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const crypto = require('crypto')
|
|
6
|
+
const { Signer, sign, unsign } = require('../signer')
|
|
7
7
|
|
|
8
|
-
test('default',
|
|
9
|
-
t.plan(
|
|
8
|
+
test('default', t => {
|
|
9
|
+
t.plan(5)
|
|
10
10
|
|
|
11
11
|
const secret = 'my-secret'
|
|
12
|
-
const signer =
|
|
12
|
+
const signer = Signer(secret)
|
|
13
13
|
|
|
14
|
-
t.test('signer.sign', (t) => {
|
|
14
|
+
t.test('signer.sign should throw if there is no value provided', (t) => {
|
|
15
15
|
t.plan(1)
|
|
16
16
|
|
|
17
|
+
t.throws(() => signer.sign(undefined), 'Cookie value must be provided as a string.')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
t.test('signer.sign', (t) => {
|
|
21
|
+
t.plan(2)
|
|
22
|
+
|
|
17
23
|
const input = 'some-value'
|
|
18
24
|
const result = signer.sign(input)
|
|
19
25
|
|
|
20
|
-
t.equal(result,
|
|
26
|
+
t.equal(result, sign(input, secret))
|
|
27
|
+
t.throws(() => sign(undefined), 'Cookie value must be provided as a string.')
|
|
21
28
|
})
|
|
22
29
|
|
|
23
|
-
t.test('
|
|
30
|
+
t.test('sign', (t) => {
|
|
24
31
|
t.plan(3)
|
|
25
32
|
|
|
26
|
-
const input =
|
|
33
|
+
const input = 'some-value'
|
|
34
|
+
const result = signer.sign(input)
|
|
35
|
+
|
|
36
|
+
t.equal(result, sign(input, secret))
|
|
37
|
+
t.equal(result, sign(input, [secret]))
|
|
38
|
+
|
|
39
|
+
t.throws(() => sign(undefined), 'Cookie value must be provided as a string.')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
t.test('signer.unsign', (t) => {
|
|
43
|
+
t.plan(4)
|
|
44
|
+
|
|
45
|
+
const input = signer.sign('some-value', secret)
|
|
27
46
|
const result = signer.unsign(input)
|
|
28
47
|
|
|
29
48
|
t.equal(result.valid, true)
|
|
30
49
|
t.equal(result.renew, false)
|
|
31
50
|
t.equal(result.value, 'some-value')
|
|
51
|
+
t.throws(() => signer.unsign(undefined), 'Signed cookie string must be provided.')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
t.test('unsign', (t) => {
|
|
55
|
+
t.plan(6)
|
|
56
|
+
|
|
57
|
+
const input = sign('some-value', secret)
|
|
58
|
+
const result = unsign(input, secret)
|
|
59
|
+
|
|
60
|
+
t.equal(result.valid, true)
|
|
61
|
+
t.equal(result.renew, false)
|
|
62
|
+
t.equal(result.value, 'some-value')
|
|
63
|
+
t.same(result, unsign(input, [secret]))
|
|
64
|
+
t.throws(() => unsign(undefined), 'Secret key must be a string.')
|
|
65
|
+
t.throws(() => unsign(undefined, secret), 'Signed cookie string must be provided.')
|
|
32
66
|
})
|
|
33
67
|
})
|
|
34
68
|
|
|
@@ -37,11 +71,11 @@ test('key rotation', (t) => {
|
|
|
37
71
|
const secret1 = 'my-secret-1'
|
|
38
72
|
const secret2 = 'my-secret-2'
|
|
39
73
|
const secret3 = 'my-secret-3'
|
|
40
|
-
const signer =
|
|
41
|
-
const
|
|
74
|
+
const signer = Signer([secret1, secret2, secret3])
|
|
75
|
+
const signSpy = sinon.spy(crypto, 'createHmac')
|
|
42
76
|
|
|
43
77
|
t.beforeEach(() => {
|
|
44
|
-
|
|
78
|
+
signSpy.resetHistory()
|
|
45
79
|
})
|
|
46
80
|
|
|
47
81
|
t.test('signer.sign always signs using first key', (t) => {
|
|
@@ -50,30 +84,51 @@ test('key rotation', (t) => {
|
|
|
50
84
|
const input = 'some-value'
|
|
51
85
|
const result = signer.sign(input)
|
|
52
86
|
|
|
53
|
-
t.equal(result,
|
|
87
|
+
t.equal(result, sign(input, secret1))
|
|
54
88
|
})
|
|
55
89
|
|
|
56
90
|
t.test('signer.unsign tries to decode using all keys till it finds', (t) => {
|
|
57
91
|
t.plan(4)
|
|
58
92
|
|
|
59
|
-
const input =
|
|
93
|
+
const input = sign('some-value', secret2)
|
|
94
|
+
signSpy.resetHistory()
|
|
60
95
|
const result = signer.unsign(input)
|
|
61
96
|
|
|
62
97
|
t.equal(result.valid, true)
|
|
63
98
|
t.equal(result.renew, true)
|
|
64
99
|
t.equal(result.value, 'some-value')
|
|
65
|
-
t.equal(
|
|
100
|
+
t.equal(signSpy.callCount, 2) // should have returned early when the right key was found
|
|
66
101
|
})
|
|
67
102
|
|
|
68
103
|
t.test('signer.unsign failure response', (t) => {
|
|
69
104
|
t.plan(4)
|
|
70
105
|
|
|
71
|
-
const input =
|
|
106
|
+
const input = sign('some-value', 'invalid-secret')
|
|
107
|
+
signSpy.resetHistory()
|
|
72
108
|
const result = signer.unsign(input)
|
|
73
109
|
|
|
74
110
|
t.equal(result.valid, false)
|
|
75
111
|
t.equal(result.renew, false)
|
|
76
112
|
t.equal(result.value, null)
|
|
77
|
-
t.equal(
|
|
113
|
+
t.equal(signSpy.callCount, 3) // should have tried all 3
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('Signer', t => {
|
|
118
|
+
t.plan(2)
|
|
119
|
+
|
|
120
|
+
t.test('Signer needs a string as secret', (t) => {
|
|
121
|
+
t.plan(4)
|
|
122
|
+
t.throws(() => Signer(1), 'Secret key must be a string.')
|
|
123
|
+
t.throws(() => Signer(undefined), 'Secret key must be a string.')
|
|
124
|
+
t.doesNotThrow(() => Signer('secret'))
|
|
125
|
+
t.doesNotThrow(() => Signer(['secret']))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
t.test('Signer handles algorithm properly', (t) => {
|
|
129
|
+
t.plan(3)
|
|
130
|
+
t.throws(() => Signer('secret', 'invalid'), 'Algorithm invalid not supported.')
|
|
131
|
+
t.doesNotThrow(() => Signer('secret', 'sha512'))
|
|
132
|
+
t.doesNotThrow(() => Signer('secret', 'sha256'))
|
|
78
133
|
})
|
|
79
134
|
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/// <reference types='node' />
|
|
2
|
+
|
|
3
|
+
import { FastifyPluginCallback } from "fastify";
|
|
4
|
+
|
|
5
|
+
declare module "fastify" {
|
|
6
|
+
interface FastifyInstance extends SignerMethods {
|
|
7
|
+
/**
|
|
8
|
+
* Manual cookie parsing method
|
|
9
|
+
* @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
|
|
10
|
+
* @param cookieHeader Raw cookie header value
|
|
11
|
+
*/
|
|
12
|
+
parseCookie(cookieHeader: string): {
|
|
13
|
+
[key: string]: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FastifyRequest extends SignerMethods {
|
|
18
|
+
/**
|
|
19
|
+
* Request cookies
|
|
20
|
+
*/
|
|
21
|
+
cookies: { [cookieName: string]: string | undefined };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FastifyReply extends SignerMethods {
|
|
25
|
+
/**
|
|
26
|
+
* Request cookies
|
|
27
|
+
*/
|
|
28
|
+
cookies: { [cookieName: string]: string | undefined };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SignerMethods {
|
|
32
|
+
/**
|
|
33
|
+
* Signs the specified cookie using the secret/signer provided.
|
|
34
|
+
* @param value cookie value
|
|
35
|
+
*/
|
|
36
|
+
signCookie(value: string): string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unsigns the specified cookie using the secret/signer provided.
|
|
40
|
+
* @param value Cookie value
|
|
41
|
+
*/
|
|
42
|
+
unsignCookie(value: string): fastifyCookie.UnsignResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type setCookieWrapper = (
|
|
46
|
+
name: string,
|
|
47
|
+
value: string,
|
|
48
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
49
|
+
) => FastifyReply;
|
|
50
|
+
|
|
51
|
+
interface FastifyReply {
|
|
52
|
+
/**
|
|
53
|
+
* Set response cookie
|
|
54
|
+
* @name setCookie
|
|
55
|
+
* @param name Cookie name
|
|
56
|
+
* @param value Cookie value
|
|
57
|
+
* @param options Serialize options
|
|
58
|
+
*/
|
|
59
|
+
setCookie(
|
|
60
|
+
name: string,
|
|
61
|
+
value: string,
|
|
62
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
63
|
+
): this;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @alias setCookie
|
|
67
|
+
*/
|
|
68
|
+
cookie(
|
|
69
|
+
name: string,
|
|
70
|
+
value: string,
|
|
71
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
72
|
+
): this;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* clear response cookie
|
|
76
|
+
* @param name Cookie name
|
|
77
|
+
* @param options Serialize options
|
|
78
|
+
*/
|
|
79
|
+
clearCookie(
|
|
80
|
+
name: string,
|
|
81
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
82
|
+
): this;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unsigns the specified cookie using the secret provided.
|
|
86
|
+
* @param value Cookie value
|
|
87
|
+
*/
|
|
88
|
+
unsignCookie(value: string): fastifyCookie.UnsignResult;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type FastifyCookiePlugin = FastifyPluginCallback<
|
|
93
|
+
NonNullable<fastifyCookie.FastifyCookieOptions>
|
|
94
|
+
>;
|
|
95
|
+
|
|
96
|
+
declare namespace fastifyCookie {
|
|
97
|
+
interface SignerBase {
|
|
98
|
+
sign: (value: string) => string;
|
|
99
|
+
unsign: (input: string) => UnsignResult;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class Signer implements SignerBase {
|
|
103
|
+
constructor (secrets: string | Array<string>, algorithm?: string)
|
|
104
|
+
sign: (value: string) => string;
|
|
105
|
+
unsign: (input: string) => UnsignResult;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CookieSerializeOptions {
|
|
109
|
+
/** The `Domain` attribute. */
|
|
110
|
+
domain?: string;
|
|
111
|
+
encode?(val: string): string;
|
|
112
|
+
/** The expiration `date` used for the `Expires` attribute. If both `expires` and `maxAge` are set, then `expires` is used. */
|
|
113
|
+
expires?: Date;
|
|
114
|
+
/** The `boolean` value of the `HttpOnly` attribute. Defaults to true. */
|
|
115
|
+
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. */
|
|
117
|
+
maxAge?: number;
|
|
118
|
+
/** The `Path` attribute. Defaults to `/` (the root path). */
|
|
119
|
+
path?: string;
|
|
120
|
+
priority?: "low" | "medium" | "high";
|
|
121
|
+
/** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `node` or `strict`. */
|
|
122
|
+
sameSite?: 'lax' | 'none' | 'strict' | boolean;
|
|
123
|
+
/** 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. */
|
|
124
|
+
secure?: boolean | 'auto';
|
|
125
|
+
signed?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FastifyCookieOptions {
|
|
129
|
+
secret?: string | string[] | Signer;
|
|
130
|
+
parseOptions?: fastifyCookie.CookieSerializeOptions;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type Sign = (value: string, secret: string, algorithm?: string) => string;
|
|
134
|
+
export type Unsign = (input: string, secret: string, algorithm?: string) => UnsignResult;
|
|
135
|
+
export type SignerFactory = (secrets: string | Array<string>, algorithm?: string) => SignerBase;
|
|
136
|
+
|
|
137
|
+
export interface UnsignResult {
|
|
138
|
+
valid: boolean;
|
|
139
|
+
renew: boolean;
|
|
140
|
+
value: string | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const signerFactory: SignerFactory;
|
|
144
|
+
export const sign: Sign;
|
|
145
|
+
export const unsign: Unsign;
|
|
146
|
+
|
|
147
|
+
export interface FastifyCookie extends FastifyCookiePlugin {
|
|
148
|
+
signerFactory: SignerFactory;
|
|
149
|
+
Signer: Signer;
|
|
150
|
+
sign: Sign;
|
|
151
|
+
unsign: Unsign;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const fastifyCookie: FastifyCookie;
|
|
155
|
+
|
|
156
|
+
export interface FastifyCookieOptions {
|
|
157
|
+
secret?: string | string[] | SignerBase;
|
|
158
|
+
algorithm?: string;
|
|
159
|
+
parseOptions?: CookieSerializeOptions;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { fastifyCookie as default };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
declare function fastifyCookie(
|
|
166
|
+
...params: Parameters<FastifyCookiePlugin>
|
|
167
|
+
): ReturnType<FastifyCookiePlugin>;
|
|
168
|
+
|
|
169
|
+
export = fastifyCookie;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import cookie from '
|
|
1
|
+
import cookie from '..';
|
|
2
2
|
import { expectType } from 'tsd';
|
|
3
3
|
import * as fastifyCookieStar from '..';
|
|
4
4
|
import fastifyCookieCjsImport = require('..');
|
|
@@ -58,6 +58,16 @@ server.after((_err) => {
|
|
|
58
58
|
.send({ hello: 'world' })
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
expectType<(value: string) => string>(server.signCookie)
|
|
63
|
+
expectType<(value: string) => fastifyCookieStar.UnsignResult>(server.unsignCookie)
|
|
64
|
+
|
|
65
|
+
server.get('/', (request, reply) => {
|
|
66
|
+
expectType<(value: string) => string>(request.signCookie)
|
|
67
|
+
expectType<(value: string) => string>(reply.signCookie)
|
|
68
|
+
expectType<(value: string) => fastifyCookieStar.UnsignResult>(request.unsignCookie)
|
|
69
|
+
expectType<(value: string) => fastifyCookieStar.UnsignResult>(reply.unsignCookie)
|
|
70
|
+
});
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
const serverWithHttp2 = fastify({ http2: true });
|
|
@@ -109,6 +119,10 @@ const appWithImplicitHttpSigned = fastify();
|
|
|
109
119
|
appWithImplicitHttpSigned.register(cookie, {
|
|
110
120
|
secret: 'testsecret',
|
|
111
121
|
});
|
|
122
|
+
appWithImplicitHttpSigned.register(cookie, {
|
|
123
|
+
secret: 'testsecret',
|
|
124
|
+
algorithm: 'sha512'
|
|
125
|
+
});
|
|
112
126
|
appWithImplicitHttpSigned.after(() => {
|
|
113
127
|
server.get('/', (request, reply) => {
|
|
114
128
|
appWithImplicitHttpSigned.unsignCookie(request.cookies.test!);
|
|
@@ -194,3 +208,9 @@ appWithCustomSigner.after(() => {
|
|
|
194
208
|
reply.send({ hello: 'world' })
|
|
195
209
|
})
|
|
196
210
|
})
|
|
211
|
+
|
|
212
|
+
new fastifyCookieStar.Signer('secretString')
|
|
213
|
+
new fastifyCookieStar.Signer(['secretStringInArray'])
|
|
214
|
+
const signer = new fastifyCookieStar.Signer(['secretStringInArray'], 'sha256')
|
|
215
|
+
signer.sign('Lorem Ipsum')
|
|
216
|
+
signer.unsign('Lorem Ipsum')
|
package/plugin.d.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/// <reference types='node' />
|
|
2
|
-
|
|
3
|
-
import { FastifyPluginCallback } from 'fastify';
|
|
4
|
-
|
|
5
|
-
declare module 'fastify' {
|
|
6
|
-
interface FastifyInstance {
|
|
7
|
-
/**
|
|
8
|
-
* Unsigns the specified cookie using the secret provided.
|
|
9
|
-
* @param value Cookie value
|
|
10
|
-
*/
|
|
11
|
-
unsignCookie(value: string): {
|
|
12
|
-
valid: boolean;
|
|
13
|
-
renew: boolean;
|
|
14
|
-
value: string | null;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Manual cookie parsing method
|
|
18
|
-
* @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
|
|
19
|
-
* @param cookieHeader Raw cookie header value
|
|
20
|
-
*/
|
|
21
|
-
parseCookie(cookieHeader: string): {
|
|
22
|
-
[key: string]: string;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Manual cookie signing method
|
|
26
|
-
* @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
|
|
27
|
-
* @param value cookie value
|
|
28
|
-
*/
|
|
29
|
-
signCookie(value: string): string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface FastifyRequest {
|
|
33
|
-
/**
|
|
34
|
-
* Request cookies
|
|
35
|
-
*/
|
|
36
|
-
cookies: { [cookieName: string]: string | undefined };
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Unsigns the specified cookie using the secret provided.
|
|
40
|
-
* @param value Cookie value
|
|
41
|
-
*/
|
|
42
|
-
unsignCookie(value: string): {
|
|
43
|
-
valid: boolean;
|
|
44
|
-
renew: boolean;
|
|
45
|
-
value: string | null;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type setCookieWrapper = (
|
|
50
|
-
name: string,
|
|
51
|
-
value: string,
|
|
52
|
-
options?: CookieSerializeOptions
|
|
53
|
-
) => FastifyReply;
|
|
54
|
-
|
|
55
|
-
interface FastifyReply {
|
|
56
|
-
/**
|
|
57
|
-
* Set response cookie
|
|
58
|
-
* @name setCookie
|
|
59
|
-
* @param name Cookie name
|
|
60
|
-
* @param value Cookie value
|
|
61
|
-
* @param options Serialize options
|
|
62
|
-
*/
|
|
63
|
-
setCookie(
|
|
64
|
-
name: string,
|
|
65
|
-
value: string,
|
|
66
|
-
options?: CookieSerializeOptions
|
|
67
|
-
): this;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @alias setCookie
|
|
71
|
-
*/
|
|
72
|
-
cookie(name: string, value: string, options?: CookieSerializeOptions): this;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* clear response cookie
|
|
76
|
-
* @param name Cookie name
|
|
77
|
-
* @param options Serialize options
|
|
78
|
-
*/
|
|
79
|
-
clearCookie(name: string, options?: CookieSerializeOptions): this;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Unsigns the specified cookie using the secret provided.
|
|
83
|
-
* @param value Cookie value
|
|
84
|
-
*/
|
|
85
|
-
unsignCookie(value: string): {
|
|
86
|
-
valid: boolean;
|
|
87
|
-
renew: boolean;
|
|
88
|
-
value: string | null;
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface CookieSerializeOptions {
|
|
94
|
-
domain?: string;
|
|
95
|
-
encode?(val: string): string;
|
|
96
|
-
expires?: Date;
|
|
97
|
-
httpOnly?: boolean;
|
|
98
|
-
maxAge?: number;
|
|
99
|
-
path?: string;
|
|
100
|
-
priority?: 'low' | 'medium' | 'high';
|
|
101
|
-
sameSite?: boolean | 'lax' | 'strict' | 'none';
|
|
102
|
-
secure?: boolean | 'auto';
|
|
103
|
-
signed?: boolean;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface Signer {
|
|
107
|
-
sign: (input: string) => string;
|
|
108
|
-
unsign: (input: string) => {
|
|
109
|
-
valid: boolean;
|
|
110
|
-
renew: boolean;
|
|
111
|
-
value: string | null;
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface FastifyCookieOptions {
|
|
116
|
-
secret?: string | string[] | Signer;
|
|
117
|
-
parseOptions?: CookieSerializeOptions;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export type Sign = (value: string, secret: string) => string;
|
|
121
|
-
export type Unsign = (input: string, secret: string) => string | false;
|
|
122
|
-
export type SignerFactory = (secret: string) => Signer;
|
|
123
|
-
|
|
124
|
-
export interface FastifyCookie extends FastifyPluginCallback<NonNullable<FastifyCookieOptions>>{
|
|
125
|
-
signerFactory: SignerFactory;
|
|
126
|
-
sign: Sign;
|
|
127
|
-
unsign: Unsign;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
declare const fastifyCookie: FastifyCookie;
|
|
131
|
-
|
|
132
|
-
export default fastifyCookie;
|
|
133
|
-
export { fastifyCookie };
|