@react-foundry/fastify-consent-cookies 0.1.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/LICENSE +22 -0
- package/README.md +176 -0
- package/dist/common.d.ts +25 -0
- package/dist/common.js +2 -0
- package/dist/common.mjs +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +128 -0
- package/dist/index.mjs +122 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2019-2025 Crown Copyright
|
|
4
|
+
Copyright (C) 2019-2026 Daniel A.C. Martin
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
11
|
+
so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
React Foundry - Fastify Consent-Cookies
|
|
2
|
+
=======================================
|
|
3
|
+
|
|
4
|
+
Fastify plugin to parse and set cookies only with user consent. This
|
|
5
|
+
aids compliance with European regulations.
|
|
6
|
+
|
|
7
|
+
Also provides a session via a cookie. (Accessible by reading and writing
|
|
8
|
+
to `req.session`.)
|
|
9
|
+
|
|
10
|
+
Features
|
|
11
|
+
--------
|
|
12
|
+
|
|
13
|
+
- Essential cookies
|
|
14
|
+
- Optional cookies with opt-in system
|
|
15
|
+
- Sessions
|
|
16
|
+
- Encryption (except when `httpOnly` is set to false)
|
|
17
|
+
- Secure by default
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Using this package
|
|
21
|
+
------------------
|
|
22
|
+
|
|
23
|
+
First install the package into your project:
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
npm install -S @react-foundry/fastify-consent-cookies
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then use it in your code as follows:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import Fastify from 'fastify';
|
|
33
|
+
import { type Cookie, type FastifyConsentCookiesOptions, fastifyConsentCookies } from '@react-foundry/fastify-consent-cookies';
|
|
34
|
+
|
|
35
|
+
const myCookies = [
|
|
36
|
+
{
|
|
37
|
+
name: 'ga',
|
|
38
|
+
description: 'Enables us to track you use of the site and helps us to optimise your experience.',
|
|
39
|
+
group: 'Analytics',
|
|
40
|
+
httpOnly: false
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'seen,
|
|
44
|
+
description: 'Allows us to know whether you have visited the site before.',
|
|
45
|
+
group: 'Analytics'
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const httpd = Fastify();
|
|
50
|
+
|
|
51
|
+
httpd.register(fastifyConsentCookies, {
|
|
52
|
+
cookies: myCookies,
|
|
53
|
+
secret: 'changeme' // Change this to a secret string that is shared across instances of your application
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
httpd.get('/', async (req, reply) => {
|
|
57
|
+
const seen = req.cookies['seen'] || false;
|
|
58
|
+
const message = (
|
|
59
|
+
seen
|
|
60
|
+
? 'Hello!'
|
|
61
|
+
: 'Welcome back.'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
reply.setCookie('seen', true);
|
|
65
|
+
|
|
66
|
+
return message;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await httpd.listen({
|
|
70
|
+
host: '::'
|
|
71
|
+
port: 8080
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Route handlers and subsequent hooks will be able to discover the cookies
|
|
76
|
+
available for use via `req.cookiesMeta` as well as the current cookies of
|
|
77
|
+
enabled cookies via `req.cookies`. New cookies can be set with
|
|
78
|
+
`reply.setCookie()`.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### `fastifyConsentCookies`
|
|
82
|
+
|
|
83
|
+
A Fastify plugin which can be 'registered' with the following options.
|
|
84
|
+
|
|
85
|
+
Options object:
|
|
86
|
+
|
|
87
|
+
- **`cookies: object`**
|
|
88
|
+
A description of all the cookies your application uses. Only cookies
|
|
89
|
+
described here will be available for reading and writing.
|
|
90
|
+
- **`name: string`**
|
|
91
|
+
The name of the cookie.
|
|
92
|
+
- **`description: string`**
|
|
93
|
+
A description of the cookies purpose. (To be shown to the user.)
|
|
94
|
+
- **`group?: string`**
|
|
95
|
+
The category that an optional cookie belongs to. e.g. 'Analytics'. If
|
|
96
|
+
the cookie is mandatory, do not define this and the cookie will
|
|
97
|
+
always be available.
|
|
98
|
+
- **`httpOnly?: boolean = true`**
|
|
99
|
+
Whether the cookie can be accessed on the client. Unlike other cookie
|
|
100
|
+
options, this _must_ be defined here and cannot be provided later,
|
|
101
|
+
when setting the cookie. This is because consent-cookies will encrypt
|
|
102
|
+
all httpOnly cookies as a security precaution and so needs to know
|
|
103
|
+
whether to decrypt them when reading them.
|
|
104
|
+
- **Other `cookie.serialize()` options besides `encode`**
|
|
105
|
+
See: https://www.npmjs.com/package/cookie
|
|
106
|
+
- **`secret: string`**
|
|
107
|
+
A secret that is used to encrypt your cookies. You should ensure that
|
|
108
|
+
you set this in production to ensure that they cannot be decrypted by
|
|
109
|
+
an attacker. If you horizontally scale your application, you must
|
|
110
|
+
ensure that the secret is shared between each instance, so that they
|
|
111
|
+
can decrypt cookies that were set by other instances.
|
|
112
|
+
- **`cookie.serialize()` options besides `encode` and `httpOnly`**
|
|
113
|
+
These will become the defaults within your application. Note that we
|
|
114
|
+
set some sensible defaults for your with a 'secure by default' mindset,
|
|
115
|
+
so you should only need to provide these options in order to loosen up
|
|
116
|
+
the security.
|
|
117
|
+
See: https://www.npmjs.com/package/cookie
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### `reply.setCookie(name: string, value: any[, options: object])`
|
|
121
|
+
|
|
122
|
+
Sets a cookie.
|
|
123
|
+
|
|
124
|
+
- **`name: string`**
|
|
125
|
+
The name of the cookie you wish to set.
|
|
126
|
+
**Note:** If this cookie has not been consented to and is not
|
|
127
|
+
mandatory, an exception will be thrown.
|
|
128
|
+
- **`value: any`**
|
|
129
|
+
The value you wish to set for the cookie. This can be anything that
|
|
130
|
+
`JSON.stringify` can serialise but must fit within the size limits of
|
|
131
|
+
cookies.
|
|
132
|
+
- **`options: object`**
|
|
133
|
+
Any `cookie.serialize()` option besides `encode` and `httpOnly`.
|
|
134
|
+
(To set `httpOnly` you must declare it when initially describing the
|
|
135
|
+
cookie.)
|
|
136
|
+
See: https://www.npmjs.com/package/cookie
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
### `req.cookies`
|
|
140
|
+
|
|
141
|
+
An object containing the data from all active cookies.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
### `req.cookiesMeta`
|
|
145
|
+
|
|
146
|
+
An object that contains a description of all supported cookies and
|
|
147
|
+
whether the user has consented to them.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
Working on this package
|
|
151
|
+
-----------------------
|
|
152
|
+
|
|
153
|
+
Before working on this package you must install its dependencies using
|
|
154
|
+
the following command:
|
|
155
|
+
|
|
156
|
+
```shell
|
|
157
|
+
pnpm install
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
### Building
|
|
162
|
+
|
|
163
|
+
Build the package by compiling the source code.
|
|
164
|
+
|
|
165
|
+
```shell
|
|
166
|
+
npm run build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
### Clean-up
|
|
171
|
+
|
|
172
|
+
Remove any previously built files.
|
|
173
|
+
|
|
174
|
+
```shell
|
|
175
|
+
npm run clean
|
|
176
|
+
```
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SerializeOptions } from 'cookie';
|
|
2
|
+
import type { FastifyInstance, FastifyRequest as _Request, FastifyReply as _Reply } from 'fastify';
|
|
3
|
+
import type { Promised } from '@react-foundry/types-helpers';
|
|
4
|
+
export type CookieOptions = Omit<SerializeOptions, 'encode'>;
|
|
5
|
+
export type SetCookie = (this: ReplyFull, name: string, value: any, options?: Omit<CookieOptions, 'httpOnly'>) => void;
|
|
6
|
+
export type SetCookieConsent = (this: ReplyFull, value: string[]) => void;
|
|
7
|
+
export type RequestExtras = {
|
|
8
|
+
cookies: Record<string, Cookie>;
|
|
9
|
+
cookiesMeta: object;
|
|
10
|
+
};
|
|
11
|
+
export type Request = _Request & Partial<RequestExtras>;
|
|
12
|
+
export type RequestFull = Request & RequestExtras;
|
|
13
|
+
type ReplyExtras = {
|
|
14
|
+
setCookie: SetCookie;
|
|
15
|
+
setCookieConsent: SetCookieConsent;
|
|
16
|
+
};
|
|
17
|
+
export type Reply = _Reply & Partial<ReplyExtras>;
|
|
18
|
+
export type ReplyFull = _Reply & ReplyExtras;
|
|
19
|
+
export type RouteHandlerMethod = (this: FastifyInstance, request: RequestFull, reply: ReplyFull) => Promised<unknown | void>;
|
|
20
|
+
export type Cookie = CookieOptions & {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
group?: string;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
package/dist/common.js
ADDED
package/dist/common.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from 'fastify';
|
|
2
|
+
import type { Cookie, CookieOptions } from './common';
|
|
3
|
+
export type FastifyConsentCookiePluginOptions = CookieOptions & {
|
|
4
|
+
cookies: Cookie[];
|
|
5
|
+
secret: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const defaultSecret = "changeme";
|
|
8
|
+
export declare const fastifyConsentCookies: FastifyPluginCallback<FastifyConsentCookiePluginOptions>;
|
|
9
|
+
export default fastifyConsentCookies;
|
|
10
|
+
export type { FastifyConsentCookiePluginOptions as FastifyConsentCookiesOptions };
|
|
11
|
+
export type { Cookie, CookieOptions, RouteHandlerMethod, Request, RequestFull, Reply, ReplyFull } from './common';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fastifyConsentCookies = exports.defaultSecret = void 0;
|
|
7
|
+
const cookie_1 = __importDefault(require("cookie"));
|
|
8
|
+
const cryptr_1 = __importDefault(require("cryptr"));
|
|
9
|
+
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
10
|
+
const consentCookie = {
|
|
11
|
+
name: 'consent',
|
|
12
|
+
description: 'A store of the cookies that you have consented to.',
|
|
13
|
+
httpOnly: false,
|
|
14
|
+
sameSite: 'lax'
|
|
15
|
+
};
|
|
16
|
+
const id = (v) => v;
|
|
17
|
+
const encodeClear = (v) => JSON.stringify(v);
|
|
18
|
+
const decodeClear = (v, req) => {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(v);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
const log = (req?.log || console);
|
|
24
|
+
log.warn('Unable to parse cookie data as JSON');
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const joinSet = (v, s) => Array.from(v).join(s);
|
|
29
|
+
exports.defaultSecret = 'changeme';
|
|
30
|
+
const fastifyConsentCookiesPlugin = async (fastify, { cookies: _cookies, secret = exports.defaultSecret, ...defaults }) => {
|
|
31
|
+
if (secret === exports.defaultSecret) {
|
|
32
|
+
fastify.log.warn('Cookie secret has not been set; cookies could be decrypted!');
|
|
33
|
+
}
|
|
34
|
+
const cryptr = new cryptr_1.default(secret, { encoding: 'base64' });
|
|
35
|
+
const encodeSecure = (v) => cryptr.encrypt(encodeClear(v));
|
|
36
|
+
const decodeSecure = (v, req) => {
|
|
37
|
+
try {
|
|
38
|
+
return decodeClear(cryptr.decrypt(v), req);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const log = (req || fastify).log;
|
|
42
|
+
log.warn('Unable to decrypt cookie data');
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const cookies = ([
|
|
47
|
+
consentCookie,
|
|
48
|
+
..._cookies
|
|
49
|
+
]).filter(id);
|
|
50
|
+
fastify.decorateRequest('cookiesMeta');
|
|
51
|
+
fastify.decorateRequest('cookies');
|
|
52
|
+
fastify.decorateReply('setCookie');
|
|
53
|
+
fastify.decorateReply('setCookieConsent');
|
|
54
|
+
fastify.addHook('preHandler', async (req, reply) => {
|
|
55
|
+
const cookieData = cookie_1.default.parse(req.headers.cookie || '');
|
|
56
|
+
const _consent = cookieData[consentCookie.name];
|
|
57
|
+
const consent = _consent || '';
|
|
58
|
+
const active = (cookies
|
|
59
|
+
.filter(e => e.group === undefined || consent.includes(e.name))
|
|
60
|
+
.reduce((acc, { name, ...cur }) => ({
|
|
61
|
+
...acc,
|
|
62
|
+
[name]: cur
|
|
63
|
+
}), {}));
|
|
64
|
+
const decryptedCookies = (Object.keys(cookieData)
|
|
65
|
+
.filter(e => active[e])
|
|
66
|
+
.reduce((acc, cur) => ({
|
|
67
|
+
...acc,
|
|
68
|
+
[cur]: (active[cur].httpOnly === false
|
|
69
|
+
? decodeClear(cookieData[cur])
|
|
70
|
+
: decodeSecure(cookieData[cur]))
|
|
71
|
+
}), {}));
|
|
72
|
+
const foundCookieNames = new Set(Object.keys(cookieData));
|
|
73
|
+
const activeNames = new Set(Object.keys(active));
|
|
74
|
+
const cookieNames = new Set(Object.keys(decryptedCookies));
|
|
75
|
+
const rejected = foundCookieNames.difference(cookieNames);
|
|
76
|
+
req.log.debug(`${foundCookieNames.size} cookies found on request; ${joinSet(foundCookieNames, ', ')}`);
|
|
77
|
+
req.log.debug(`${activeNames.size} cookies are active; ${joinSet(activeNames, ', ')}`);
|
|
78
|
+
req.log.debug(`${cookieNames.size} cookies are available; ${joinSet(cookieNames, ', ')}`);
|
|
79
|
+
if (rejected.size) {
|
|
80
|
+
req.log.warn(`${rejected.size} cookies were rejected; ${joinSet(rejected, ', ')}`);
|
|
81
|
+
}
|
|
82
|
+
req.cookiesMeta = cookies.map(({ name, description, group }) => ({
|
|
83
|
+
name,
|
|
84
|
+
description,
|
|
85
|
+
group,
|
|
86
|
+
consent: consent.includes(name)
|
|
87
|
+
}));
|
|
88
|
+
req.cookies = decryptedCookies;
|
|
89
|
+
const setCookie = function (name, value, options) {
|
|
90
|
+
if (active[name]) {
|
|
91
|
+
const { description, group, httpOnly: _httpOnly, ...declaration } = active[name];
|
|
92
|
+
const httpOnly = _httpOnly !== false;
|
|
93
|
+
const content = (!httpOnly
|
|
94
|
+
? encodeClear(value)
|
|
95
|
+
: encodeSecure(value));
|
|
96
|
+
const size = content.length;
|
|
97
|
+
const maxSize = 4096;
|
|
98
|
+
if (size > maxSize) {
|
|
99
|
+
const overrun = size - maxSize;
|
|
100
|
+
req.log.warn(`Attempting to set cookie, '${name}', which is ${overrun} bytes larger than allowed (4kiB) and likely to be rejected`);
|
|
101
|
+
}
|
|
102
|
+
this.header('Set-Cookie', cookie_1.default.serialize(name, content, {
|
|
103
|
+
path: '/',
|
|
104
|
+
domain: undefined,
|
|
105
|
+
sameSite: 'strict',
|
|
106
|
+
secure: true,
|
|
107
|
+
...defaults,
|
|
108
|
+
...declaration,
|
|
109
|
+
...options,
|
|
110
|
+
httpOnly
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new Error(`No consent for cookie, "${name}".`);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const setCookieConsent = function (value) {
|
|
118
|
+
this.setCookie(consentCookie.name, value);
|
|
119
|
+
};
|
|
120
|
+
reply.setCookie = setCookie;
|
|
121
|
+
reply.setCookieConsent = setCookieConsent;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
exports.fastifyConsentCookies = (0, fastify_plugin_1.default)(fastifyConsentCookiesPlugin, {
|
|
125
|
+
fastify: '5.x',
|
|
126
|
+
name: 'consent-cookies',
|
|
127
|
+
});
|
|
128
|
+
exports.default = exports.fastifyConsentCookies;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import cookie from 'cookie';
|
|
2
|
+
import Cryptr from 'cryptr';
|
|
3
|
+
import fp from 'fastify-plugin';
|
|
4
|
+
const consentCookie = {
|
|
5
|
+
name: 'consent',
|
|
6
|
+
description: 'A store of the cookies that you have consented to.',
|
|
7
|
+
httpOnly: false,
|
|
8
|
+
sameSite: 'lax'
|
|
9
|
+
};
|
|
10
|
+
const id = (v) => v;
|
|
11
|
+
const encodeClear = (v) => JSON.stringify(v);
|
|
12
|
+
const decodeClear = (v, req) => {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(v);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
const log = (req?.log || console);
|
|
18
|
+
log.warn('Unable to parse cookie data as JSON');
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const joinSet = (v, s) => Array.from(v).join(s);
|
|
23
|
+
export const defaultSecret = 'changeme';
|
|
24
|
+
const fastifyConsentCookiesPlugin = async (fastify, { cookies: _cookies, secret = defaultSecret, ...defaults }) => {
|
|
25
|
+
if (secret === defaultSecret) {
|
|
26
|
+
fastify.log.warn('Cookie secret has not been set; cookies could be decrypted!');
|
|
27
|
+
}
|
|
28
|
+
const cryptr = new Cryptr(secret, { encoding: 'base64' });
|
|
29
|
+
const encodeSecure = (v) => cryptr.encrypt(encodeClear(v));
|
|
30
|
+
const decodeSecure = (v, req) => {
|
|
31
|
+
try {
|
|
32
|
+
return decodeClear(cryptr.decrypt(v), req);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
const log = (req || fastify).log;
|
|
36
|
+
log.warn('Unable to decrypt cookie data');
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const cookies = ([
|
|
41
|
+
consentCookie,
|
|
42
|
+
..._cookies
|
|
43
|
+
]).filter(id);
|
|
44
|
+
fastify.decorateRequest('cookiesMeta');
|
|
45
|
+
fastify.decorateRequest('cookies');
|
|
46
|
+
fastify.decorateReply('setCookie');
|
|
47
|
+
fastify.decorateReply('setCookieConsent');
|
|
48
|
+
fastify.addHook('preHandler', async (req, reply) => {
|
|
49
|
+
const cookieData = cookie.parse(req.headers.cookie || '');
|
|
50
|
+
const _consent = cookieData[consentCookie.name];
|
|
51
|
+
const consent = _consent || '';
|
|
52
|
+
const active = (cookies
|
|
53
|
+
.filter(e => e.group === undefined || consent.includes(e.name))
|
|
54
|
+
.reduce((acc, { name, ...cur }) => ({
|
|
55
|
+
...acc,
|
|
56
|
+
[name]: cur
|
|
57
|
+
}), {}));
|
|
58
|
+
const decryptedCookies = (Object.keys(cookieData)
|
|
59
|
+
.filter(e => active[e])
|
|
60
|
+
.reduce((acc, cur) => ({
|
|
61
|
+
...acc,
|
|
62
|
+
[cur]: (active[cur].httpOnly === false
|
|
63
|
+
? decodeClear(cookieData[cur])
|
|
64
|
+
: decodeSecure(cookieData[cur]))
|
|
65
|
+
}), {}));
|
|
66
|
+
const foundCookieNames = new Set(Object.keys(cookieData));
|
|
67
|
+
const activeNames = new Set(Object.keys(active));
|
|
68
|
+
const cookieNames = new Set(Object.keys(decryptedCookies));
|
|
69
|
+
const rejected = foundCookieNames.difference(cookieNames);
|
|
70
|
+
req.log.debug(`${foundCookieNames.size} cookies found on request; ${joinSet(foundCookieNames, ', ')}`);
|
|
71
|
+
req.log.debug(`${activeNames.size} cookies are active; ${joinSet(activeNames, ', ')}`);
|
|
72
|
+
req.log.debug(`${cookieNames.size} cookies are available; ${joinSet(cookieNames, ', ')}`);
|
|
73
|
+
if (rejected.size) {
|
|
74
|
+
req.log.warn(`${rejected.size} cookies were rejected; ${joinSet(rejected, ', ')}`);
|
|
75
|
+
}
|
|
76
|
+
req.cookiesMeta = cookies.map(({ name, description, group }) => ({
|
|
77
|
+
name,
|
|
78
|
+
description,
|
|
79
|
+
group,
|
|
80
|
+
consent: consent.includes(name)
|
|
81
|
+
}));
|
|
82
|
+
req.cookies = decryptedCookies;
|
|
83
|
+
const setCookie = function (name, value, options) {
|
|
84
|
+
if (active[name]) {
|
|
85
|
+
const { description, group, httpOnly: _httpOnly, ...declaration } = active[name];
|
|
86
|
+
const httpOnly = _httpOnly !== false;
|
|
87
|
+
const content = (!httpOnly
|
|
88
|
+
? encodeClear(value)
|
|
89
|
+
: encodeSecure(value));
|
|
90
|
+
const size = content.length;
|
|
91
|
+
const maxSize = 4096;
|
|
92
|
+
if (size > maxSize) {
|
|
93
|
+
const overrun = size - maxSize;
|
|
94
|
+
req.log.warn(`Attempting to set cookie, '${name}', which is ${overrun} bytes larger than allowed (4kiB) and likely to be rejected`);
|
|
95
|
+
}
|
|
96
|
+
this.header('Set-Cookie', cookie.serialize(name, content, {
|
|
97
|
+
path: '/',
|
|
98
|
+
domain: undefined,
|
|
99
|
+
sameSite: 'strict',
|
|
100
|
+
secure: true,
|
|
101
|
+
...defaults,
|
|
102
|
+
...declaration,
|
|
103
|
+
...options,
|
|
104
|
+
httpOnly
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new Error(`No consent for cookie, "${name}".`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const setCookieConsent = function (value) {
|
|
112
|
+
this.setCookie(consentCookie.name, value);
|
|
113
|
+
};
|
|
114
|
+
reply.setCookie = setCookie;
|
|
115
|
+
reply.setCookieConsent = setCookieConsent;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
export const fastifyConsentCookies = fp(fastifyConsentCookiesPlugin, {
|
|
119
|
+
fastify: '5.x',
|
|
120
|
+
name: 'consent-cookies',
|
|
121
|
+
});
|
|
122
|
+
export default fastifyConsentCookies;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-foundry/fastify-consent-cookies",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fastify plugin to parse and set cookies only with user consent.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"/dist"
|
|
16
|
+
],
|
|
17
|
+
"author": "Daniel A.C. Martin <npm@daniel-martin.co.uk> (http://daniel-martin.co.uk/)",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"fastify": "5.7.1",
|
|
24
|
+
"jest": "30.2.0",
|
|
25
|
+
"jest-environment-jsdom": "30.2.0",
|
|
26
|
+
"ts-jest": "29.4.6",
|
|
27
|
+
"typescript": "5.9.3",
|
|
28
|
+
"@react-foundry/types-helpers": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"cookie": "^1.1.1",
|
|
32
|
+
"cryptr": "^6.4.0",
|
|
33
|
+
"fastify-plugin": "^5.1.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
37
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
38
|
+
"build:esm": "tsc -m es2022 && find dist -name '*.js' -exec sh -c 'mv \"$0\" \"${0%.js}.mjs\"' {} \\;",
|
|
39
|
+
"build:cjs": "tsc",
|
|
40
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo"
|
|
41
|
+
},
|
|
42
|
+
"module": "dist/index.mjs",
|
|
43
|
+
"typings": "dist/index.d.ts"
|
|
44
|
+
}
|