@easy-cookie/core 0.0.0-stage → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 easy-cookie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020-2025 Orest Bida
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,210 @@
1
- # Temporary Holding Version
1
+ # @easy-cookie/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A self-hosted, GDPR/ePrivacy-compliant cookie consent library. It covers:
4
+ - the banner and preferences panel;
5
+ - blocking scripts and embeds until consent;
6
+ - Google Consent Mode v2;
7
+ - proof of consent sent to your own server.
8
+
9
+ Everything is driven by **one config**.
10
+
11
+ It is built on a source fork of [vanilla-cookieconsent](https://github.com/orestbida/cookieconsent) v3.3.0 (MIT, © Orest Bida); see [UPSTREAM.md](UPSTREAM.md).
12
+
13
+ ## What you get by default (Italian Garante 2021 / EDPB)
14
+
15
+ - "Accept all" and "Reject all" have **identical weight**: two equal halves of one row.
16
+ - The **X closes the banner and counts as a rejection**, recorded as `close`.
17
+ - Categories are **off by default**, and scrolling never counts as consent.
18
+ - **Re-prompt** when you change `revision`, or after `expiryDays` (default 182 days ≈ 6 months).
19
+ - A **floating preferences button** stays available after the first choice. Withdrawing consent clears the category's cookies and reloads the page.
20
+ - **Proof of consent**: every choice is POSTed to your consent server (`log`). If the server is unreachable, the record is queued and retried on the next page load.
21
+
22
+ ## Install
23
+
24
+ ### 1. Script tag via your consent server (Iubenda-style)
25
+
26
+ ```html
27
+ <head>
28
+ <script src="https://consent.example.com/v1/sites/pk_live_xxx/embed.js"></script>
29
+ <!-- then GTM / gtag -->
30
+ </head>
31
+ ```
32
+
33
+ `embed.js` contains:
34
+ - the Consent Mode default for the visitor's stored state;
35
+ - the published config;
36
+ - a loader for the runtime and the stylesheet.
37
+
38
+ The config is edited in the admin console and published as revisions.
39
+
40
+ ### 2. npm
41
+
42
+ ```sh
43
+ pnpm add @easy-cookie/core
44
+ ```
45
+
46
+ ```ts
47
+ import EasyCookie, { defineConfig } from '@easy-cookie/core';
48
+ import '@easy-cookie/core/style.css';
49
+
50
+ const config = defineConfig({
51
+ revision: '2026-11-13',
52
+ policyUrl: '/cookie-policy',
53
+ language: { default: 'it', available: ['it', 'en'] },
54
+ googleConsentMode: { enabled: true },
55
+ log: { endpoint: 'https://consent.example.com', siteKey: 'pk_live_xxx' },
56
+ categories: {
57
+ necessary: { readOnly: true },
58
+ measurement: {},
59
+ marketing: {},
60
+ },
61
+ services: {
62
+ ga4: {
63
+ category: 'measurement',
64
+ label: 'Google Analytics 4',
65
+ provider: 'Google Ireland Ltd',
66
+ privacyUrl: 'https://policies.google.com/privacy',
67
+ cookies: [{ name: '^_ga', pattern: true, duration: { it: '2 anni', en: '2 years' } }],
68
+ },
69
+ },
70
+ });
71
+
72
+ EasyCookie.run(config);
73
+ ```
74
+
75
+ Put the Consent Mode head snippet **before** GTM. In plain HTML, use the `head.js` served by the consent server. For server-rendered apps, generate it yourself:
76
+
77
+ ```ts
78
+ import { renderHeadScript } from '@easy-cookie/core/shared';
79
+ const inline = renderHeadScript(config); // <script>{inline}</script> in <head>
80
+ ```
81
+
82
+ React and Next.js are covered by `@easy-cookie/react` and `@easy-cookie/next`.
83
+
84
+ ## Categories follow the services
85
+
86
+ As in Iubenda, the banner only asks about what the site actually uses. Once `services` declares at least one service, an optional category with no services is left out of the banner, the preferences, the cookie policy and Consent Mode, and its signals stay `denied`. Read-only categories (`necessary`) are always shown. A config without any `services` keeps every category, for setups that only use blocked script tags.
87
+
88
+ ## Blocking scripts and embeds
89
+
90
+ ```html
91
+ <script type="text/plain" data-category="measurement" data-service="ga4"
92
+ data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>
93
+
94
+ <iframe data-category="experience" data-service="youtube"
95
+ data-src="https://www.youtube-nocookie.com/embed/ID" width="560" height="315"
96
+ title="Video"></iframe>
97
+ ```
98
+
99
+ - **Scripts** run as soon as their category, or their service, is granted.
100
+ - **Iframes** stay empty and show a placeholder with two options: show this content once, or always enable the category.
101
+
102
+ ## Google Consent Mode v2 and GTM
103
+
104
+ Category → signal defaults:
105
+
106
+ | Category id | Consent Mode signals |
107
+ |---|---|
108
+ | `measurement` | `analytics_storage` |
109
+ | `marketing` | `ad_storage`, `ad_user_data`, `ad_personalization` |
110
+ | `functionality` | `functionality_storage` |
111
+ | `experience` | `personalization_storage` |
112
+
113
+ `security_storage` is always granted. Override the mapping per category with `gcm: [...]`.
114
+
115
+ When the visitor makes a first choice or changes it, the runtime calls `gtag('consent','update', …)` and pushes this event:
116
+
117
+ ```js
118
+ { event: 'easy_cookie_update', easyCookie: { categories: { measurement: true, … }, method, first, revision } }
119
+ ```
120
+
121
+ > **GTM setup:** a tag blocked by consent on `gtm.init` / `All Pages` is **not** fired again by a later consent update. Add a *Custom Event* trigger `easy_cookie_update` to your GA4 / Ads / Meta base tags, next to their page-view trigger.
122
+
123
+ Page loads with a stored choice do **not** push the event (the head snippet already set the right default), so tags never fire twice.
124
+
125
+ ## API
126
+
127
+ ```ts
128
+ EasyCookie.get() // ConsentState | null
129
+ EasyCookie.isGranted('measurement')
130
+ EasyCookie.isServiceGranted('ga4')
131
+ EasyCookie.on('consent' | 'change' | 'replay' | 'ready', cb) // returns an unsubscribe function
132
+ EasyCookie.onGranted('marketing', () => loadPixel())
133
+ EasyCookie.openPreferences() // e.g. from a footer link
134
+ EasyCookie.acceptAll() / rejectAll() / accept(['measurement'])
135
+ EasyCookie.identify(userId) // link the anonymous consent id to a user (after login)
136
+ EasyCookie.reset() // forget the choice (logged as withdraw) and re-prompt
137
+ ```
138
+
139
+ The same events are dispatched on `window` as `easycookie:consent`, `easycookie:change`, `easycookie:replay` and `easycookie:ready`.
140
+
141
+ No-JS footer links: any element with the `data-ec-open-preferences` attribute opens the preferences when clicked (the default action is prevented). The listener is delegated on `document`, so elements added after `run()` work too, for example the `{{preferences-link}}` of a legal document.
142
+
143
+ ```html
144
+ <a href="#" data-ec-open-preferences>Preferenze cookie</a>
145
+ ```
146
+
147
+ Server side (middleware, RSC, route handlers):
148
+
149
+ ```ts
150
+ import { consentFromCookieHeader } from '@easy-cookie/core/shared';
151
+ const consent = consentFromCookieHeader(request.headers.get('cookie'), { revision: config.revision });
152
+ if (consent?.categories.includes('measurement')) sendServerSideAnalytics();
153
+ ```
154
+
155
+ ## Service catalog
156
+
157
+ `@easy-cookie/core/catalog` ships about 30 ready-made service declarations (GA4, GTM, PostHog, Matomo, Plausible, Hotjar, Clarity, Mixpanel, Amplitude, Google Ads, Meta Pixel, LinkedIn, TikTok, Pinterest, Microsoft UET, X, YouTube, Vimeo, Google Maps, Spotify, Calendly, Intercom, Crisp, HubSpot, Zendesk, Stripe, PayPal, reCAPTCHA, hCaptcha, Cloudflare, Sentry, easy-cookie itself). Each entry has the EU-facing provider, the privacy link, the default consent category, cookies with IT/EN durations and descriptions, data transfers and a note for the owner. It is plain data: the console, the CLI and npm users share it, and nothing reads it at runtime.
158
+
159
+ ```ts
160
+ import { CATALOG, findCatalogEntry, catalogToService } from '@easy-cookie/core/catalog';
161
+
162
+ const config = defineConfig({
163
+ // …
164
+ services: {
165
+ ga4: catalogToService(findCatalogEntry('ga4')!), // { category: 'measurement', …, from: 'ga4' }
166
+ youtube: catalogToService(findCatalogEntry('youtube')!),
167
+ },
168
+ });
169
+ ```
170
+
171
+ `catalogToService` returns a deep copy with `from: entry.id`. Edit the result freely: `from` is informational. Default categories follow the Italian Garante 2021 guidelines: third-party analytics → `measurement`, advertising → `marketing`, embeds → `experience`, support chats → `functionality`, payments, security and infrastructure → `necessary`. Read each entry's `note`, and check the data against your own setup and contracts: providers change their cookies over time.
172
+
173
+ ## Theming
174
+
175
+ Every visual value is a CSS custom property. Set them on `:root`, or on `html.ec--dark` for the dark scheme (or `ui.colorScheme: 'dark' | 'auto'`).
176
+
177
+ | Variable | Default | Role |
178
+ |---|---|---|
179
+ | `--ec-font` | system UI stack | Banner font (set it to your site's font) |
180
+ | `--ec-bg` / `--ec-ink` / `--ec-ink-2` | `#fff` / `#1c1c1a` / `#4a4a46` | Ground, text, secondary text |
181
+ | `--ec-hair` / `--ec-wash` | `#d9d9d3` / `#f4f4f1` | Hairlines, hover wash |
182
+ | `--ec-border` | `1px solid var(--ec-ink)` | Banner and button border |
183
+ | `--ec-radius` | `0px` | Corner radius everywhere |
184
+ | `--ec-width` / `--ec-pad` | `560px` / `28px` | Banner width and padding |
185
+ | `--ec-text` / `--ec-title` / `--ec-small` | `15px` / `20px` / `13px` | Type sizes |
186
+ | `--ec-module` | `40px` | Size of the category modules |
187
+ | `--ec-c-necessary` … `--ec-c-marketing` | Olivetti palette | Category module colours |
188
+ | `--ec-shadow` | `none` | Banner shadow |
189
+ | `--ec-focus` | 2px ink ring | Focus indicator |
190
+
191
+ Example, for a brand with rounded corners and an amber accent:
192
+
193
+ ```css
194
+ :root {
195
+ --ec-font: "Inter", system-ui, sans-serif;
196
+ --ec-radius: 10px;
197
+ --ec-ink: #1b1530;
198
+ --ec-c-measurement: #f2a516;
199
+ }
200
+ ```
201
+
202
+ ## The consent cookie
203
+
204
+ - `ec_consent` is first-party, `SameSite=Lax`, `Secure` on HTTPS, and lasts `expiryDays`.
205
+ - Its value is JSON (engine format). easy-cookie's revision and method live in `data.ec`.
206
+ - Read it with `parseConsent` / `consentFromCookieHeader`; never parse it by hand.
207
+
208
+ ## License
209
+
210
+ MIT. The bundled engine is MIT © Orest Bida ([LICENSE.upstream](LICENSE.upstream)).
package/UPSTREAM.md ADDED
@@ -0,0 +1,18 @@
1
+ # Upstream
2
+
3
+ `src/engine/` and `test/engine/` started as a source copy of
4
+ [orestbida/cookieconsent](https://github.com/orestbida/cookieconsent) **v3.3.0**
5
+ (commit `944bb270df5be3732d5384d8d5e12c224468f1d1`), MIT License,
6
+ Copyright (c) 2020-2025 Orest Bida. The original license text is kept in
7
+ [`LICENSE.upstream`](LICENSE.upstream) and must ship with this package.
8
+
9
+ The engine is not an npm dependency: easy-cookie owns and modifies this code.
10
+ Upstream fixes are reviewed and cherry-picked by hand. When you do, record it below.
11
+
12
+ ## Local changes to the engine
13
+
14
+ - Engine tests stay on Jest (paths adjusted to `test/engine`); the GUI-less "core build" test was removed because easy-cookie ships a single build.
15
+
16
+ ## Cherry-picked upstream changes
17
+
18
+ _None yet._