theseus/bundled-addons/consent/index.js
Local Dev d4b9de93a6 feat(theseus): Cookie Pop-ups — consent banners answered automatically
A bundled add-on that answers cookie consent dialogs, rejecting all but
the essentials by default (or accepting, if the user prefers the banner
simply gone), so pages open without one. Built on DuckDuckGo's
autoconsent (MPL-2.0): its rule bundle covers hundreds of consent
managers, and a reject-button heuristic handles unknown banners in
reject mode. The library runs through the page-inject slot in every
http(s) frame's isolated world; the add-on hands each frame the user's
settings and the rules, and counts what was handled per site for the
panel, which also excludes a site with one click.

build-inject.js assembles inject.js from the library in node_modules
plus the glue, and copies the compact rules and licence into the add-on
so a rule update can ship through the add-on channel.

Host side: page-inject scripts get theseus.evalInPage for the few rules
that need the page's own JavaScript (they already reach the page via
contextBridge, so no new trust tier), and api.tabs is open to
page-inject add-ons as well as request-filter ones.
2026-09-27 19:55:43 +02:00

105 lines
5.7 KiB
JavaScript

// Cookie Pop-ups — answers cookie consent dialogs automatically.
//
// The work happens in inject.js (DuckDuckGo's autoconsent library, MPL-2.0,
// running in every http(s) frame's isolated world). This side holds the
// user's settings, hands the rule bundle to each frame that asks, and keeps
// count of what was handled so the panel can show it per site.
const fs = require("node:fs");
const path = require("node:path");
let api = null;
let rules = null; // { autoconsent: [], compact: <compact-rules.json> }
let rulesVersion = "";
let enabled = true;
let mode = "optOut"; // "optOut" = reject all but essentials; "optIn" = accept (just make it go away)
let skip = new Set(); // hosts where the add-on stays out
let handledTotal = 0;
let perTab = new Map(); // tabId -> { host, handled, cmp, at, failed }
let unsubTabs = null, emitTimer = null, saveTimer = null;
const hostOf = (u) => { try { return new URL(u).host; } catch { return ""; } };
function loadRules() {
const dir = path.join(api.folder, "rules");
const compact = JSON.parse(fs.readFileSync(path.join(dir, "compact-rules.json"), "utf8"));
try { rulesVersion = fs.readFileSync(path.join(dir, "VERSION"), "utf8").trim(); } catch { rulesVersion = ""; }
rules = { autoconsent: [], compact };
api.log(`rules loaded: ${Array.isArray(compact.r) ? compact.r.length : "?"} CMP rules (autoconsent ${rulesVersion || "?"})`);
}
// The configuration every frame runs with. Mirrors DuckDuckGo's own
// defaults; heuristics catch banners no rule knows yet.
function frameConfig() {
return {
enabled: true, autoAction: mode, disabledCmps: [],
enablePrehide: true, enableCosmeticRules: true, enableGeneratedRules: true,
detectRetries: 20, isMainWorld: false, prehideTimeout: 2000,
enableHeuristicDetection: true, enablePopupMutationObserver: false,
visualTest: false, performanceLoggingEnabled: false,
// Unknown banners: in reject mode only press buttons that read as a
// rejection; in accept mode the library's first tier of accept buttons.
heuristicPopupSearchTimeout: 1000, heuristicMode: mode === "optOut" ? "reject" : "tier1",
logs: { lifecycle: false, rulesteps: false, detectionsteps: false, evals: false, errors: false, messages: false, waits: false },
};
}
function snapshot() {
let tab = null; try { tab = api.tabs.active(); } catch {}
const host = tab ? tab.host : "";
const httpPage = !!tab && /^https?:/i.test(tab.url || "");
const t = tab ? perTab.get(tab.id) : null;
const here = t && t.host === host ? t : null;
return {
enabled, mode, handledTotal, rulesVersion,
site: httpPage && host ? { host, skipped: skip.has(host), handled: here ? here.handled : 0, cmp: here ? here.cmp : "", failed: here ? here.failed : 0 } : null,
};
}
function pushState() { try { api.emit("state", snapshot()); } catch {} }
function pushStateSoon() { if (emitTimer) return; emitTimer = setTimeout(() => { emitTimer = null; pushState(); }, 200); }
function scheduleSave() { if (saveTimer) return; saveTimer = setTimeout(() => { saveTimer = null; api.storage.set("handledTotal", handledTotal); }, 2000); }
async function activate(a) {
api = a;
enabled = api.storage.get("enabled", true) !== false;
mode = api.storage.get("mode", "optOut") === "optIn" ? "optIn" : "optOut";
skip = new Set((api.storage.get("skip", []) || []).map(String));
handledTotal = Number(api.storage.get("handledTotal", 0)) || 0;
try { loadRules(); } catch (err) { api.log("rules unavailable:", err?.message); rules = null; }
api.registerSidebarPanel({ id: "main", title: "Cookie Pop-ups", page: "panel.html" });
// From frames: the library asking whether (and how) to run here.
api.onMessage("consent.init", (p, ctx) => {
if (!ctx || ctx.from !== "page" || !enabled || !rules) return { enabled: false };
const host = hostOf(ctx.origin);
if (host && skip.has(host)) return { enabled: false };
return { enabled: true, config: frameConfig(), rules };
});
// From frames: what happened. Count a handled pop-up per tab once the
// library reports it is done; a failed opt-out is remembered for the panel.
api.onMessage("consent.event", (p, ctx) => {
if (!ctx || ctx.from !== "page" || !p) return false;
const host = hostOf(ctx.origin);
let t = perTab.get(ctx.tabId);
if (!t || t.host !== host) { t = { host, handled: 0, cmp: "", at: 0, failed: 0 }; perTab.set(ctx.tabId, t); }
if (p.type === "autoconsentDone") { t.handled++; t.cmp = String(p.cmp || ""); t.at = Date.now(); handledTotal++; scheduleSave(); pushStateSoon(); }
else if ((p.type === "optOutResult" || p.type === "optInResult") && p.result === false) { t.failed++; t.cmp = String(p.cmp || ""); pushStateSoon(); }
else if (p.type === "autoconsentError") { t.failed++; pushStateSoon(); }
return true;
});
// From the panel.
api.onMessage("state", () => snapshot());
api.onMessage("set-enabled", (p) => { enabled = !!(p && p.enabled); api.storage.set("enabled", enabled); pushState(); return snapshot(); });
api.onMessage("set-mode", (p) => { mode = p && p.mode === "optIn" ? "optIn" : "optOut"; api.storage.set("mode", mode); pushState(); return snapshot(); });
api.onMessage("skip-site", (p) => {
const host = String((p && p.host) || "").toLowerCase(); if (!host) return snapshot();
if (p.skipped) skip.add(host); else skip.delete(host);
api.storage.set("skip", [...skip]); pushState(); return snapshot();
});
unsubTabs = api.tabs.onChange(() => pushState());
api.log(`active — ${enabled ? "on" : "off"}, mode ${mode}, ${skip.size} site(s) excluded`);
}
function deactivate() {
try { unsubTabs && unsubTabs(); } catch {}
clearTimeout(emitTimer); if (saveTimer) { clearTimeout(saveTimer); api.storage.set("handledTotal", handledTotal); }
}
module.exports = { activate, deactivate };