@emplugins/emprivacy 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -18
- package/dist/index.d.mts +80 -4
- package/dist/index.mjs +876 -9
- package/docs/DEVELOPMENT.md +6 -10
- package/docs/GETTING_STARTED.md +11 -7
- package/docs/RELEASE_NOTES.md +3 -18
- package/docs/TESTING.md +9 -9
- package/package.json +11 -11
- package/dist/config-BJ1LHuri.mjs +0 -166
- package/dist/config-CqOtRoQR.d.mts +0 -63
- package/dist/sandbox-entry.d.mts +0 -283
- package/dist/sandbox-entry.mjs +0 -693
package/dist/index.mjs
CHANGED
|
@@ -1,22 +1,889 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { definePlugin } from "emdash";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
|
|
4
|
+
//#region src/config.ts
|
|
5
|
+
/**
|
|
6
|
+
* EmPrivacy KV document (`consent:config`) and shared helpers.
|
|
7
|
+
*/
|
|
8
|
+
const KV_KEY = "consent:config";
|
|
9
|
+
const COOKIE_NAME = "emprivacy_cc";
|
|
10
|
+
const PLUGIN_ID = "emprivacy";
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
bannerTitle: "Cookies & privacy",
|
|
13
|
+
bannerMessage: "We use cookies to run this site and optionally for analytics and marketing. You can accept or customize your choices.",
|
|
14
|
+
privacyPolicyUrl: "/privacy",
|
|
15
|
+
cookiePolicyUrl: "",
|
|
16
|
+
strictDefaults: true,
|
|
17
|
+
policyVersion: "1",
|
|
18
|
+
cloudflareWebAnalyticsToken: "",
|
|
19
|
+
analyticsProvider: "cloudflare",
|
|
20
|
+
analyticsScriptUrls: [],
|
|
21
|
+
marketingScriptUrls: [],
|
|
22
|
+
googleConsentMode: false,
|
|
23
|
+
logConsentToServer: false
|
|
24
|
+
};
|
|
25
|
+
function hasUnsafeUrlChars(s) {
|
|
26
|
+
return /[\u0000-\u001F\u007F\s]/.test(s);
|
|
27
|
+
}
|
|
28
|
+
function isHttpsUrl(s) {
|
|
29
|
+
try {
|
|
30
|
+
const t = s.trim();
|
|
31
|
+
if (!t) return false;
|
|
32
|
+
if (hasUnsafeUrlChars(t)) return false;
|
|
33
|
+
if (/%0d|%0a|%09|%0b|%0c|%20/i.test(t)) return false;
|
|
34
|
+
const u = new URL(t);
|
|
35
|
+
if (u.protocol !== "https:") return false;
|
|
36
|
+
if (u.username || u.password) return false;
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function invalidScriptUrlError(kind, u) {
|
|
43
|
+
if (u.includes("<") || u.includes(">")) return `Invalid ${kind} script URL (https only). Paste one full https:// URL per line (the script src only), not a <script> tag or HTML comment. Example: https://static.cloudflareinsights.com/beacon.min.js. The invalid line was: ${u}`;
|
|
44
|
+
return `Invalid ${kind} script URL (https only): ${u}`;
|
|
45
|
+
}
|
|
46
|
+
function isAnalyticsProvider(s) {
|
|
47
|
+
return s === "cloudflare" || s === "none" || s === "custom";
|
|
48
|
+
}
|
|
49
|
+
/** Reject characters that would break JSON or attributes; allow typical Cloudflare site token strings. */
|
|
50
|
+
function assertValidCloudflareToken(t) {
|
|
51
|
+
const s = t.trim();
|
|
52
|
+
if (s.length === 0) return;
|
|
53
|
+
if (s.length > 256) throw new Error("Cloudflare site token is too long.");
|
|
54
|
+
if (/[\s<>'"&]/.test(s)) throw new Error("Cloudflare site token contains invalid characters.");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Root-relative public path (EmDash page route), e.g. `/privacy`.
|
|
58
|
+
* Rejects protocol-relative URLs (`//…`) and whitespace.
|
|
59
|
+
*/
|
|
60
|
+
function isRootRelativeSitePath(s) {
|
|
61
|
+
const t = s.trim();
|
|
62
|
+
if (t.length === 0) return false;
|
|
63
|
+
if (!t.startsWith("/")) return false;
|
|
64
|
+
if (t.startsWith("//")) return false;
|
|
65
|
+
if (hasUnsafeUrlChars(t)) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/** For hooks: turn stored path or absolute URL into a public absolute URL. */
|
|
69
|
+
function resolvePolicyHref(stored, ctx) {
|
|
70
|
+
const t = stored.trim();
|
|
71
|
+
if (!t) return t;
|
|
72
|
+
if (isRootRelativeSitePath(t)) return ctx.url(t);
|
|
73
|
+
return t;
|
|
74
|
+
}
|
|
75
|
+
function isValidPolicyHrefInput(s) {
|
|
76
|
+
const t = s.trim();
|
|
77
|
+
if (!t) return false;
|
|
78
|
+
return isHttpsUrl(t) || isRootRelativeSitePath(t);
|
|
79
|
+
}
|
|
80
|
+
function parseUrlList(text) {
|
|
81
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
82
|
+
return [...new Set(lines)];
|
|
83
|
+
}
|
|
84
|
+
function assertLength(label, s, min, max) {
|
|
85
|
+
const t = s.trim();
|
|
86
|
+
if (t.length < min) throw new Error(`${label} is required.`);
|
|
87
|
+
if (t.length > max) throw new Error(`${label} is too long.`);
|
|
88
|
+
}
|
|
89
|
+
function assertOptionalLength(label, s, max) {
|
|
90
|
+
if (s.trim().length > max) throw new Error(`${label} is too long.`);
|
|
91
|
+
}
|
|
92
|
+
function normalizeConfig(raw) {
|
|
93
|
+
if (typeof raw !== "object" || raw === null) return { ...DEFAULT_CONFIG };
|
|
94
|
+
const o = raw;
|
|
95
|
+
const analytics = Array.isArray(o.analyticsScriptUrls) ? o.analyticsScriptUrls.filter((x) => typeof x === "string") : DEFAULT_CONFIG.analyticsScriptUrls;
|
|
96
|
+
const marketing = Array.isArray(o.marketingScriptUrls) ? o.marketingScriptUrls.filter((x) => typeof x === "string") : DEFAULT_CONFIG.marketingScriptUrls;
|
|
97
|
+
const cfTok = typeof o.cloudflareWebAnalyticsToken === "string" ? o.cloudflareWebAnalyticsToken : "";
|
|
98
|
+
const apCandidate = typeof o.analyticsProvider === "string" ? o.analyticsProvider.trim() : "";
|
|
99
|
+
let analyticsProvider;
|
|
100
|
+
if (isAnalyticsProvider(apCandidate)) analyticsProvider = apCandidate;
|
|
101
|
+
else if (analytics.length > 0) analyticsProvider = "custom";
|
|
102
|
+
else analyticsProvider = DEFAULT_CONFIG.analyticsProvider;
|
|
103
|
+
return {
|
|
104
|
+
bannerTitle: typeof o.bannerTitle === "string" ? o.bannerTitle : DEFAULT_CONFIG.bannerTitle,
|
|
105
|
+
bannerMessage: typeof o.bannerMessage === "string" ? o.bannerMessage : DEFAULT_CONFIG.bannerMessage,
|
|
106
|
+
privacyPolicyUrl: typeof o.privacyPolicyUrl === "string" ? o.privacyPolicyUrl : DEFAULT_CONFIG.privacyPolicyUrl,
|
|
107
|
+
cookiePolicyUrl: typeof o.cookiePolicyUrl === "string" ? o.cookiePolicyUrl : DEFAULT_CONFIG.cookiePolicyUrl,
|
|
108
|
+
strictDefaults: typeof o.strictDefaults === "boolean" ? o.strictDefaults : DEFAULT_CONFIG.strictDefaults,
|
|
109
|
+
policyVersion: typeof o.policyVersion === "string" && o.policyVersion.length > 0 ? o.policyVersion : DEFAULT_CONFIG.policyVersion,
|
|
110
|
+
cloudflareWebAnalyticsToken: cfTok,
|
|
111
|
+
analyticsProvider,
|
|
112
|
+
analyticsScriptUrls: analytics,
|
|
113
|
+
marketingScriptUrls: marketing,
|
|
114
|
+
googleConsentMode: typeof o.googleConsentMode === "boolean" ? o.googleConsentMode : DEFAULT_CONFIG.googleConsentMode,
|
|
115
|
+
logConsentToServer: typeof o.logConsentToServer === "boolean" ? o.logConsentToServer : DEFAULT_CONFIG.logConsentToServer
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Validate admin-submitted fields; throws an Error with a short message on failure */
|
|
119
|
+
function assertValidSavedConfig(input) {
|
|
120
|
+
assertLength("Policy version", input.policyVersion, 1, 64);
|
|
121
|
+
assertOptionalLength("Banner title", input.bannerTitle, 120);
|
|
122
|
+
assertOptionalLength("Short notice", input.bannerMessage, 600);
|
|
123
|
+
const privacyPolicyUrl = input.privacyPolicyUrl.trim();
|
|
124
|
+
if (!isValidPolicyHrefInput(privacyPolicyUrl)) throw new Error("Privacy policy must be a full https:// URL or a site path to your EmDash Page (e.g. /privacy).");
|
|
125
|
+
if (privacyPolicyUrl.length > 2048) throw new Error("Privacy policy URL/path is too long.");
|
|
126
|
+
let cookiePolicyUrl = input.cookiePolicyUrl.trim();
|
|
127
|
+
if (cookiePolicyUrl && !isValidPolicyHrefInput(cookiePolicyUrl)) throw new Error("Cookie policy must be a valid https:// URL or a root-relative path (e.g. /cookies), or empty.");
|
|
128
|
+
if (cookiePolicyUrl.length > 2048) throw new Error("Cookie policy URL/path is too long.");
|
|
129
|
+
if (!cookiePolicyUrl) cookiePolicyUrl = DEFAULT_CONFIG.cookiePolicyUrl;
|
|
130
|
+
const ap = input.analyticsPlatform.trim() || "cloudflare";
|
|
131
|
+
if (!isAnalyticsProvider(ap)) throw new Error("Analytics: choose a supported option (Cloudflare, None, or Custom).");
|
|
132
|
+
const tokenTrim = input.cloudflareToken.trim();
|
|
133
|
+
if (ap === "cloudflare") assertValidCloudflareToken(tokenTrim);
|
|
134
|
+
const analyticsScriptUrls = [];
|
|
135
|
+
if (ap === "custom") {
|
|
136
|
+
const list = parseUrlList(input.analyticsUrlsText);
|
|
137
|
+
if (list.length > 50) throw new Error("Analytics script URL list is too long (max 50).");
|
|
138
|
+
for (const u of list) {
|
|
139
|
+
if (u.length > 2048) throw new Error(invalidScriptUrlError("analytics", u));
|
|
140
|
+
if (!isHttpsUrl(u)) throw new Error(invalidScriptUrlError("analytics", u));
|
|
141
|
+
analyticsScriptUrls.push(u);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const marketingScriptUrls = [];
|
|
145
|
+
const mlist = parseUrlList(input.marketingUrlsText);
|
|
146
|
+
if (mlist.length > 50) throw new Error("Marketing script URL list is too long (max 50).");
|
|
147
|
+
for (const u of mlist) {
|
|
148
|
+
if (u.length > 2048) throw new Error(invalidScriptUrlError("marketing", u));
|
|
149
|
+
if (!isHttpsUrl(u)) throw new Error(invalidScriptUrlError("marketing", u));
|
|
150
|
+
marketingScriptUrls.push(u);
|
|
151
|
+
}
|
|
152
|
+
return normalizeConfig({
|
|
153
|
+
bannerTitle: input.bannerTitle.trim() || DEFAULT_CONFIG.bannerTitle,
|
|
154
|
+
bannerMessage: input.bannerMessage.trim() || DEFAULT_CONFIG.bannerMessage,
|
|
155
|
+
privacyPolicyUrl,
|
|
156
|
+
cookiePolicyUrl,
|
|
157
|
+
strictDefaults: input.strictDefaults,
|
|
158
|
+
policyVersion: input.policyVersion.trim(),
|
|
159
|
+
analyticsProvider: ap,
|
|
160
|
+
cloudflareWebAnalyticsToken: ap === "cloudflare" ? tokenTrim : "",
|
|
161
|
+
analyticsScriptUrls: ap === "custom" ? analyticsScriptUrls : [],
|
|
162
|
+
marketingScriptUrls,
|
|
163
|
+
googleConsentMode: input.googleConsentMode,
|
|
164
|
+
logConsentToServer: input.logConsentToServer
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
3
169
|
//#region src/version.ts
|
|
4
|
-
const VERSION = "
|
|
170
|
+
const VERSION = "2.0.0";
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/runtime.ts
|
|
174
|
+
const ADMIN_SETTINGS_PATH = "/settings";
|
|
175
|
+
const SAVE_ACTION_ID = "emprivacy-save";
|
|
176
|
+
const recordInput = z.object({
|
|
177
|
+
policyVersion: z.string().trim().min(1).max(64),
|
|
178
|
+
analytics: z.boolean(),
|
|
179
|
+
marketing: z.boolean()
|
|
180
|
+
});
|
|
181
|
+
async function loadConfig(ctx) {
|
|
182
|
+
const raw = await ctx.kv.get(KV_KEY);
|
|
183
|
+
if (!raw) return normalizeConfig({});
|
|
184
|
+
try {
|
|
185
|
+
return normalizeConfig(JSON.parse(raw));
|
|
186
|
+
} catch {
|
|
187
|
+
return normalizeConfig({});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function saveConfigString(ctx, json) {
|
|
191
|
+
await ctx.kv.set(KV_KEY, json);
|
|
192
|
+
}
|
|
193
|
+
/** Path-only record URL for browser fetch (same origin). */
|
|
194
|
+
function recordPathForSite() {
|
|
195
|
+
return `/_emdash/api/plugins/${PLUGIN_ID}/record`;
|
|
196
|
+
}
|
|
197
|
+
/** Absolute URL for metadata / banner; paths resolved via EmDash `ctx.url()`. */
|
|
198
|
+
function absolutePolicyHref(stored, ctx) {
|
|
199
|
+
const r = resolvePolicyHref(stored, ctx).trim();
|
|
200
|
+
if (!r) return null;
|
|
201
|
+
try {
|
|
202
|
+
if (new URL(r).protocol !== "https:") return null;
|
|
203
|
+
return r;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function buildGoogleConsentHeadScript() {
|
|
209
|
+
return `(function(){window.dataLayer=window.dataLayer||[];function g(){window.dataLayer.push(arguments);}window.gtag=g;g("consent","default",{"analytics_storage":"denied","ad_storage":"denied","ad_user_data":"denied","ad_personalization":"denied","functionality_storage":"granted","security_storage":"granted","personalization_storage":"denied"});})();`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Public-site bootstrap: no HTML from admin strings in DOM APIs beyond JSON.parse —
|
|
213
|
+
* banner copy is applied with textContent / createTextNode.
|
|
214
|
+
*/
|
|
215
|
+
function buildBodyBootstrap(pr) {
|
|
216
|
+
return `(function(){
|
|
217
|
+
var C=${JSON.stringify(pr).replace(/[<>&\u2028\u2029]/g, (c) => {
|
|
218
|
+
switch (c) {
|
|
219
|
+
case "<": return "\\u003c";
|
|
220
|
+
case ">": return "\\u003e";
|
|
221
|
+
case "&": return "\\u0026";
|
|
222
|
+
case "\u2028": return "\\u2028";
|
|
223
|
+
case "\u2029": return "\\u2029";
|
|
224
|
+
default: return c;
|
|
225
|
+
}
|
|
226
|
+
})};
|
|
227
|
+
var CN="${COOKIE_NAME}";
|
|
228
|
+
function readCookie(){
|
|
229
|
+
try{
|
|
230
|
+
var m=document.cookie.match(new RegExp("(?:^|;\\\\s*)"+CN+"=([^;]*)"));
|
|
231
|
+
var v=m?decodeURIComponent(m[1]):"";
|
|
232
|
+
if(!v||v.length>1024)return"";
|
|
233
|
+
return v;
|
|
234
|
+
}catch(e){return"";}
|
|
235
|
+
}
|
|
236
|
+
function writeCookie(val){
|
|
237
|
+
var secure=document.location.protocol==="https:"?"; Secure":"";
|
|
238
|
+
document.cookie=CN+"="+encodeURIComponent(val)+"; Path=/; SameSite=Lax"+secure+"; Max-Age=31536000";
|
|
239
|
+
}
|
|
240
|
+
function parseState(s){
|
|
241
|
+
try{
|
|
242
|
+
var o=JSON.parse(s);
|
|
243
|
+
if(!o||typeof o!=="object")return null;
|
|
244
|
+
if(typeof o.v!=="string")return null;
|
|
245
|
+
if(o.v!==C.policyVersion)return null;
|
|
246
|
+
var a=o.a,m=o.m;
|
|
247
|
+
var aOk=(a===0||a===1||a===true||a===false);
|
|
248
|
+
var mOk=(m===0||m===1||m===true||m===false);
|
|
249
|
+
if(!aOk||!mOk)return null;
|
|
250
|
+
return {v:o.v,a:!!a,m:!!m};
|
|
251
|
+
}catch(e){return null;}
|
|
252
|
+
}
|
|
253
|
+
function needBanner(){
|
|
254
|
+
var c=parseState(readCookie());
|
|
255
|
+
if(!c)return true;
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
function alreadyScriptSrc(u){
|
|
259
|
+
try{
|
|
260
|
+
return Array.prototype.some.call(document.getElementsByTagName("script"),function(s){
|
|
261
|
+
return s.src===u;
|
|
262
|
+
});
|
|
263
|
+
}catch(e){
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function loadScript(src){
|
|
268
|
+
if(alreadyScriptSrc(src))return;
|
|
269
|
+
var e=document.createElement("script");
|
|
270
|
+
e.src=src;e.async=true;e.referrerPolicy="no-referrer-when-downgrade";
|
|
271
|
+
document.head.appendChild(e);
|
|
272
|
+
}
|
|
273
|
+
function loadCloudflareBeacon(token){
|
|
274
|
+
var u="https://static.cloudflareinsights.com/beacon.min.js";
|
|
275
|
+
if(!token)return;
|
|
276
|
+
if(alreadyScriptSrc(u))return;
|
|
277
|
+
var e=document.createElement("script");
|
|
278
|
+
e.src=u;
|
|
279
|
+
e.defer=true;
|
|
280
|
+
e.setAttribute("data-cf-beacon",JSON.stringify({token:token}));
|
|
281
|
+
e.referrerPolicy="no-referrer-when-downgrade";
|
|
282
|
+
document.head.appendChild(e);
|
|
283
|
+
}
|
|
284
|
+
function applyScripts(a,m){
|
|
285
|
+
if(a){
|
|
286
|
+
if(C.analyticsProvider==="cloudflare"){
|
|
287
|
+
loadCloudflareBeacon(C.cloudflareToken);
|
|
288
|
+
}else if(C.analyticsProvider==="custom"){
|
|
289
|
+
C.analyticsScriptUrls.forEach(loadScript);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if(m)C.marketingScriptUrls.forEach(loadScript);
|
|
293
|
+
}
|
|
294
|
+
function gtagUpdate(a,m){
|
|
295
|
+
if(!C.googleConsentMode||!window.gtag)return;
|
|
296
|
+
window.gtag("consent","update",{
|
|
297
|
+
analytics_storage:a?"granted":"denied",
|
|
298
|
+
ad_storage:m?"granted":"denied",
|
|
299
|
+
ad_user_data:m?"granted":"denied",
|
|
300
|
+
ad_personalization:m?"granted":"denied",
|
|
301
|
+
personalization_storage:m?"granted":"denied"
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function logServer(a,m){
|
|
305
|
+
if(!C.logConsent)return;
|
|
306
|
+
var p=C.recordPath;
|
|
307
|
+
fetch(p,{
|
|
308
|
+
method:"POST",
|
|
309
|
+
credentials:"same-origin",
|
|
310
|
+
headers:{"Content-Type":"application/json"},
|
|
311
|
+
body:JSON.stringify({policyVersion:C.policyVersion,analytics:!!a,marketing:!!m})
|
|
312
|
+
}).catch(function(){});
|
|
313
|
+
}
|
|
314
|
+
function hide(el){if(el)el.setAttribute("hidden","");el&&(el.style.display="none");}
|
|
315
|
+
function show(el){if(el)el.removeAttribute("hidden");el&&(el.style.display="");}
|
|
316
|
+
function mount(){
|
|
317
|
+
var root=document.getElementById("emprivacy-root");
|
|
318
|
+
if(!root)return;
|
|
319
|
+
var trigger=document.createElement("button");
|
|
320
|
+
trigger.type="button";
|
|
321
|
+
trigger.className="emprivacy-cookie-trigger";
|
|
322
|
+
trigger.setAttribute("aria-label","Cookie and privacy settings");
|
|
323
|
+
var ic=document.createElement("span");
|
|
324
|
+
ic.setAttribute("aria-hidden","true");
|
|
325
|
+
ic.className="emprivacy-cookie-trigger-icon";
|
|
326
|
+
ic.appendChild(document.createTextNode("🍪"));
|
|
327
|
+
trigger.appendChild(ic);
|
|
328
|
+
function showTrigger(){
|
|
329
|
+
trigger.removeAttribute("hidden");
|
|
330
|
+
}
|
|
331
|
+
function hideTrigger(){
|
|
332
|
+
trigger.setAttribute("hidden","");
|
|
333
|
+
}
|
|
334
|
+
function mkRow(label,id,on){
|
|
335
|
+
var w=document.createElement("label");
|
|
336
|
+
w.className="emprivacy-switch";
|
|
337
|
+
var cb=document.createElement("input");
|
|
338
|
+
cb.type="checkbox";
|
|
339
|
+
cb.id=id;
|
|
340
|
+
cb.checked=on;
|
|
341
|
+
w.appendChild(cb);
|
|
342
|
+
w.appendChild(document.createTextNode(" "+label));
|
|
343
|
+
return {wrap:w,box:cb};
|
|
344
|
+
}
|
|
345
|
+
function addPolicyLinks(links){
|
|
346
|
+
function addOne(href,text){
|
|
347
|
+
if(!href)return;
|
|
348
|
+
var ok=href.indexOf("https://")===0;
|
|
349
|
+
if(!ok)return;
|
|
350
|
+
var a=document.createElement("a");
|
|
351
|
+
a.href=href;
|
|
352
|
+
a.rel="nofollow noopener";
|
|
353
|
+
a.target="_blank";
|
|
354
|
+
a.appendChild(document.createTextNode(text));
|
|
355
|
+
links.appendChild(a);
|
|
356
|
+
}
|
|
357
|
+
addOne(C.privacyPolicyUrl,"Privacy policy");
|
|
358
|
+
if(C.cookiePolicyUrl)addOne(C.cookiePolicyUrl,"Cookie policy");
|
|
359
|
+
}
|
|
360
|
+
function openReopenPanel(){
|
|
361
|
+
if(root.querySelector(".emprivacy-bar--reopen"))return;
|
|
362
|
+
var st=parseState(readCookie());
|
|
363
|
+
if(!st)return;
|
|
364
|
+
hideTrigger();
|
|
365
|
+
var aOn=!!st.a;
|
|
366
|
+
var mOn=!!st.m;
|
|
367
|
+
var bar=document.createElement("div");
|
|
368
|
+
bar.className="emprivacy-bar emprivacy-bar--reopen";
|
|
369
|
+
bar.setAttribute("role","dialog");
|
|
370
|
+
bar.setAttribute("aria-modal","true");
|
|
371
|
+
var title=document.createElement("h2");
|
|
372
|
+
title.className="emprivacy-title";
|
|
373
|
+
title.appendChild(document.createTextNode(C.bannerTitle));
|
|
374
|
+
var msg=document.createElement("p");
|
|
375
|
+
msg.className="emprivacy-msg";
|
|
376
|
+
msg.appendChild(document.createTextNode(C.bannerMessage));
|
|
377
|
+
var links=document.createElement("div");
|
|
378
|
+
links.className="emprivacy-links";
|
|
379
|
+
addPolicyLinks(links);
|
|
380
|
+
var opts=document.createElement("div");
|
|
381
|
+
opts.className="emprivacy-opts";
|
|
382
|
+
var er=mkRow("Analytics","emprivacy-ra",aOn);
|
|
383
|
+
var mr=mkRow("Marketing","emprivacy-rm",mOn);
|
|
384
|
+
opts.appendChild(er.wrap);
|
|
385
|
+
opts.appendChild(mr.wrap);
|
|
386
|
+
var actions=document.createElement("div");
|
|
387
|
+
actions.className="emprivacy-actions";
|
|
388
|
+
function doReopenPersist(a,m){
|
|
389
|
+
writeCookie(JSON.stringify({v:C.policyVersion,a:a?1:0,m:m?1:0}));
|
|
390
|
+
logServer(a,m);
|
|
391
|
+
location.reload();
|
|
392
|
+
}
|
|
393
|
+
var btnAll=document.createElement("button");
|
|
394
|
+
btnAll.type="button";
|
|
395
|
+
btnAll.className="emprivacy-btn emprivacy-btn-primary";
|
|
396
|
+
btnAll.appendChild(document.createTextNode("Accept all"));
|
|
397
|
+
btnAll.addEventListener("click",function(){doReopenPersist(true,true);});
|
|
398
|
+
var btnRej=document.createElement("button");
|
|
399
|
+
btnRej.type="button";
|
|
400
|
+
btnRej.className="emprivacy-btn";
|
|
401
|
+
btnRej.appendChild(document.createTextNode("Reject non-essential"));
|
|
402
|
+
btnRej.addEventListener("click",function(){doReopenPersist(false,false);});
|
|
403
|
+
var btnSave=document.createElement("button");
|
|
404
|
+
btnSave.type="button";
|
|
405
|
+
btnSave.className="emprivacy-btn emprivacy-btn-primary";
|
|
406
|
+
btnSave.appendChild(document.createTextNode("Save choices"));
|
|
407
|
+
btnSave.addEventListener("click",function(){doReopenPersist(!!er.box.checked,!!mr.box.checked);});
|
|
408
|
+
var btnClose=document.createElement("button");
|
|
409
|
+
btnClose.type="button";
|
|
410
|
+
btnClose.className="emprivacy-btn";
|
|
411
|
+
btnClose.appendChild(document.createTextNode("Close"));
|
|
412
|
+
btnClose.addEventListener("click",function(){
|
|
413
|
+
if(bar.parentNode)bar.parentNode.removeChild(bar);
|
|
414
|
+
showTrigger();
|
|
415
|
+
});
|
|
416
|
+
actions.appendChild(btnAll);
|
|
417
|
+
actions.appendChild(btnRej);
|
|
418
|
+
actions.appendChild(btnSave);
|
|
419
|
+
actions.appendChild(btnClose);
|
|
420
|
+
bar.appendChild(title);
|
|
421
|
+
bar.appendChild(msg);
|
|
422
|
+
bar.appendChild(links);
|
|
423
|
+
bar.appendChild(opts);
|
|
424
|
+
bar.appendChild(actions);
|
|
425
|
+
root.appendChild(bar);
|
|
426
|
+
}
|
|
427
|
+
root.appendChild(trigger);
|
|
428
|
+
hideTrigger();
|
|
429
|
+
if(!needBanner()){
|
|
430
|
+
var st=parseState(readCookie());
|
|
431
|
+
applyScripts(!!(st&&st.a),!!(st&&st.m));
|
|
432
|
+
gtagUpdate(!!(st&&st.a),!!(st&&st.m));
|
|
433
|
+
showTrigger();
|
|
434
|
+
trigger.addEventListener("click",function(){openReopenPanel();});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
var aOn=C.strictDefaults?false:true;
|
|
438
|
+
var mOn=C.strictDefaults?false:true;
|
|
439
|
+
var bar=document.createElement("div");
|
|
440
|
+
bar.className="emprivacy-bar";
|
|
441
|
+
bar.setAttribute("role","dialog");
|
|
442
|
+
bar.setAttribute("aria-modal","false");
|
|
443
|
+
var title=document.createElement("h2");
|
|
444
|
+
title.className="emprivacy-title";
|
|
445
|
+
title.appendChild(document.createTextNode(C.bannerTitle));
|
|
446
|
+
var msg=document.createElement("p");
|
|
447
|
+
msg.className="emprivacy-msg";
|
|
448
|
+
msg.appendChild(document.createTextNode(C.bannerMessage));
|
|
449
|
+
var links=document.createElement("div");
|
|
450
|
+
links.className="emprivacy-links";
|
|
451
|
+
addPolicyLinks(links);
|
|
452
|
+
var opts=document.createElement("div");
|
|
453
|
+
opts.className="emprivacy-opts";
|
|
454
|
+
opts.setAttribute("hidden","");
|
|
455
|
+
var er=mkRow("Analytics","emprivacy-a",aOn);
|
|
456
|
+
var mr=mkRow("Marketing","emprivacy-m",mOn);
|
|
457
|
+
opts.appendChild(er.wrap);
|
|
458
|
+
opts.appendChild(mr.wrap);
|
|
459
|
+
var actions=document.createElement("div");
|
|
460
|
+
actions.className="emprivacy-actions";
|
|
461
|
+
function persist(a,m){
|
|
462
|
+
writeCookie(JSON.stringify({v:C.policyVersion,a:a?1:0,m:m?1:0}));
|
|
463
|
+
logServer(a,m);
|
|
464
|
+
// For Cloudflare Web Analytics (and most tag-based analytics), injecting after load can miss the initial pageview.
|
|
465
|
+
// Reload ensures the script runs in the normal page lifecycle immediately after consent is persisted.
|
|
466
|
+
location.reload();
|
|
467
|
+
}
|
|
468
|
+
var btnAll=document.createElement("button");
|
|
469
|
+
btnAll.type="button";
|
|
470
|
+
btnAll.className="emprivacy-btn emprivacy-btn-primary";
|
|
471
|
+
btnAll.appendChild(document.createTextNode("Accept all"));
|
|
472
|
+
btnAll.addEventListener("click",function(){persist(true,true);});
|
|
473
|
+
var btnRej=document.createElement("button");
|
|
474
|
+
btnRej.type="button";
|
|
475
|
+
btnRej.className="emprivacy-btn";
|
|
476
|
+
btnRej.appendChild(document.createTextNode("Reject non-essential"));
|
|
477
|
+
btnRej.addEventListener("click",function(){persist(false,false);});
|
|
478
|
+
var btnCust=document.createElement("button");
|
|
479
|
+
btnCust.type="button";
|
|
480
|
+
btnCust.className="emprivacy-btn";
|
|
481
|
+
btnCust.appendChild(document.createTextNode("Customize"));
|
|
482
|
+
btnCust.addEventListener("click",function(){show(opts);btnSave.removeAttribute("hidden");});
|
|
483
|
+
var btnSave=document.createElement("button");
|
|
484
|
+
btnSave.type="button";
|
|
485
|
+
btnSave.className="emprivacy-btn emprivacy-btn-primary";
|
|
486
|
+
btnSave.setAttribute("hidden","");
|
|
487
|
+
btnSave.appendChild(document.createTextNode("Save choices"));
|
|
488
|
+
btnSave.addEventListener("click",function(){persist(!!er.box.checked,!!mr.box.checked);});
|
|
489
|
+
actions.appendChild(btnAll);
|
|
490
|
+
actions.appendChild(btnRej);
|
|
491
|
+
actions.appendChild(btnCust);
|
|
492
|
+
actions.appendChild(btnSave);
|
|
493
|
+
bar.appendChild(title);
|
|
494
|
+
bar.appendChild(msg);
|
|
495
|
+
bar.appendChild(links);
|
|
496
|
+
bar.appendChild(opts);
|
|
497
|
+
bar.appendChild(actions);
|
|
498
|
+
root.appendChild(bar);
|
|
499
|
+
}
|
|
500
|
+
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",mount);
|
|
501
|
+
else mount();
|
|
502
|
+
})();`;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Native EmDash runtime — loaded via the named `createPlugin` export.
|
|
506
|
+
* Uses `page:fragments`, which requires trusted in-process (native) execution.
|
|
507
|
+
*/
|
|
508
|
+
function createPlugin() {
|
|
509
|
+
return definePlugin({
|
|
510
|
+
id: PLUGIN_ID,
|
|
511
|
+
version: VERSION,
|
|
512
|
+
capabilities: ["hooks.page-fragments:register"],
|
|
513
|
+
storage: { consentEvents: { indexes: ["createdAt", "policyVersion"] } },
|
|
514
|
+
admin: { pages: [{
|
|
515
|
+
path: ADMIN_SETTINGS_PATH,
|
|
516
|
+
label: "EmPrivacy",
|
|
517
|
+
icon: "shield"
|
|
518
|
+
}] },
|
|
519
|
+
hooks: {
|
|
520
|
+
"plugin:install": async (_e, ctx) => {
|
|
521
|
+
if (!await ctx.kv.get(KV_KEY)) {
|
|
522
|
+
await saveConfigString(ctx, JSON.stringify(normalizeConfig({})));
|
|
523
|
+
ctx.log.info("EmPrivacy: seeded default configuration");
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
"page:metadata": { handler: async (_e, ctx) => {
|
|
527
|
+
const cfg = await loadConfig(ctx);
|
|
528
|
+
const out = [];
|
|
529
|
+
const privacyHref = absolutePolicyHref(cfg.privacyPolicyUrl, ctx);
|
|
530
|
+
if (privacyHref) out.push({
|
|
531
|
+
kind: "link",
|
|
532
|
+
rel: "site.standard.document",
|
|
533
|
+
href: privacyHref,
|
|
534
|
+
key: "emprivacy:privacy"
|
|
535
|
+
});
|
|
536
|
+
const cookieHref = cfg.cookiePolicyUrl ? absolutePolicyHref(cfg.cookiePolicyUrl, ctx) : null;
|
|
537
|
+
if (cookieHref) out.push({
|
|
538
|
+
kind: "meta",
|
|
539
|
+
name: "cookie-policy",
|
|
540
|
+
content: cookieHref,
|
|
541
|
+
key: "emprivacy:cookie-meta"
|
|
542
|
+
});
|
|
543
|
+
return out.length ? out : null;
|
|
544
|
+
} },
|
|
545
|
+
"page:fragments": { handler: async (_e, ctx) => {
|
|
546
|
+
const cfg = await loadConfig(ctx);
|
|
547
|
+
const privacyResolved = absolutePolicyHref(cfg.privacyPolicyUrl, ctx) ?? "";
|
|
548
|
+
const cookieResolved = cfg.cookiePolicyUrl ? absolutePolicyHref(cfg.cookiePolicyUrl, ctx) ?? "" : "";
|
|
549
|
+
const pr = {
|
|
550
|
+
bannerTitle: cfg.bannerTitle,
|
|
551
|
+
bannerMessage: cfg.bannerMessage,
|
|
552
|
+
privacyPolicyUrl: privacyResolved,
|
|
553
|
+
cookiePolicyUrl: cookieResolved,
|
|
554
|
+
strictDefaults: cfg.strictDefaults,
|
|
555
|
+
policyVersion: cfg.policyVersion,
|
|
556
|
+
analyticsProvider: cfg.analyticsProvider,
|
|
557
|
+
cloudflareToken: cfg.cloudflareWebAnalyticsToken,
|
|
558
|
+
analyticsScriptUrls: cfg.analyticsScriptUrls,
|
|
559
|
+
marketingScriptUrls: cfg.marketingScriptUrls,
|
|
560
|
+
googleConsentMode: cfg.googleConsentMode,
|
|
561
|
+
logConsent: cfg.logConsentToServer,
|
|
562
|
+
recordPath: recordPathForSite()
|
|
563
|
+
};
|
|
564
|
+
const style = {
|
|
565
|
+
kind: "html",
|
|
566
|
+
placement: "body:end",
|
|
567
|
+
key: "emprivacy:style",
|
|
568
|
+
html: `<style id="emprivacy-style">
|
|
569
|
+
#emprivacy-root{font-family:system-ui,sans-serif;font-size:14px}
|
|
570
|
+
.emprivacy-bar{position:fixed;z-index:99999;left:0;right:0;bottom:0;background:#111;color:#eee;padding:16px 20px 20px;box-shadow:0 -4px 24px rgba(0,0,0,.25);max-height:45vh;overflow:auto}
|
|
571
|
+
.emprivacy-title{margin:0 0 8px;font-size:1.1rem}
|
|
572
|
+
.emprivacy-msg{margin:0 0 10px;line-height:1.4;opacity:.95}
|
|
573
|
+
.emprivacy-links a{color:#8ecbff;margin-right:12px}
|
|
574
|
+
.emprivacy-opts{margin:10px 0;border-top:1px solid #333;padding-top:10px}
|
|
575
|
+
.emprivacy-switch{display:block;margin:6px 0}
|
|
576
|
+
.emprivacy-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
|
577
|
+
.emprivacy-btn{border-radius:6px;border:1px solid #555;background:#222;color:#eee;padding:8px 12px;cursor:pointer}
|
|
578
|
+
.emprivacy-btn-primary{background:#3b82f6;border-color:#3b82f6;color:#fff}
|
|
579
|
+
.emprivacy-cookie-trigger{position:fixed;z-index:99998;left:16px;bottom:16px;width:48px;height:48px;border-radius:50%;border:1px solid #555;background:#222;color:#e8b86d;box-shadow:0 2px 12px rgba(0,0,0,.2);cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center}
|
|
580
|
+
.emprivacy-cookie-trigger:hover{background:#2a2a2a}
|
|
581
|
+
.emprivacy-cookie-trigger:focus{outline:2px solid #3b82f6;outline-offset:2px}
|
|
582
|
+
.emprivacy-cookie-trigger[hidden]{display:none!important}
|
|
583
|
+
.emprivacy-cookie-trigger-icon{font-size:1.35rem;line-height:1}
|
|
584
|
+
.emprivacy-bar--reopen{z-index:100000}
|
|
585
|
+
</style>`
|
|
586
|
+
};
|
|
587
|
+
const shell = {
|
|
588
|
+
kind: "html",
|
|
589
|
+
placement: "body:end",
|
|
590
|
+
key: "emprivacy:shell",
|
|
591
|
+
html: `<div id="emprivacy-root" data-emprivacy="1"></div>`
|
|
592
|
+
};
|
|
593
|
+
const headScripts = [];
|
|
594
|
+
if (cfg.googleConsentMode) headScripts.push({
|
|
595
|
+
kind: "inline-script",
|
|
596
|
+
placement: "head",
|
|
597
|
+
key: "emprivacy:gcm-default",
|
|
598
|
+
code: buildGoogleConsentHeadScript()
|
|
599
|
+
});
|
|
600
|
+
const boot = {
|
|
601
|
+
kind: "inline-script",
|
|
602
|
+
placement: "body:end",
|
|
603
|
+
key: "emprivacy:boot",
|
|
604
|
+
code: buildBodyBootstrap(pr)
|
|
605
|
+
};
|
|
606
|
+
return [
|
|
607
|
+
...headScripts,
|
|
608
|
+
style,
|
|
609
|
+
shell,
|
|
610
|
+
boot
|
|
611
|
+
];
|
|
612
|
+
} }
|
|
613
|
+
},
|
|
614
|
+
routes: {
|
|
615
|
+
admin: { handler: async (ctx) => {
|
|
616
|
+
const interaction = ctx.input;
|
|
617
|
+
if (interaction?.type === "page_load" && interaction.page === ADMIN_SETTINGS_PATH) return buildSettingsPage(ctx);
|
|
618
|
+
if (interaction?.type === "form_submit" && interaction.action_id === SAVE_ACTION_ID) {
|
|
619
|
+
const v = interaction.values ?? {};
|
|
620
|
+
const asBool = (x) => {
|
|
621
|
+
if (x === true) return true;
|
|
622
|
+
if (x === false) return false;
|
|
623
|
+
if (typeof x === "string") return x === "true" || x === "on" || x === "1";
|
|
624
|
+
return Boolean(x);
|
|
625
|
+
};
|
|
626
|
+
try {
|
|
627
|
+
const next = assertValidSavedConfig({
|
|
628
|
+
bannerTitle: String(v.banner_title ?? ""),
|
|
629
|
+
bannerMessage: String(v.banner_message ?? ""),
|
|
630
|
+
privacyPolicyUrl: String(v.privacy_url ?? ""),
|
|
631
|
+
cookiePolicyUrl: String(v.cookie_url ?? ""),
|
|
632
|
+
strictDefaults: asBool(v.strict_defaults),
|
|
633
|
+
policyVersion: String(v.policy_version ?? ""),
|
|
634
|
+
analyticsPlatform: String(v.analytics_platform ?? "cloudflare"),
|
|
635
|
+
cloudflareToken: String(v.cloudflare_token ?? ""),
|
|
636
|
+
analyticsUrlsText: String(v.analytics_urls ?? ""),
|
|
637
|
+
marketingUrlsText: String(v.marketing_urls ?? ""),
|
|
638
|
+
googleConsentMode: asBool(v.google_cm),
|
|
639
|
+
logConsentToServer: asBool(v.log_server)
|
|
640
|
+
});
|
|
641
|
+
await saveConfigString(ctx, JSON.stringify(next));
|
|
642
|
+
return {
|
|
643
|
+
...await buildSettingsPage(ctx),
|
|
644
|
+
toast: {
|
|
645
|
+
message: "EmPrivacy settings saved.",
|
|
646
|
+
type: "success"
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
} catch (e) {
|
|
650
|
+
const msg = e instanceof Error ? e.message : "Save failed.";
|
|
651
|
+
return {
|
|
652
|
+
...await buildSettingsPage(ctx),
|
|
653
|
+
toast: {
|
|
654
|
+
message: msg,
|
|
655
|
+
type: "error"
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return { blocks: [] };
|
|
661
|
+
} },
|
|
662
|
+
record: {
|
|
663
|
+
public: true,
|
|
664
|
+
input: recordInput,
|
|
665
|
+
handler: async (ctx) => {
|
|
666
|
+
if (ctx.request.method !== "POST") return { ok: false };
|
|
667
|
+
try {
|
|
668
|
+
const origin = ctx.request.headers.get("origin");
|
|
669
|
+
if (origin) {
|
|
670
|
+
if (origin !== new URL(ctx.url("/")).origin) return new Response("forbidden", { status: 403 });
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
return new Response("bad_request", { status: 400 });
|
|
674
|
+
}
|
|
675
|
+
if (!(ctx.request.headers.get("content-type") ?? "").toLowerCase().includes("application/json")) return new Response("unsupported_media_type", { status: 415 });
|
|
676
|
+
const cfg = await loadConfig(ctx);
|
|
677
|
+
if (!cfg.logConsentToServer) return {
|
|
678
|
+
ok: false,
|
|
679
|
+
reason: "logging_disabled"
|
|
680
|
+
};
|
|
681
|
+
const input = ctx.input;
|
|
682
|
+
if (input.policyVersion !== cfg.policyVersion) return new Response("policy_version_mismatch", { status: 400 });
|
|
683
|
+
const row = {
|
|
684
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
685
|
+
policyVersion: input.policyVersion,
|
|
686
|
+
analytics: input.analytics,
|
|
687
|
+
marketing: input.marketing
|
|
688
|
+
};
|
|
689
|
+
try {
|
|
690
|
+
if ((await ctx.storage.consentEvents.query({
|
|
691
|
+
orderBy: { createdAt: "desc" },
|
|
692
|
+
limit: 500
|
|
693
|
+
})).items.length >= 500) return {
|
|
694
|
+
ok: false,
|
|
695
|
+
reason: "log_full"
|
|
696
|
+
};
|
|
697
|
+
} catch {}
|
|
698
|
+
await ctx.storage.consentEvents.put(`${Date.now()}-${Math.random().toString(36).slice(2)}`, row);
|
|
699
|
+
return { ok: true };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async function buildSettingsPage(ctx) {
|
|
706
|
+
const cfg = await loadConfig(ctx);
|
|
707
|
+
const analyticsText = cfg.analyticsScriptUrls.join("\n");
|
|
708
|
+
const marketingText = cfg.marketingScriptUrls.join("\n");
|
|
709
|
+
const platform = cfg.analyticsProvider;
|
|
710
|
+
const docsUrl = "https://github.com/EmPlugins/EmPrivacy/blob/main/docs/PLUGIN_SETTINGS.md";
|
|
711
|
+
const consentFields = await (async () => {
|
|
712
|
+
try {
|
|
713
|
+
return (await ctx.storage.consentEvents.query({
|
|
714
|
+
orderBy: { createdAt: "desc" },
|
|
715
|
+
limit: 15
|
|
716
|
+
})).items.map((item) => {
|
|
717
|
+
const d = item.data;
|
|
718
|
+
return {
|
|
719
|
+
label: d.createdAt,
|
|
720
|
+
value: `policy ${d.policyVersion} · analytics ${d.analytics ? "on" : "off"} · marketing ${d.marketing ? "on" : "off"}`
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
} catch {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
})();
|
|
727
|
+
return { blocks: [
|
|
728
|
+
{
|
|
729
|
+
type: "header",
|
|
730
|
+
text: "EmPrivacy — Cookie & consent"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
type: "context",
|
|
734
|
+
text: `Documentation: ${docsUrl}`
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
type: "context",
|
|
738
|
+
text: "Configure the public banner. **Analytics:** pick a platform (Cloudflare is built in — enter your **site token** from Cloudflare → Web Analytics; EmPrivacy injects the standard beacon with `data-cf-beacon` after consent). **None** = no third-party analytics scripts. **Custom** = one `https://` script URL per line, `src` only. **Marketing:** `https` script URLs, one per line, after marketing consent. No arbitrary admin HTML in the public site."
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
type: "context",
|
|
742
|
+
text: "Privacy / cookie links: use your EmDash **Page** public path (e.g. `/privacy` — same as in the browser address bar when you view that Page) or a full `https://…` URL if the policy is hosted elsewhere."
|
|
743
|
+
},
|
|
744
|
+
{ type: "divider" },
|
|
745
|
+
{
|
|
746
|
+
type: "form",
|
|
747
|
+
blockId: "emprivacy-form",
|
|
748
|
+
fields: [
|
|
749
|
+
{
|
|
750
|
+
type: "text_input",
|
|
751
|
+
action_id: "banner_title",
|
|
752
|
+
label: "Banner title",
|
|
753
|
+
initial_value: cfg.bannerTitle
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
type: "text_input",
|
|
757
|
+
action_id: "banner_message",
|
|
758
|
+
label: "Short notice",
|
|
759
|
+
multiline: true,
|
|
760
|
+
initial_value: cfg.bannerMessage
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
type: "text_input",
|
|
764
|
+
action_id: "privacy_url",
|
|
765
|
+
label: "Privacy policy — EmDash Page path or https URL",
|
|
766
|
+
placeholder: "/privacy or https://…",
|
|
767
|
+
initial_value: cfg.privacyPolicyUrl
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
type: "text_input",
|
|
771
|
+
action_id: "cookie_url",
|
|
772
|
+
label: "Cookie policy (optional) — path or https URL",
|
|
773
|
+
placeholder: "/cookies or https://…",
|
|
774
|
+
initial_value: cfg.cookiePolicyUrl || ""
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
type: "text_input",
|
|
778
|
+
action_id: "policy_version",
|
|
779
|
+
label: "Policy / consent version",
|
|
780
|
+
initial_value: cfg.policyVersion
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: "toggle",
|
|
784
|
+
action_id: "strict_defaults",
|
|
785
|
+
label: "Strict defaults (require opt-in for analytics & marketing)",
|
|
786
|
+
initial_value: cfg.strictDefaults
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
type: "radio",
|
|
790
|
+
action_id: "analytics_platform",
|
|
791
|
+
label: "Analytics platform",
|
|
792
|
+
options: [
|
|
793
|
+
{
|
|
794
|
+
value: "cloudflare",
|
|
795
|
+
label: "Cloudflare Web Analytics"
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
value: "none",
|
|
799
|
+
label: "None"
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
value: "custom",
|
|
803
|
+
label: "Custom (https script URLs, one per line)"
|
|
804
|
+
}
|
|
805
|
+
],
|
|
806
|
+
initial_value: platform
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
type: "text_input",
|
|
810
|
+
action_id: "cloudflare_token",
|
|
811
|
+
label: "Cloudflare Web Analytics site token",
|
|
812
|
+
placeholder: "Token from Web Analytics in the Cloudflare dashboard",
|
|
813
|
+
initial_value: cfg.cloudflareWebAnalyticsToken,
|
|
814
|
+
condition: {
|
|
815
|
+
field: "analytics_platform",
|
|
816
|
+
eq: "cloudflare"
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
type: "text_input",
|
|
821
|
+
action_id: "analytics_urls",
|
|
822
|
+
label: "Custom analytics script URLs (one https URL per line, not a <script> tag)",
|
|
823
|
+
placeholder: "https://… (one per line)",
|
|
824
|
+
multiline: true,
|
|
825
|
+
initial_value: analyticsText,
|
|
826
|
+
condition: {
|
|
827
|
+
field: "analytics_platform",
|
|
828
|
+
eq: "custom"
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
type: "text_input",
|
|
833
|
+
action_id: "marketing_urls",
|
|
834
|
+
label: "Marketing script URLs (one https URL per line, not a <script> tag)",
|
|
835
|
+
placeholder: "https://… (one per line)",
|
|
836
|
+
multiline: true,
|
|
837
|
+
initial_value: marketingText
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
type: "toggle",
|
|
841
|
+
action_id: "google_cm",
|
|
842
|
+
label: "Google Consent Mode v2 (denied defaults in head; updates after choice)",
|
|
843
|
+
initial_value: cfg.googleConsentMode
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
type: "toggle",
|
|
847
|
+
action_id: "log_server",
|
|
848
|
+
label: "Log consent choices to the server (minimal record; no IP stored)",
|
|
849
|
+
initial_value: cfg.logConsentToServer
|
|
850
|
+
}
|
|
851
|
+
],
|
|
852
|
+
submit: {
|
|
853
|
+
label: "Save settings",
|
|
854
|
+
action_id: SAVE_ACTION_ID
|
|
855
|
+
}
|
|
856
|
+
},
|
|
857
|
+
...cfg.logConsentToServer && consentFields.length > 0 ? [
|
|
858
|
+
{ type: "divider" },
|
|
859
|
+
{
|
|
860
|
+
type: "header",
|
|
861
|
+
text: "Recent consent records"
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
type: "fields",
|
|
865
|
+
fields: consentFields
|
|
866
|
+
}
|
|
867
|
+
] : []
|
|
868
|
+
] };
|
|
869
|
+
}
|
|
5
870
|
|
|
6
871
|
//#endregion
|
|
7
872
|
//#region src/index.ts
|
|
873
|
+
var src_default = createPlugin;
|
|
8
874
|
/**
|
|
9
|
-
* EmDash plugin descriptor — add to `plugins: []` in `emdash({ ... })` inside `astro.config`.
|
|
10
|
-
*
|
|
875
|
+
* EmDash native plugin descriptor — add to `plugins: []` in `emdash({ ... })` inside `astro.config`.
|
|
876
|
+
*
|
|
877
|
+
* EmPrivacy uses `page:fragments` (banner + consent scripts), which only runs for **native**
|
|
878
|
+
* plugins registered in `plugins: []`. Do not place this descriptor in `sandboxed: []`.
|
|
879
|
+
* See [Page fragments](https://docs.emdashcms.com/plugins/creating-native-plugins/page-fragments/).
|
|
11
880
|
*/
|
|
12
881
|
function emprivacyPlugin() {
|
|
13
882
|
return {
|
|
14
883
|
id: PLUGIN_ID,
|
|
15
884
|
version: VERSION,
|
|
16
|
-
format: "
|
|
17
|
-
entrypoint: "@emplugins/emprivacy
|
|
18
|
-
capabilities: ["hooks.page-fragments:register"],
|
|
19
|
-
storage: { consentEvents: { indexes: ["createdAt", "policyVersion"] } },
|
|
885
|
+
format: "native",
|
|
886
|
+
entrypoint: "@emplugins/emprivacy",
|
|
20
887
|
adminPages: [{
|
|
21
888
|
path: "/settings",
|
|
22
889
|
label: "EmPrivacy",
|
|
@@ -26,4 +893,4 @@ function emprivacyPlugin() {
|
|
|
26
893
|
}
|
|
27
894
|
|
|
28
895
|
//#endregion
|
|
29
|
-
export { COOKIE_NAME, DEFAULT_CONFIG, KV_KEY, assertValidCloudflareToken, emprivacyPlugin, isRootRelativeSitePath, isValidPolicyHrefInput, normalizeConfig, resolvePolicyHref };
|
|
896
|
+
export { COOKIE_NAME, DEFAULT_CONFIG, KV_KEY, assertValidCloudflareToken, createPlugin, src_default as default, emprivacyPlugin, isRootRelativeSitePath, isValidPolicyHrefInput, normalizeConfig, resolvePolicyHref };
|