@owlmeans/consent 0.1.18-rc.8 → 0.1.18
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/README.md +229 -0
- package/build/consts.d.ts +84 -23
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +110 -29
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +12 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +12 -0
- package/build/consts.local.js.map +1 -0
- package/build/geo/types.d.ts +47 -0
- package/build/geo/types.d.ts.map +1 -0
- package/build/geo/types.js +2 -0
- package/build/geo/types.js.map +1 -0
- package/build/geo.d.ts +4 -0
- package/build/geo.d.ts.map +1 -0
- package/build/geo.js +96 -0
- package/build/geo.js.map +1 -0
- package/build/gtobserver/types.d.ts +75 -0
- package/build/gtobserver/types.d.ts.map +1 -0
- package/build/gtobserver/types.js +2 -0
- package/build/gtobserver/types.js.map +1 -0
- package/build/gtobserver.d.ts +7 -0
- package/build/gtobserver.d.ts.map +1 -0
- package/build/gtobserver.js +142 -0
- package/build/gtobserver.js.map +1 -0
- package/build/i18n/be.json +9 -4
- package/build/i18n/de.json +9 -4
- package/build/i18n/en.json +9 -4
- package/build/i18n/es.json +9 -4
- package/build/i18n/fr.json +9 -4
- package/build/i18n/pl.json +9 -4
- package/build/i18n/ru.json +9 -4
- package/build/i18n/types.d.ts +11 -0
- package/build/i18n/types.d.ts.map +1 -0
- package/build/i18n/types.js +2 -0
- package/build/i18n/types.js.map +1 -0
- package/build/i18n/uk.json +9 -4
- package/build/i18n.d.ts +4 -14
- package/build/i18n.d.ts.map +1 -1
- package/build/i18n.js +12 -27
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +13 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -4
- package/build/index.js.map +1 -1
- package/build/linker/types.d.ts +59 -0
- package/build/linker/types.d.ts.map +1 -0
- package/build/linker/types.js +2 -0
- package/build/linker/types.js.map +1 -0
- package/build/linker.d.ts +5 -76
- package/build/linker.d.ts.map +1 -1
- package/build/linker.js +290 -339
- package/build/linker.js.map +1 -1
- package/build/plugins/types.d.ts +42 -0
- package/build/plugins/types.d.ts.map +1 -0
- package/build/plugins/types.js +2 -0
- package/build/plugins/types.js.map +1 -0
- package/build/plugins.d.ts +4 -69
- package/build/plugins.d.ts.map +1 -1
- package/build/plugins.js +73 -62
- package/build/plugins.js.map +1 -1
- package/build/storage/types.d.ts +31 -0
- package/build/storage/types.d.ts.map +1 -0
- package/build/storage/types.js +2 -0
- package/build/storage/types.js.map +1 -0
- package/build/storage.d.ts +6 -22
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +72 -81
- package/build/storage.js.map +1 -1
- package/build/store.d.ts +2 -2
- package/build/store.d.ts.map +1 -1
- package/build/store.js +116 -67
- package/build/store.js.map +1 -1
- package/build/types.d.ts +147 -26
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +9 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/package.json +3 -3
- package/src/consts.local.ts +16 -0
- package/src/consts.ts +126 -34
- package/src/geo/types.ts +49 -0
- package/src/geo.ts +122 -0
- package/src/gtobserver/types.ts +73 -0
- package/src/gtobserver.ts +182 -0
- package/src/i18n/be.json +9 -4
- package/src/i18n/de.json +9 -4
- package/src/i18n/en.json +9 -4
- package/src/i18n/es.json +9 -4
- package/src/i18n/fr.json +9 -4
- package/src/i18n/pl.json +9 -4
- package/src/i18n/ru.json +9 -4
- package/src/i18n/types.ts +11 -0
- package/src/i18n/uk.json +9 -4
- package/src/i18n.ts +20 -32
- package/src/index.ts +13 -13
- package/src/linker/types.ts +59 -0
- package/src/linker.ts +301 -364
- package/src/plugins/types.ts +42 -0
- package/src/plugins.ts +82 -106
- package/src/storage/types.ts +31 -0
- package/src/storage.ts +75 -77
- package/src/store.ts +122 -69
- package/src/types.local.ts +9 -0
- package/src/types.ts +153 -25
- package/tests/consent.spec.ts +26 -28
- package/tests/geo.spec.ts +474 -0
- package/tests/linker.spec.ts +144 -309
- package/tests/tracking.spec.ts +14 -14
- package/build/functional.d.ts +0 -35
- package/build/functional.d.ts.map +0 -1
- package/build/functional.js +0 -67
- package/build/functional.js.map +0 -1
- package/build/gtm.d.ts +0 -72
- package/build/gtm.d.ts.map +0 -1
- package/build/gtm.js +0 -182
- package/build/gtm.js.map +0 -1
- package/src/functional.ts +0 -73
- package/src/gtm.ts +0 -220
package/build/linker.js
CHANGED
|
@@ -1,357 +1,308 @@
|
|
|
1
|
-
import { CONSENT_COOKIE_DAYS,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const toBase64Url = (json) => btoa(json).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
|
-
const fromBase64Url = (value) => {
|
|
18
|
-
try {
|
|
19
|
-
let padded = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
20
|
-
while (padded.length % 4 !== 0) {
|
|
21
|
-
padded += '=';
|
|
1
|
+
import { CONSENT_COOKIE_DAYS, CONSENT_KEY, CONSENT_LANGUAGE_KEY, CONSENT_SCHEMA_VERSION, DEFAULT_CONSENT_CATEGORIES, CONSENT_LINK_MAX_AGE, CONSENT_LINK_PARAM, CONSENT_LINK_SKEW } from './consts.js';
|
|
2
|
+
import { consentPluginHelper } from './plugins.js';
|
|
3
|
+
import { consentStorageHelper } from './storage.js';
|
|
4
|
+
import { CONSENT_LINK_VERSION, LANGUAGE_TAG } from './consts.local.js';
|
|
5
|
+
export const createConsentLinkHelper = () => {
|
|
6
|
+
const categoriesOf = (opts) => opts?.categories ?? DEFAULT_CONSENT_CATEGORIES;
|
|
7
|
+
const optionalOf = (opts) => categoriesOf(opts).filter(category => category.required !== true);
|
|
8
|
+
const toBase64Url = (json) => btoa(json).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
9
|
+
const fromBase64Url = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
let padded = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
12
|
+
while (padded.length % 4 !== 0) {
|
|
13
|
+
padded += '=';
|
|
14
|
+
}
|
|
15
|
+
return atob(padded);
|
|
22
16
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
/** The language this document is written in — `<html lang>`, the one signal every page has. */
|
|
30
|
-
const documentLanguage = () => {
|
|
31
|
-
if (typeof document === 'undefined') {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const lang = document.documentElement?.lang?.trim() ?? '';
|
|
35
|
-
return LANGUAGE_TAG.test(lang) ? lang.toLowerCase() : null;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* The link parameter's value: the current document's decision as `c` (one `0`/`1` per optional
|
|
39
|
-
* category — empty while `record` is `null`, i.e. before the visitor has decided) and, when
|
|
40
|
-
* `opts.linker.language` is set, the page's language as `l`.
|
|
41
|
-
*/
|
|
42
|
-
export const encodeConsentLink = (record, opts) => {
|
|
43
|
-
const c = {};
|
|
44
|
-
if (record != null) {
|
|
45
|
-
for (const category of optionalOf(opts)) {
|
|
46
|
-
c[category.key] = record[category.key] === true ? 1 : 0;
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
47
19
|
}
|
|
48
|
-
}
|
|
49
|
-
const language = opts?.linker?.language != null ? documentLanguage() : null;
|
|
50
|
-
const payload = {
|
|
51
|
-
v: CONSENT_LINK_VERSION, c, t: Math.floor(Date.now() / 1000), ...(language != null ? { l: language } : {}),
|
|
52
20
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
export const decodeConsentLink = (value) => {
|
|
57
|
-
const json = fromBase64Url(value);
|
|
58
|
-
if (json == null) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const parsed = JSON.parse(json);
|
|
63
|
-
if (parsed == null || typeof parsed !== 'object') {
|
|
21
|
+
/** The language this document is written in — `<html lang>`, the one signal every page has. */
|
|
22
|
+
const documentLanguage = () => {
|
|
23
|
+
if (typeof document === 'undefined') {
|
|
64
24
|
return null;
|
|
65
25
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
26
|
+
const lang = document.documentElement?.lang?.trim() ?? '';
|
|
27
|
+
return LANGUAGE_TAG.test(lang) ? lang.toLowerCase() : null;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The visitor's OWN decision, or `null`. An automatic one (`auto`) was derived from where they
|
|
31
|
+
* browsed from and nobody chose it, so it never travels as their choice — the receiving domain
|
|
32
|
+
* locates them itself.
|
|
33
|
+
*/
|
|
34
|
+
const explicitOf = (record) => record != null && typeof record.auto !== 'number' ? record : null;
|
|
35
|
+
const encodeConsentLink = (input, opts) => {
|
|
36
|
+
const c = {};
|
|
37
|
+
const record = explicitOf(input);
|
|
38
|
+
if (record != null) {
|
|
39
|
+
for (const category of optionalOf(opts)) {
|
|
40
|
+
c[category.key] = record[category.key] === true ? 1 : 0;
|
|
41
|
+
}
|
|
69
42
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...(typeof l === 'string' && LANGUAGE_TAG.test(l) ? { l: l.toLowerCase() } : {}),
|
|
43
|
+
const language = opts?.linker?.language != null ? documentLanguage() : null;
|
|
44
|
+
const payload = {
|
|
45
|
+
v: CONSENT_LINK_VERSION, c, t: Math.floor(Date.now() / 1000), ...(language != null ? { l: language } : {}),
|
|
74
46
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* The payload of the link this document was opened from, once it has passed every check that does
|
|
93
|
-
* not depend on WHAT it carries: the parameter decodes and its version matches, its timestamp is
|
|
94
|
-
* fresh (and not from the future beyond the allowed skew), and the request's referrer host is a
|
|
95
|
-
* LISTED domain — read from `document.referrer`, never from the parameter itself. `null` otherwise.
|
|
96
|
-
*/
|
|
97
|
-
const trustedPayload = (opts) => {
|
|
98
|
-
const linker = opts.linker;
|
|
99
|
-
if (linker == null || typeof location === 'undefined') {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const raw = new URLSearchParams(location.search).get(linker.param ?? CONSENT_LINK_PARAM);
|
|
103
|
-
if (raw == null) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
const decoded = decodeConsentLink(raw);
|
|
107
|
-
if (decoded == null) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
const now = Math.floor(Date.now() / 1000);
|
|
111
|
-
const maxAge = linker.maxAge ?? CONSENT_LINK_MAX_AGE;
|
|
112
|
-
if (now - decoded.t > maxAge || decoded.t - now > CONSENT_LINK_SKEW) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
const referrer = referrerHostname();
|
|
116
|
-
if (referrer == null || !linker.domains.includes(referrer)) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
return decoded;
|
|
120
|
-
};
|
|
121
|
-
/** The entry of `supported` that `language` names — exactly, else by its base tag — or `null`. */
|
|
122
|
-
const supportedLanguage = (language, supported) => {
|
|
123
|
-
const wanted = language.toLowerCase();
|
|
124
|
-
const exact = supported.find(candidate => candidate.toLowerCase() === wanted);
|
|
125
|
-
if (exact != null) {
|
|
126
|
-
return exact;
|
|
127
|
-
}
|
|
128
|
-
const base = wanted.split(/[-_]/)[0];
|
|
129
|
-
return supported.find(candidate => candidate.toLowerCase() === base) ?? null;
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* Persist an adopted language where the application's i18n layer reads its explicit choice from —
|
|
133
|
-
* `linker.language.storageKey`, `owlmeans-lng` by default — and only while the visitor has GRANTED
|
|
134
|
-
* `functional` on this document (a stored record, or the one adopted from the very link that
|
|
135
|
-
* carried the language). Without that grant nothing is written and `false` comes back: remembering
|
|
136
|
-
* a preference on a visitor's device is storage like any other, and a refusal ("reject all") is an
|
|
137
|
-
* answer, not a gap to fill. The gate lives here, not in the callers, so no caller can forget it.
|
|
138
|
-
*
|
|
139
|
-
* It overwrites what is there on purpose: the carried language is the one the visitor was reading a
|
|
140
|
-
* moment ago, which outranks a choice they made on this domain some other day. Storage that refuses
|
|
141
|
-
* the write is not an error. Returns whether the language was written.
|
|
142
|
-
*/
|
|
143
|
-
export const writeConsentLanguage = (language, opts) => {
|
|
144
|
-
if (!functionalGranted(opts)) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
localStorage.setItem(opts?.linker?.language?.storageKey ?? CONSENT_LANGUAGE_KEY, language);
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// Private modes and blocked storage both throw; the app simply keeps choosing its own language.
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
/** Escape `<` inside a JSON literal meant for an inline `<script>` — the same discipline every
|
|
157
|
-
* caller of `consentBootstrapScript`/`consentGateScript` already owes the HTML it lands in, applied
|
|
158
|
-
* here at the source since this fragment's dynamic values (a configured domain, a storage key) are
|
|
159
|
-
* least likely to have been reviewed for it by whoever composes the final page. */
|
|
160
|
-
const jsonForScript = (value) => JSON.stringify(value).replace(/</g, '\\u003c');
|
|
161
|
-
/**
|
|
162
|
-
* Cross-domain consent: share this decision, and adopt one, between the domains named in
|
|
163
|
-
* `opts.linker.domains`.
|
|
164
|
-
*
|
|
165
|
-
* `start` decorates outgoing first-party links; `adopt` reads and validates an incoming one;
|
|
166
|
-
* `domains` discloses the list a dialog or policy page names. `decorate` is generic (a bare `URL`,
|
|
167
|
-
* no DOM) so the same trust rule serves a real anchor click and a programmatic navigation
|
|
168
|
-
* (`decorateConsentUrl`) alike — the `rel="noreferrer"` exemption is a DOM-only concept and is
|
|
169
|
-
* therefore checked by `start`'s click handler, never here.
|
|
170
|
-
*/
|
|
171
|
-
export const consentLinker = () => {
|
|
172
|
-
let installed = false;
|
|
173
|
-
return {
|
|
174
|
-
alias: 'linker',
|
|
175
|
-
start: opts => {
|
|
176
|
-
if (typeof document === 'undefined' || installed || opts.linker == null) {
|
|
177
|
-
return;
|
|
47
|
+
return toBase64Url(JSON.stringify(payload));
|
|
48
|
+
};
|
|
49
|
+
const decodeConsentLink = (value) => {
|
|
50
|
+
const json = fromBase64Url(value);
|
|
51
|
+
if (json == null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(json);
|
|
56
|
+
if (parsed == null || typeof parsed !== 'object') {
|
|
57
|
+
return null;
|
|
178
58
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
59
|
+
const { v, c, t, l } = parsed;
|
|
60
|
+
if (v !== CONSENT_LINK_VERSION || typeof t !== 'number' || c == null || typeof c !== 'object') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
v: CONSENT_LINK_VERSION, c: c, t,
|
|
65
|
+
// A malformed language is dropped, never a reason to refuse the decision beside it.
|
|
66
|
+
...(typeof l === 'string' && LANGUAGE_TAG.test(l) ? { l: l.toLowerCase() } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const referrerHostname = () => {
|
|
74
|
+
if (typeof document === 'undefined' || document.referrer === '') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return new URL(document.referrer).hostname;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The payload of the link this document was opened from, once it has passed every check that does
|
|
86
|
+
* not depend on WHAT it carries: the parameter decodes and its version matches, its timestamp is
|
|
87
|
+
* fresh (and not from the future beyond the allowed skew), and the request's referrer host is a
|
|
88
|
+
* LISTED domain — read from `document.referrer`, never from the parameter itself. `null` otherwise.
|
|
89
|
+
*/
|
|
90
|
+
const trustedPayload = (opts) => {
|
|
91
|
+
const linker = opts.linker;
|
|
92
|
+
if (linker == null || typeof location === 'undefined') {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const raw = new URLSearchParams(location.search).get(linker.param ?? CONSENT_LINK_PARAM);
|
|
96
|
+
if (raw == null) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const decoded = decodeConsentLink(raw);
|
|
100
|
+
if (decoded == null) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const now = Math.floor(Date.now() / 1000);
|
|
104
|
+
const maxAge = linker.maxAge ?? CONSENT_LINK_MAX_AGE;
|
|
105
|
+
if (now - decoded.t > maxAge || decoded.t - now > CONSENT_LINK_SKEW) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const referrer = referrerHostname();
|
|
109
|
+
if (referrer == null || !linker.domains.includes(referrer)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return decoded;
|
|
113
|
+
};
|
|
114
|
+
/** The entry of `supported` that `language` names — exactly, else by its base tag — or `null`. */
|
|
115
|
+
const supportedLanguage = (language, supported) => {
|
|
116
|
+
const wanted = language.toLowerCase();
|
|
117
|
+
const exact = supported.find(candidate => candidate.toLowerCase() === wanted);
|
|
118
|
+
if (exact != null) {
|
|
119
|
+
return exact;
|
|
120
|
+
}
|
|
121
|
+
const base = wanted.split(/[-_]/)[0];
|
|
122
|
+
return supported.find(candidate => candidate.toLowerCase() === base) ?? null;
|
|
123
|
+
};
|
|
124
|
+
const writeConsentLanguage = (language, opts) => {
|
|
125
|
+
try {
|
|
126
|
+
localStorage.setItem(opts?.linker?.language?.storageKey ?? CONSENT_LANGUAGE_KEY, language);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Private modes and blocked storage both throw; the app simply keeps choosing its own language.
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/** Escape `<` inside a JSON literal meant for an inline `<script>` — the same discipline every
|
|
135
|
+
* caller of `consentBootstrapScript`/`consentGateScript` already owes the HTML it lands in, applied
|
|
136
|
+
* here at the source since this fragment's dynamic values (a configured domain, a storage key) are
|
|
137
|
+
* least likely to have been reviewed for it by whoever composes the final page. */
|
|
138
|
+
const jsonForScript = (value) => JSON.stringify(value).replace(/</g, '\\u003c');
|
|
139
|
+
const consentLinker = () => {
|
|
140
|
+
let installed = false;
|
|
141
|
+
return {
|
|
142
|
+
alias: 'linker',
|
|
143
|
+
start: opts => {
|
|
144
|
+
if (typeof document === 'undefined' || installed || opts.linker == null) {
|
|
184
145
|
return;
|
|
185
146
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
147
|
+
installed = true;
|
|
148
|
+
const onNavigate = (event) => {
|
|
149
|
+
const target = event.target;
|
|
150
|
+
const anchor = target instanceof Element ? target.closest('a[href]') : null;
|
|
151
|
+
if (!(anchor instanceof HTMLAnchorElement)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// The trust rule keeps `noreferrer` links untouched: a link that refuses to disclose the
|
|
155
|
+
// referrer is refusing exactly the signal the receiving side's `adopt` needs to trust it.
|
|
156
|
+
if (/\bnoreferrer\b/.test(anchor.getAttribute('rel') ?? '')) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// No decision yet is still worth a decoration when a language rides along with it.
|
|
160
|
+
const record = explicitOf(consentStorageHelper.readConsent(opts));
|
|
161
|
+
if (record == null && opts.linker?.language == null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let url;
|
|
165
|
+
try {
|
|
166
|
+
url = new URL(anchor.href, location.href);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const decorated = consentPluginHelper.decorateConsentUrl(url, record, opts);
|
|
172
|
+
if (decorated.toString() !== url.toString()) {
|
|
173
|
+
anchor.href = decorated.toString();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
// Capture phase: this must decide before the click's own navigation (or a framework router
|
|
177
|
+
// intercepting it) reads `href`. `auxclick`/`contextmenu` cover a middle-click and "copy link
|
|
178
|
+
// address" — an address copied undecorated is a link that silently fails to carry the choice.
|
|
179
|
+
document.addEventListener('click', onNavigate, true);
|
|
180
|
+
document.addEventListener('auxclick', onNavigate, true);
|
|
181
|
+
document.addEventListener('contextmenu', onNavigate, true);
|
|
182
|
+
},
|
|
183
|
+
decorate: (url, input, opts) => {
|
|
184
|
+
const record = explicitOf(input);
|
|
185
|
+
const linker = opts.linker;
|
|
186
|
+
const host = typeof location !== 'undefined' ? location.hostname : '';
|
|
187
|
+
if (linker == null || url.hostname === host || !linker.domains.includes(url.hostname)) {
|
|
188
|
+
return null;
|
|
190
189
|
}
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return;
|
|
190
|
+
// A link carries a decision, a language, or both; with neither there is nothing to say.
|
|
191
|
+
if (record == null && (linker.language == null || documentLanguage() == null)) {
|
|
192
|
+
return null;
|
|
195
193
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
const next = new URL(url.toString());
|
|
195
|
+
next.searchParams.set(linker.param ?? CONSENT_LINK_PARAM, encodeConsentLink(record, opts));
|
|
196
|
+
return next;
|
|
197
|
+
},
|
|
198
|
+
adopt: opts => {
|
|
199
|
+
const decoded = trustedPayload(opts);
|
|
200
|
+
if (decoded == null) {
|
|
201
|
+
return null;
|
|
199
202
|
}
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
const optional = optionalOf(opts);
|
|
204
|
+
if (!optional.every(category => decoded.c[category.key] === 0 || decoded.c[category.key] === 1)) {
|
|
205
|
+
return null;
|
|
202
206
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
const record = { v: CONSENT_SCHEMA_VERSION };
|
|
208
|
+
for (const category of categoriesOf(opts)) {
|
|
209
|
+
record[category.key] = category.required === true || decoded.c[category.key] === 1;
|
|
206
210
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
211
|
+
return record;
|
|
212
|
+
},
|
|
213
|
+
adoptLanguage: opts => {
|
|
214
|
+
const supported = opts.linker?.language?.supported;
|
|
215
|
+
const decoded = supported != null ? trustedPayload(opts) : null;
|
|
216
|
+
return decoded?.l != null && supported != null ? supportedLanguage(decoded.l, supported) : null;
|
|
217
|
+
},
|
|
218
|
+
domains: opts => opts.linker?.domains ?? [],
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const stripConsentLinkParam = (opts) => {
|
|
222
|
+
const linker = opts.linker;
|
|
223
|
+
if (linker == null || typeof location === 'undefined' || typeof history === 'undefined') {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const param = linker.param ?? CONSENT_LINK_PARAM;
|
|
227
|
+
const url = new URL(location.href);
|
|
228
|
+
if (!url.searchParams.has(param)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
url.searchParams.delete(param);
|
|
232
|
+
try {
|
|
233
|
+
history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// A sandboxed/embedded context may refuse history writes; the parameter just stays visible.
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const consentLinkerScript = (opts) => {
|
|
240
|
+
const linker = opts.linker;
|
|
241
|
+
if (linker == null) {
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
const param = jsonForScript(linker.param ?? CONSENT_LINK_PARAM);
|
|
245
|
+
const domains = jsonForScript(linker.domains);
|
|
246
|
+
const storageKey = jsonForScript(opts.storageKey ?? CONSENT_KEY);
|
|
247
|
+
const optionalKeys = jsonForScript(optionalOf(opts).map(category => category.key));
|
|
248
|
+
const allCategories = jsonForScript(categoriesOf(opts).map(category => ({ key: category.key, required: category.required === true })));
|
|
249
|
+
const maxAge = linker.maxAge ?? CONSENT_LINK_MAX_AGE;
|
|
250
|
+
const skew = CONSENT_LINK_SKEW;
|
|
251
|
+
const cookieDays = opts.cookieDays ?? CONSENT_COOKIE_DAYS;
|
|
252
|
+
const version = CONSENT_LINK_VERSION;
|
|
253
|
+
// The language rides the same trust decision and is written unconditionally — it is strictly
|
|
254
|
+
// necessary storage, whatever the cookie decision is. Mirrors `supportedLanguage` +
|
|
255
|
+
// `writeConsentLanguage`; empty for a page that does not receive a language.
|
|
256
|
+
const language = linker.language?.supported != null
|
|
257
|
+
? `var L=${jsonForScript(linker.language.supported)};` +
|
|
258
|
+
`if(typeof j.l==='string'){var lc=j.l.toLowerCase(),lm=null,q;` +
|
|
259
|
+
`for(q=0;q<L.length;q++){if(String(L[q]).toLowerCase()===lc){lm=L[q];break}}` +
|
|
260
|
+
`if(!lm){lc=lc.split(/[-_]/)[0];for(q=0;q<L.length;q++){if(String(L[q]).toLowerCase()===lc){lm=L[q];break}}}` +
|
|
261
|
+
`if(lm){try{w.localStorage.setItem(${jsonForScript(linker.language.storageKey ?? CONSENT_LANGUAGE_KEY)},lm)}catch(e){}}}`
|
|
262
|
+
: '';
|
|
263
|
+
return `(function(w,d){` +
|
|
264
|
+
`var p=${param};var qs;try{qs=new URLSearchParams(location.search)}catch(e){return}` +
|
|
265
|
+
`var raw=qs.get(p);if(raw==null)return;` +
|
|
266
|
+
`qs.delete(p);var rest=qs.toString();` +
|
|
267
|
+
`var url=location.pathname+(rest?('?'+rest):'')+location.hash;` +
|
|
268
|
+
`try{history.replaceState(history.state,'',url)}catch(e){}` +
|
|
269
|
+
`try{` +
|
|
270
|
+
`var b=raw.replace(/-/g,'+').replace(/_/g,'/');while(b.length%4)b+='=';` +
|
|
271
|
+
`var j=JSON.parse(atob(b));` +
|
|
272
|
+
`if(!j||j.v!==${version}||typeof j.t!=='number'||!j.c||typeof j.c!=='object')return;` +
|
|
273
|
+
`var now=Math.floor(Date.now()/1000);` +
|
|
274
|
+
`if(now-j.t>${maxAge}||j.t-now>${skew})return;` +
|
|
275
|
+
`var ref=null;try{ref=new URL(d.referrer).hostname}catch(e){}` +
|
|
276
|
+
`if(!ref||${domains}.indexOf(ref)<0)return;` +
|
|
277
|
+
`var existing=null;try{existing=w.localStorage.getItem(${storageKey})}catch(e){}` +
|
|
278
|
+
`if(!existing){var cp=('; '+d.cookie).split('; '+${storageKey}+'=');` +
|
|
279
|
+
`if(cp.length===2)existing=cp.pop().split(';').shift()}` +
|
|
280
|
+
// An automatic decision is replaced by the visitor's own; an unparseable one still blocks.
|
|
281
|
+
`var ex=null;if(existing){try{ex=JSON.parse(existing)}catch(e){ex=false}}` +
|
|
282
|
+
`if(!existing||(ex&&typeof ex.auto==='number')){` +
|
|
283
|
+
`var keys=${optionalKeys},ok=true;` +
|
|
284
|
+
`for(var i=0;i<keys.length;i++){if(j.c[keys[i]]!==0&&j.c[keys[i]]!==1){ok=false;break}}` +
|
|
285
|
+
`if(ok){` +
|
|
286
|
+
`var cats=${allCategories},rec={v:${version}};` +
|
|
287
|
+
`for(var k=0;k<cats.length;k++){var c=cats[k];rec[c.key]=c.required||j.c[c.key]===1}` +
|
|
288
|
+
`var val=JSON.stringify(rec);` +
|
|
289
|
+
`try{w.localStorage.setItem(${storageKey},val)}catch(e){}` +
|
|
290
|
+
`var exp=new Date(Date.now()+${cookieDays}*86400000).toUTCString();` +
|
|
291
|
+
`d.cookie=${storageKey}+'='+val+';expires='+exp+';path=/;SameSite=Lax'` +
|
|
292
|
+
`}` +
|
|
293
|
+
`}` +
|
|
294
|
+
language +
|
|
295
|
+
`}catch(e){}` +
|
|
296
|
+
`})(window,document)`;
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
encodeConsentLink, decodeConsentLink, writeConsentLanguage, consentLinker, stripConsentLinkParam,
|
|
300
|
+
consentLinkerScript,
|
|
250
301
|
};
|
|
251
302
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*/
|
|
258
|
-
export const stripConsentLinkParam = (opts) => {
|
|
259
|
-
const linker = opts.linker;
|
|
260
|
-
if (linker == null || typeof location === 'undefined' || typeof history === 'undefined') {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const param = linker.param ?? CONSENT_LINK_PARAM;
|
|
264
|
-
const url = new URL(location.href);
|
|
265
|
-
if (!url.searchParams.has(param)) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
url.searchParams.delete(param);
|
|
269
|
-
try {
|
|
270
|
-
history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
// A sandboxed/embedded context may refuse history writes; the parameter just stays visible.
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
/**
|
|
277
|
-
* The inline `<head>` fragment that adopts a decorated link's decision and strips the parameter —
|
|
278
|
-
* standalone, so a page can stamp it even where nothing else here runs (a legal page with no tag
|
|
279
|
-
* script). `@owlmeans/web-gtm`'s `consentBootstrapScript` embeds this SAME fragment right after
|
|
280
|
-
* pushing Consent Mode's defaults, so a page that has both never runs it twice with different
|
|
281
|
-
* effect — the second attempt finds the parameter already gone and does nothing.
|
|
282
|
-
*
|
|
283
|
-
* Mirrors `consentLinker().adopt` exactly (v2, referrer + freshness/skew + full optional coverage +
|
|
284
|
-
* no existing record), but as hand-rolled JS text rather than a call into this module — the same
|
|
285
|
-
* discipline `consentBootstrapScript`/`consentGateScript` already follow, because this has to run
|
|
286
|
-
* before any bundle (this module included) has loaded. The parameter is ALWAYS stripped via
|
|
287
|
-
* `history.replaceState`, whether or not the trust rule accepts it: a stale or foreign `owlcc` is
|
|
288
|
-
* exactly as much noise as an adopted one, and neither belongs in the visible URL or in what a page
|
|
289
|
-
* reload, a shared link or a browser history entry carries forward.
|
|
290
|
-
*/
|
|
291
|
-
export const consentLinkerScript = (opts) => {
|
|
292
|
-
const linker = opts.linker;
|
|
293
|
-
if (linker == null) {
|
|
294
|
-
return '';
|
|
295
|
-
}
|
|
296
|
-
const param = jsonForScript(linker.param ?? CONSENT_LINK_PARAM);
|
|
297
|
-
const domains = jsonForScript(linker.domains);
|
|
298
|
-
const storageKey = jsonForScript(opts.storageKey ?? CONSENT_KEY);
|
|
299
|
-
const optionalKeys = jsonForScript(optionalOf(opts).map(category => category.key));
|
|
300
|
-
const allCategories = jsonForScript(categoriesOf(opts).map(category => ({ key: category.key, required: category.required === true })));
|
|
301
|
-
const maxAge = linker.maxAge ?? CONSENT_LINK_MAX_AGE;
|
|
302
|
-
const skew = CONSENT_LINK_SKEW;
|
|
303
|
-
const cookieDays = opts.cookieDays ?? CONSENT_COOKIE_DAYS;
|
|
304
|
-
const version = CONSENT_LINK_VERSION;
|
|
305
|
-
// The language rides the same trust decision but is WRITTEN only while `functional` is granted
|
|
306
|
-
// here — in the stored record, or in the record just adopted from this very link (`fg` below).
|
|
307
|
-
// Otherwise it is left on `window` for the store to pick up (memory, never storage): the visitor
|
|
308
|
-
// may still grant functional storage later in this page's life. Mirrors `supportedLanguage` +
|
|
309
|
-
// `writeConsentLanguage`; empty for a page that does not receive a language.
|
|
310
|
-
const functionalKey = jsonForScript(CONSENT_FUNCTIONAL);
|
|
311
|
-
const language = linker.language?.supported != null
|
|
312
|
-
? `var L=${jsonForScript(linker.language.supported)};` +
|
|
313
|
-
`if(typeof j.l==='string'){var lc=j.l.toLowerCase(),lm=null,q;` +
|
|
314
|
-
`for(q=0;q<L.length;q++){if(String(L[q]).toLowerCase()===lc){lm=L[q];break}}` +
|
|
315
|
-
`if(!lm){lc=lc.split(/[-_]/)[0];for(q=0;q<L.length;q++){if(String(L[q]).toLowerCase()===lc){lm=L[q];break}}}` +
|
|
316
|
-
`if(lm){if(fg){try{w.localStorage.setItem(${jsonForScript(linker.language.storageKey ?? CONSENT_LANGUAGE_KEY)},lm)}catch(e){}}` +
|
|
317
|
-
`else{w[${jsonForScript(CONSENT_PENDING_LANGUAGE)}]=lm}}}`
|
|
318
|
-
: '';
|
|
319
|
-
return `(function(w,d){` +
|
|
320
|
-
`var p=${param};var qs;try{qs=new URLSearchParams(location.search)}catch(e){return}` +
|
|
321
|
-
`var raw=qs.get(p);if(raw==null)return;` +
|
|
322
|
-
`qs.delete(p);var rest=qs.toString();` +
|
|
323
|
-
`var url=location.pathname+(rest?('?'+rest):'')+location.hash;` +
|
|
324
|
-
`try{history.replaceState(history.state,'',url)}catch(e){}` +
|
|
325
|
-
`try{` +
|
|
326
|
-
`var b=raw.replace(/-/g,'+').replace(/_/g,'/');while(b.length%4)b+='=';` +
|
|
327
|
-
`var j=JSON.parse(atob(b));` +
|
|
328
|
-
`if(!j||j.v!==${version}||typeof j.t!=='number'||!j.c||typeof j.c!=='object')return;` +
|
|
329
|
-
`var now=Math.floor(Date.now()/1000);` +
|
|
330
|
-
`if(now-j.t>${maxAge}||j.t-now>${skew})return;` +
|
|
331
|
-
`var ref=null;try{ref=new URL(d.referrer).hostname}catch(e){}` +
|
|
332
|
-
`if(!ref||${domains}.indexOf(ref)<0)return;` +
|
|
333
|
-
`var existing=null;try{existing=w.localStorage.getItem(${storageKey})}catch(e){}` +
|
|
334
|
-
`if(!existing){var cp=('; '+d.cookie).split('; '+${storageKey}+'=');` +
|
|
335
|
-
`if(cp.length===2)existing=cp.pop().split(';').shift()}` +
|
|
336
|
-
// `fg`: functional storage is granted here — by a parseable stored record, or by the one
|
|
337
|
-
// adopted below. A record with no `functional` key (saved before the category existed) is not
|
|
338
|
-
// a grant.
|
|
339
|
-
`var fg=false;` +
|
|
340
|
-
`if(existing){try{var ex=JSON.parse(existing);fg=!!ex&&typeof ex==='object'&&ex[${functionalKey}]===true}catch(e){}}` +
|
|
341
|
-
`else{` +
|
|
342
|
-
`var keys=${optionalKeys},ok=true;` +
|
|
343
|
-
`for(var i=0;i<keys.length;i++){if(j.c[keys[i]]!==0&&j.c[keys[i]]!==1){ok=false;break}}` +
|
|
344
|
-
`if(ok){` +
|
|
345
|
-
`var cats=${allCategories},rec={v:${version}};` +
|
|
346
|
-
`for(var k=0;k<cats.length;k++){var c=cats[k];rec[c.key]=c.required||j.c[c.key]===1}` +
|
|
347
|
-
`var val=JSON.stringify(rec);` +
|
|
348
|
-
`try{w.localStorage.setItem(${storageKey},val)}catch(e){}` +
|
|
349
|
-
`var exp=new Date(Date.now()+${cookieDays}*86400000).toUTCString();` +
|
|
350
|
-
`d.cookie=${storageKey}+'='+val+';expires='+exp+';path=/;SameSite=Lax';fg=rec[${functionalKey}]===true` +
|
|
351
|
-
`}` +
|
|
352
|
-
`}` +
|
|
353
|
-
language +
|
|
354
|
-
`}catch(e){}` +
|
|
355
|
-
`})(window,document)`;
|
|
356
|
-
};
|
|
303
|
+
export const consentLinkHelper = createConsentLinkHelper();
|
|
304
|
+
/** @deprecated compat:factory-refactor — use `consentLinkHelper.encodeConsentLink(…)` */
|
|
305
|
+
export const encodeConsentLink = (record, opts) => consentLinkHelper.encodeConsentLink(record, opts);
|
|
306
|
+
/** @deprecated compat:factory-refactor — use `consentLinkHelper.consentLinkerScript(…)` */
|
|
307
|
+
export const consentLinkerScript = (opts) => consentLinkHelper.consentLinkerScript(opts);
|
|
357
308
|
//# sourceMappingURL=linker.js.map
|