@rubytech/create-maxy-code 0.1.126 → 0.1.129
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/dist/index.js +15 -0
- package/package.json +1 -1
- package/payload/platform/config/brand.json +20 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +33 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +119 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +172 -0
- package/payload/platform/lib/aeo-llms-txt-writer/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +73 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +34 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +176 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -0
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +19 -57
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts +5 -38
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts.map +1 -1
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js +7 -114
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js.map +1 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
- package/payload/server/public/consent.css +97 -0
- package/payload/server/public/consent.js +259 -0
- package/payload/server/public/privacy.html +68 -5
- package/payload/server/public/v.js +53 -0
- package/payload/server/server.js +229 -84
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/* PECR consent banner (Task 375).
|
|
2
|
+
*
|
|
3
|
+
* Loads BEFORE /v.js on any page that wires in the visitor pixel:
|
|
4
|
+
*
|
|
5
|
+
* <link rel="stylesheet" href="/consent.css">
|
|
6
|
+
* <script defer src="/consent.js"></script>
|
|
7
|
+
* <script defer src="/v.js" data-site="<slug>"></script>
|
|
8
|
+
*
|
|
9
|
+
* Responsibility:
|
|
10
|
+
* 1. Reads mxy_consent cookie. If absent → render banner.
|
|
11
|
+
* 2. On accept → POST /v/consent {decision:"yes", v?:<token>}, server sets
|
|
12
|
+
* mxy_consent (12mo) + (if v= present in URL and HMAC-valid) mxy_v.
|
|
13
|
+
* 3. On reject → POST /v/consent {decision:"no"}, banner hides, never re-shows
|
|
14
|
+
* until manageCookies() is called or cookie expires.
|
|
15
|
+
* 4. After accept with ?consent=needed&v=… in URL, calls history.replaceState
|
|
16
|
+
* to strip both params so the token doesn't sit in browser history.
|
|
17
|
+
* 5. Dispatches `mxy:consent-changed` so v.js can react without reloading.
|
|
18
|
+
* 6. Exposes window.mxyManageCookies() to re-open the banner from a footer link.
|
|
19
|
+
* 7. On yes→no transition, purges every visitor-graph storage key the pixel
|
|
20
|
+
* could have written.
|
|
21
|
+
*
|
|
22
|
+
* No build step. Plain JS IIFE. ~2.5 KB.
|
|
23
|
+
*/
|
|
24
|
+
(function () {
|
|
25
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return
|
|
26
|
+
|
|
27
|
+
var COOKIE_NAME = 'mxy_consent'
|
|
28
|
+
var CONSENT_ENDPOINT = '/v/consent'
|
|
29
|
+
var BRAND_CONFIG_ENDPOINT = '/v/brand-config'
|
|
30
|
+
// Storage keys the pixel may write. Mirror v.js literals.
|
|
31
|
+
var PIXEL_STORAGE_KEYS = [
|
|
32
|
+
'mxy_visitor_id', // localStorage
|
|
33
|
+
'mxy_session_id', // sessionStorage
|
|
34
|
+
'mxy_session_pv',
|
|
35
|
+
'mxy_last_pv',
|
|
36
|
+
'mxy_scroll_depth',
|
|
37
|
+
'mxy_dwell_start',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
var DEFAULT_COPY = {
|
|
41
|
+
title: 'Before you browse',
|
|
42
|
+
body: 'We use a small first-party script to count page views, scrolls and clicks on this listing. Nothing is shared with third parties. You can change your mind at any time.',
|
|
43
|
+
accept: 'Accept',
|
|
44
|
+
reject: 'Reject',
|
|
45
|
+
manage: 'Manage cookies',
|
|
46
|
+
policyLinkText: 'Read the cookie policy',
|
|
47
|
+
policyHref: '/privacy.html#cookies',
|
|
48
|
+
}
|
|
49
|
+
var DEFAULT_PALETTE = {
|
|
50
|
+
bg: '#FFFFFF',
|
|
51
|
+
fg: '#1F1F1F',
|
|
52
|
+
accent: '#1F1F1F',
|
|
53
|
+
accentFg: '#FFFFFF',
|
|
54
|
+
border: '#E5E5E2',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readCookie(name) {
|
|
58
|
+
var raw = document.cookie || ''
|
|
59
|
+
var parts = raw.split(';')
|
|
60
|
+
for (var i = 0; i < parts.length; i++) {
|
|
61
|
+
var kv = parts[i].trim().split('=')
|
|
62
|
+
if (kv[0] === name) return decodeURIComponent(kv.slice(1).join('='))
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readUrlToken() {
|
|
68
|
+
try {
|
|
69
|
+
var u = new URL(window.location.href)
|
|
70
|
+
if (u.searchParams.get('consent') !== 'needed') return null
|
|
71
|
+
var v = u.searchParams.get('v')
|
|
72
|
+
return v && v.length > 0 && v.length <= 2048 ? v : null
|
|
73
|
+
} catch (_e) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stripConsentQuery() {
|
|
79
|
+
try {
|
|
80
|
+
var u = new URL(window.location.href)
|
|
81
|
+
if (!u.searchParams.has('consent') && !u.searchParams.has('v')) return
|
|
82
|
+
u.searchParams.delete('consent')
|
|
83
|
+
u.searchParams.delete('v')
|
|
84
|
+
var next = u.pathname + (u.searchParams.toString() ? '?' + u.searchParams.toString() : '') + u.hash
|
|
85
|
+
history.replaceState(null, '', next)
|
|
86
|
+
} catch (_e) { /* never break the page */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function purgePixelStorage() {
|
|
90
|
+
for (var i = 0; i < PIXEL_STORAGE_KEYS.length; i++) {
|
|
91
|
+
var k = PIXEL_STORAGE_KEYS[i]
|
|
92
|
+
try { localStorage.removeItem(k) } catch (_e) {}
|
|
93
|
+
try { sessionStorage.removeItem(k) } catch (_e) {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fireChange(decision) {
|
|
98
|
+
try {
|
|
99
|
+
window.dispatchEvent(new CustomEvent('mxy:consent-changed', { detail: { decision: decision } }))
|
|
100
|
+
} catch (_e) {
|
|
101
|
+
// IE/older Safari fallback — synthesise a plain Event.
|
|
102
|
+
try {
|
|
103
|
+
var ev = document.createEvent('Event')
|
|
104
|
+
ev.initEvent('mxy:consent-changed', true, true)
|
|
105
|
+
ev.detail = { decision: decision }
|
|
106
|
+
window.dispatchEvent(ev)
|
|
107
|
+
} catch (_e2) {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function postConsent(decision, token) {
|
|
112
|
+
var body = JSON.stringify(token ? { decision: decision, v: token } : { decision: decision })
|
|
113
|
+
return fetch(CONSENT_ENDPOINT, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: body,
|
|
117
|
+
credentials: 'include',
|
|
118
|
+
keepalive: true,
|
|
119
|
+
}).catch(function () { /* server records the decision; UI proceeds regardless */ })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fetchBrandConfig() {
|
|
123
|
+
return fetch(BRAND_CONFIG_ENDPOINT, { credentials: 'omit' })
|
|
124
|
+
.then(function (r) { return r.ok ? r.json() : null })
|
|
125
|
+
.catch(function () { return null })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function applyPalette(palette) {
|
|
129
|
+
var root = document.documentElement
|
|
130
|
+
for (var k in palette) {
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(palette, k)) {
|
|
132
|
+
root.style.setProperty('--mxy-consent-' + k, palette[k])
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var bannerEl = null
|
|
138
|
+
|
|
139
|
+
function buildBanner(copy) {
|
|
140
|
+
var root = document.createElement('div')
|
|
141
|
+
root.id = 'mxy-consent'
|
|
142
|
+
root.setAttribute('role', 'dialog')
|
|
143
|
+
root.setAttribute('aria-live', 'polite')
|
|
144
|
+
root.setAttribute('aria-label', copy.title)
|
|
145
|
+
|
|
146
|
+
var inner = document.createElement('div')
|
|
147
|
+
inner.className = 'mxy-consent__inner'
|
|
148
|
+
|
|
149
|
+
var text = document.createElement('div')
|
|
150
|
+
text.className = 'mxy-consent__text'
|
|
151
|
+
var h = document.createElement('strong')
|
|
152
|
+
h.className = 'mxy-consent__title'
|
|
153
|
+
h.textContent = copy.title
|
|
154
|
+
var p = document.createElement('p')
|
|
155
|
+
p.className = 'mxy-consent__body'
|
|
156
|
+
p.textContent = copy.body
|
|
157
|
+
var link = document.createElement('a')
|
|
158
|
+
link.className = 'mxy-consent__link'
|
|
159
|
+
link.href = copy.policyHref
|
|
160
|
+
link.textContent = copy.policyLinkText
|
|
161
|
+
link.target = '_blank'
|
|
162
|
+
link.rel = 'noopener'
|
|
163
|
+
text.appendChild(h)
|
|
164
|
+
text.appendChild(p)
|
|
165
|
+
text.appendChild(link)
|
|
166
|
+
|
|
167
|
+
var actions = document.createElement('div')
|
|
168
|
+
actions.className = 'mxy-consent__actions'
|
|
169
|
+
var reject = document.createElement('button')
|
|
170
|
+
reject.className = 'mxy-consent__btn mxy-consent__btn--reject'
|
|
171
|
+
reject.type = 'button'
|
|
172
|
+
reject.textContent = copy.reject
|
|
173
|
+
var accept = document.createElement('button')
|
|
174
|
+
accept.className = 'mxy-consent__btn mxy-consent__btn--accept'
|
|
175
|
+
accept.type = 'button'
|
|
176
|
+
accept.textContent = copy.accept
|
|
177
|
+
// Order: reject first then accept; CSS equal-weights them. ICO requires
|
|
178
|
+
// the reject path to be no harder than the accept path.
|
|
179
|
+
actions.appendChild(reject)
|
|
180
|
+
actions.appendChild(accept)
|
|
181
|
+
|
|
182
|
+
inner.appendChild(text)
|
|
183
|
+
inner.appendChild(actions)
|
|
184
|
+
root.appendChild(inner)
|
|
185
|
+
|
|
186
|
+
accept.addEventListener('click', function () { onDecide('yes') })
|
|
187
|
+
reject.addEventListener('click', function () { onDecide('no') })
|
|
188
|
+
|
|
189
|
+
return root
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function hideBanner() {
|
|
193
|
+
if (bannerEl && bannerEl.parentNode) {
|
|
194
|
+
bannerEl.parentNode.removeChild(bannerEl)
|
|
195
|
+
}
|
|
196
|
+
bannerEl = null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onDecide(decision) {
|
|
200
|
+
var token = decision === 'yes' ? readUrlToken() : null
|
|
201
|
+
// Optimistic local cookie write so v.js sees consent immediately. The
|
|
202
|
+
// server POST below will refresh the lifetime to the full 12 months;
|
|
203
|
+
// this JS write is ITP-clamped to 7 days on Safari but covers the gap
|
|
204
|
+
// until the server response lands.
|
|
205
|
+
document.cookie = COOKIE_NAME + '=' + decision + '; Max-Age=' + (60 * 60 * 24 * 365) + '; Path=/; SameSite=Lax; Secure'
|
|
206
|
+
if (decision === 'no') purgePixelStorage()
|
|
207
|
+
if (decision === 'yes') stripConsentQuery()
|
|
208
|
+
hideBanner()
|
|
209
|
+
|
|
210
|
+
var p = postConsent(decision, token)
|
|
211
|
+
if (decision === 'yes' && token) {
|
|
212
|
+
// Wait for the server to set HttpOnly mxy_v before letting v.js fire
|
|
213
|
+
// the first pageview. Otherwise the first event is attributed to
|
|
214
|
+
// AnonVisitor and the cross-session merge runs one event later.
|
|
215
|
+
p.then(function () { fireChange(decision) })
|
|
216
|
+
} else {
|
|
217
|
+
// No token to bind → no reason to wait; emit immediately.
|
|
218
|
+
fireChange(decision)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function maybeShowBanner() {
|
|
223
|
+
var current = readCookie(COOKIE_NAME)
|
|
224
|
+
if (current === 'yes' || current === 'no') return
|
|
225
|
+
fetchBrandConfig().then(function (cfg) {
|
|
226
|
+
var consent = (cfg && cfg.consent) || {}
|
|
227
|
+
var copy = {}
|
|
228
|
+
for (var k in DEFAULT_COPY) copy[k] = (consent.copy && consent.copy[k]) || DEFAULT_COPY[k]
|
|
229
|
+
var palette = {}
|
|
230
|
+
for (var p in DEFAULT_PALETTE) palette[p] = (consent.palette && consent.palette[p]) || DEFAULT_PALETTE[p]
|
|
231
|
+
applyPalette(palette)
|
|
232
|
+
bannerEl = buildBanner(copy)
|
|
233
|
+
document.body.appendChild(bannerEl)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Withdrawal entry point — footer links call this to re-open the banner
|
|
238
|
+
// when the visitor wants to change their mind. Purge happens in onDecide
|
|
239
|
+
// when the visitor actually flips to reject, not here.
|
|
240
|
+
window.mxyManageCookies = function () {
|
|
241
|
+
if (bannerEl) hideBanner()
|
|
242
|
+
fetchBrandConfig().then(function (cfg) {
|
|
243
|
+
var consent = (cfg && cfg.consent) || {}
|
|
244
|
+
var copy = {}
|
|
245
|
+
for (var k in DEFAULT_COPY) copy[k] = (consent.copy && consent.copy[k]) || DEFAULT_COPY[k]
|
|
246
|
+
var palette = {}
|
|
247
|
+
for (var p in DEFAULT_PALETTE) palette[p] = (consent.palette && consent.palette[p]) || DEFAULT_PALETTE[p]
|
|
248
|
+
applyPalette(palette)
|
|
249
|
+
bannerEl = buildBanner(copy)
|
|
250
|
+
document.body.appendChild(bannerEl)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (document.readyState === 'loading') {
|
|
255
|
+
document.addEventListener('DOMContentLoaded', maybeShowBanner)
|
|
256
|
+
} else {
|
|
257
|
+
maybeShowBanner()
|
|
258
|
+
}
|
|
259
|
+
})()
|
|
@@ -41,11 +41,74 @@
|
|
|
41
41
|
</table>
|
|
42
42
|
<p class="muted">We do not collect: cursor positions, keystrokes, form contents, screen recordings, or anything from third-party tracking networks. The pixel is loaded from the same site as the listing, never from a third party.</p>
|
|
43
43
|
|
|
44
|
-
<h2>Cookies</h2>
|
|
45
|
-
<p>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
<h2 id="cookies">Cookies and storage</h2>
|
|
45
|
+
<p>Before any of the storage below is used, you see a consent banner and choose to accept or reject. If you reject, nothing in the second table is ever written. The only cookie that is set without consent is the one that remembers your consent decision — this is permitted under PECR Regulation 6(4) as strictly necessary to deliver the consent service you asked for.</p>
|
|
46
|
+
|
|
47
|
+
<p><strong>Set even when you reject (strictly necessary):</strong></p>
|
|
48
|
+
<table>
|
|
49
|
+
<thead><tr><th>Key</th><th>Type</th><th>Purpose</th><th>Lifetime</th></tr></thead>
|
|
50
|
+
<tbody>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><code>mxy_consent</code></td>
|
|
53
|
+
<td>Cookie (<code>SameSite=Lax</code>, <code>Secure</code>)</td>
|
|
54
|
+
<td>Remembers whether you accepted or rejected, so the banner doesn't re-show on every page.</td>
|
|
55
|
+
<td>12 months</td>
|
|
56
|
+
</tr>
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
|
|
60
|
+
<p><strong>Set only after you accept:</strong></p>
|
|
61
|
+
<table>
|
|
62
|
+
<thead><tr><th>Key</th><th>Type</th><th>Purpose</th><th>Lifetime</th></tr></thead>
|
|
63
|
+
<tbody>
|
|
64
|
+
<tr>
|
|
65
|
+
<td><code>mxy_v</code></td>
|
|
66
|
+
<td>Cookie (<code>HttpOnly</code>, <code>SameSite=Lax</code>, <code>Secure</code>)</td>
|
|
67
|
+
<td>Signed token containing your contact identifier — only set if you arrived via a personalised marketing link. Lets the site recognise you on later visits without you clicking the link again.</td>
|
|
68
|
+
<td>30 days</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<td><code>mxy_visitor_id</code></td>
|
|
72
|
+
<td>localStorage</td>
|
|
73
|
+
<td>Random ID so we can tell two visits from one browser apart from two browsers.</td>
|
|
74
|
+
<td>Until you clear browser storage</td>
|
|
75
|
+
</tr>
|
|
76
|
+
<tr>
|
|
77
|
+
<td><code>mxy_session_id</code></td>
|
|
78
|
+
<td>sessionStorage</td>
|
|
79
|
+
<td>Random ID so one tab's activity stays grouped together.</td>
|
|
80
|
+
<td>Until you close the tab</td>
|
|
81
|
+
</tr>
|
|
82
|
+
<tr>
|
|
83
|
+
<td><code>mxy_session_pv</code></td>
|
|
84
|
+
<td>sessionStorage</td>
|
|
85
|
+
<td>Page-view identifier used to roll scroll milestones up into a single record per page.</td>
|
|
86
|
+
<td>Until you close the tab</td>
|
|
87
|
+
</tr>
|
|
88
|
+
<tr>
|
|
89
|
+
<td><code>mxy_last_pv</code></td>
|
|
90
|
+
<td>sessionStorage</td>
|
|
91
|
+
<td>Deduplicates a single page view across very fast client-side navigations (within 5 seconds).</td>
|
|
92
|
+
<td>Until you close the tab</td>
|
|
93
|
+
</tr>
|
|
94
|
+
<tr>
|
|
95
|
+
<td><code>mxy_scroll_depth</code></td>
|
|
96
|
+
<td>sessionStorage</td>
|
|
97
|
+
<td>Highest scroll milestone already recorded for the current page, so we only send each one once.</td>
|
|
98
|
+
<td>Until you close the tab</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<tr>
|
|
101
|
+
<td><code>mxy_dwell_start</code></td>
|
|
102
|
+
<td>sessionStorage</td>
|
|
103
|
+
<td>Time you arrived on the page, used to compute dwell when you navigate away.</td>
|
|
104
|
+
<td>Until you close the tab</td>
|
|
105
|
+
</tr>
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
|
|
109
|
+
<h2>Withdrawing consent</h2>
|
|
110
|
+
<p>To change your mind after accepting or rejecting, click <strong>Manage cookies</strong> in the page footer. The banner re-opens and you can switch your choice. If you flip from accept to reject, every key in the second table is cleared from your browser immediately. The personalised cookie (<code>mxy_v</code>) is cleared by your browser when it next sees a response that overrides it; if you want it removed sooner, clear cookies for this site in your browser settings.</p>
|
|
111
|
+
<p>Withdrawing consent stops new visit data from being written. It does not delete visit data already on our servers from your previous accepted sessions. For that, see “Right to access and erasure” below.</p>
|
|
49
112
|
|
|
50
113
|
<h2>Cross-session attribution</h2>
|
|
51
114
|
<p>If you visit anonymously today and later click a personalised marketing link from the agent, prior anonymous visits from the same browser are linked to your contact record. This is the only way an anonymous visit ever becomes named.</p>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
var siteSlug = script.getAttribute('data-site') || ''
|
|
26
26
|
var endpoint = new URL('/v/event', script.src).toString()
|
|
27
27
|
|
|
28
|
+
var CONSENT_KEY = 'mxy_consent'
|
|
28
29
|
var VISITOR_KEY = 'mxy_visitor_id'
|
|
29
30
|
var SESSION_KEY = 'mxy_session_id'
|
|
30
31
|
var SESSION_PV_KEY = 'mxy_session_pv'
|
|
@@ -32,6 +33,55 @@
|
|
|
32
33
|
var SCROLL_KEY = 'mxy_scroll_depth'
|
|
33
34
|
var DWELL_KEY = 'mxy_dwell_start'
|
|
34
35
|
|
|
36
|
+
// ─────────── PECR consent gate (Task 375) ───────────
|
|
37
|
+
// Reads the mxy_consent cookie set by /consent.js (and persisted server-side
|
|
38
|
+
// via POST /v/consent). When the visitor has not accepted, the pixel is a
|
|
39
|
+
// total no-op: no storage writes, no /v/event POSTs, no event listeners
|
|
40
|
+
// attached. A later flip yes ← rebooted via the mxy:consent-changed event.
|
|
41
|
+
function readConsentCookie() {
|
|
42
|
+
var raw = (typeof document !== 'undefined' && document.cookie) || ''
|
|
43
|
+
var parts = raw.split(';')
|
|
44
|
+
for (var i = 0; i < parts.length; i++) {
|
|
45
|
+
var kv = parts[i].trim().split('=')
|
|
46
|
+
if (kv[0] === CONSENT_KEY) return decodeURIComponent(kv.slice(1).join('='))
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
function purgeAll() {
|
|
51
|
+
var keys = [VISITOR_KEY, SESSION_KEY, SESSION_PV_KEY, LAST_PV_KEY, SCROLL_KEY, DWELL_KEY]
|
|
52
|
+
for (var i = 0; i < keys.length; i++) {
|
|
53
|
+
try { localStorage.removeItem(keys[i]) } catch (_e) {}
|
|
54
|
+
try { sessionStorage.removeItem(keys[i]) } catch (_e) {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
var started = false
|
|
58
|
+
function start() {
|
|
59
|
+
if (started) return
|
|
60
|
+
started = true
|
|
61
|
+
boot()
|
|
62
|
+
}
|
|
63
|
+
// React to consent changes from the banner: yes→start (emit deferred
|
|
64
|
+
// pageview), yes→no→purge storage and detach listeners by setting a flag
|
|
65
|
+
// that all emit-paths check before sending.
|
|
66
|
+
var revoked = false
|
|
67
|
+
window.addEventListener('mxy:consent-changed', function (ev) {
|
|
68
|
+
var decision = (ev && ev.detail && ev.detail.decision) || readConsentCookie()
|
|
69
|
+
if (decision === 'yes') {
|
|
70
|
+
revoked = false
|
|
71
|
+
start()
|
|
72
|
+
} else if (decision === 'no') {
|
|
73
|
+
revoked = true
|
|
74
|
+
purgeAll()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
if (readConsentCookie() !== 'yes') {
|
|
78
|
+
// Banner / server will fire mxy:consent-changed when the visitor accepts.
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
start()
|
|
82
|
+
|
|
83
|
+
function boot() {
|
|
84
|
+
|
|
35
85
|
function randomId() {
|
|
36
86
|
if (window.crypto && window.crypto.getRandomValues) {
|
|
37
87
|
var buf = new Uint8Array(16)
|
|
@@ -71,6 +121,7 @@
|
|
|
71
121
|
var sessionId = getSessionId()
|
|
72
122
|
|
|
73
123
|
function send(payload) {
|
|
124
|
+
if (revoked) return
|
|
74
125
|
payload.site = siteSlug
|
|
75
126
|
payload.vid = visitorId
|
|
76
127
|
payload.sid = sessionId
|
|
@@ -98,6 +149,7 @@
|
|
|
98
149
|
|
|
99
150
|
// ─────────── pageview (idempotent within 5s for SPA nav) ───────────
|
|
100
151
|
function emitPageview() {
|
|
152
|
+
if (revoked) return
|
|
101
153
|
try {
|
|
102
154
|
var last = sessionStorage.getItem(LAST_PV_KEY)
|
|
103
155
|
if (last) {
|
|
@@ -188,4 +240,5 @@
|
|
|
188
240
|
|
|
189
241
|
// Boot.
|
|
190
242
|
emitPageview()
|
|
243
|
+
} // end boot()
|
|
191
244
|
})()
|