feat(theseus): Shield — tracker and ad blocking as a bundled add-on
Theseus had no content blocking at all. Shield blocks requests to known tracking and advertising hosts on every site, using EasyList and EasyPrivacy through Ghostery's adblocker engine (the matcher those lists are written for). The lists ship inside the add-on so blocking works from the first launch, offline; the compiled engine is cached under the add-on's data dir (a 22 ms load instead of a 500 ms parse), and the lists refresh from their publishers about once a day. The panel shows what was stopped on the current page, a one-click allow for the site, the global switch, the running total and the rule versions with an "Update now". Network filters only for now: a blocked request never leaves the browser, but leftover empty ad boxes are not hidden yet. Host side: a "request-filter" capability. Chromium allows one onBeforeRequest listener per session, so main owns it and consults the add-ons' filters; a top-level navigation is never blocked, only http(s) subresources are offered. api.tabs (active tab and a change event) lets the panel show per-site numbers without seeing page content.
This commit is contained in:
parent
8ed051c2da
commit
053d7bc127
9 changed files with 142545 additions and 4 deletions
|
|
@ -24,6 +24,15 @@ const path = require("node:path");
|
||||||
// proxy, page-inject, etc.
|
// proxy, page-inject, etc.
|
||||||
const KNOWN_CAPABILITIES = new Set([
|
const KNOWN_CAPABILITIES = new Set([
|
||||||
"sidebar-panel", "session-proxy",
|
"sidebar-panel", "session-proxy",
|
||||||
|
// request-filter: api.registerRequestFilter(fn) — fn sees every subresource
|
||||||
|
// request of the session ({url, resourceType, method,
|
||||||
|
// frameUrl, initiator, webContentsId}) and returns true to
|
||||||
|
// block it. Main never lets a filter cancel a top-level
|
||||||
|
// navigation. Also unlocks api.tabs (active tab's url/host
|
||||||
|
// and a change event) so a blocker can show per-site
|
||||||
|
// numbers. Sees every URL the browser loads: same trust
|
||||||
|
// bar as page-inject on "*://*/*".
|
||||||
|
"request-filter",
|
||||||
// vault-derive: api.vault.derive(purposePath) — HKDF child of the password
|
// vault-derive: api.vault.derive(purposePath) — HKDF child of the password
|
||||||
// vault's root, namespaced under the add-on id.
|
// vault's root, namespaced under the add-on id.
|
||||||
// page-inject: manifest["page-inject"] = { preload, origins } — the add-on's
|
// page-inject: manifest["page-inject"] = { preload, origins } — the add-on's
|
||||||
|
|
@ -205,7 +214,7 @@ function validateManifest(raw, folderName) {
|
||||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||||
// of the app queries via `getActive()` / `getInstalled()`.
|
// of the app queries via `getActive()` / `getInstalled()`.
|
||||||
class AddonHost {
|
class AddonHost {
|
||||||
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, scanTabForUris, checkAndStageUpdates, restartApp, revealSidebar }) {
|
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, scanTabForUris, checkAndStageUpdates, restartApp, revealSidebar, requestFilter, tabs }) {
|
||||||
this.addonsDir = addonsDir;
|
this.addonsDir = addonsDir;
|
||||||
this.dataDir = dataDir;
|
this.dataDir = dataDir;
|
||||||
this.isDisabled = isDisabled || (() => false);
|
this.isDisabled = isDisabled || (() => false);
|
||||||
|
|
@ -231,6 +240,8 @@ class AddonHost {
|
||||||
// from their folder can't see Theseus's deps (ws, @noble/*, …). Main
|
// from their folder can't see Theseus's deps (ws, @noble/*, …). Main
|
||||||
// hands us its own require so add-ons can share the bundled tree.
|
// hands us its own require so add-ons can share the bundled tree.
|
||||||
this._hostRequire = typeof hostRequire === "function" ? hostRequire : null;
|
this._hostRequire = typeof hostRequire === "function" ? hostRequire : null;
|
||||||
|
this._requestFilter = requestFilter && typeof requestFilter.set === "function" ? requestFilter : null;
|
||||||
|
this._tabs = tabs && typeof tabs.active === "function" ? tabs : null;
|
||||||
// ESM-only deps (@noble/*, @scure/*) can't be require()d by Electron's
|
// ESM-only deps (@noble/*, @scure/*) can't be require()d by Electron's
|
||||||
// Node; hostImport resolves them from the app tree and import()s them.
|
// Node; hostImport resolves them from the app tree and import()s them.
|
||||||
this._hostImport = typeof hostImport === "function" ? hostImport : null;
|
this._hostImport = typeof hostImport === "function" ? hostImport : null;
|
||||||
|
|
@ -372,7 +383,7 @@ class AddonHost {
|
||||||
if (!mod || typeof mod.activate !== "function") {
|
if (!mod || typeof mod.activate !== "function") {
|
||||||
throw new Error(`main file must export an activate(api) function`);
|
throw new Error(`main file must export an activate(api) function`);
|
||||||
}
|
}
|
||||||
const active = { manifest, folder, exports: mod, sidebarPanels: [], handlers: new Map(), inject: null };
|
const active = { manifest, folder, exports: mod, sidebarPanels: [], handlers: new Map(), inject: null, tabListeners: [] };
|
||||||
if (manifest.pageInject) {
|
if (manifest.pageInject) {
|
||||||
// Read the inject source once at activation. It's shipped to every
|
// Read the inject source once at activation. It's shipped to every
|
||||||
// matching tab's preload verbatim, so a syntax error surfaces in the
|
// matching tab's preload verbatim, so a syntax error surfaces in the
|
||||||
|
|
@ -394,6 +405,8 @@ class AddonHost {
|
||||||
// (sockets, timers) from a previous activation doesn't pile up.
|
// (sockets, timers) from a previous activation doesn't pile up.
|
||||||
_deactivateAll() {
|
_deactivateAll() {
|
||||||
for (const [id, active] of this._active) {
|
for (const [id, active] of this._active) {
|
||||||
|
try { if (this._requestFilter) this._requestFilter.clear(active.manifest.id); } catch {}
|
||||||
|
for (const off of active.tabListeners || []) { try { off(); } catch {} }
|
||||||
try { if (typeof active.exports.deactivate === "function") active.exports.deactivate(); }
|
try { if (typeof active.exports.deactivate === "function") active.exports.deactivate(); }
|
||||||
catch (e) { this.log(`[${id}] deactivate() threw: ${e?.message || e}`); }
|
catch (e) { this.log(`[${id}] deactivate() threw: ${e?.message || e}`); }
|
||||||
}
|
}
|
||||||
|
|
@ -461,6 +474,30 @@ class AddonHost {
|
||||||
// fully replacing the browser's outgoing network path — they're
|
// fully replacing the browser's outgoing network path — they're
|
||||||
// the trust boundary while active. Same-signature as the built-in
|
// the trust boundary while active. Same-signature as the built-in
|
||||||
// Tor toggle uses under the hood.
|
// Tor toggle uses under the hood.
|
||||||
|
// Request filter: one function per add-on, consulted by main for every
|
||||||
|
// subresource request. Returns an unregister function.
|
||||||
|
registerRequestFilter: (fn) => {
|
||||||
|
if (typeof fn !== "function") throw new Error(`registerRequestFilter needs a function`);
|
||||||
|
if (!manifest.capabilities.includes("request-filter")) throw new Error(`add-on "${manifest.id}" must declare the "request-filter" capability in addon.json`);
|
||||||
|
if (!this._requestFilter) { this.log(`[${manifest.id}] request filter unavailable (host not wired)`); return () => {}; }
|
||||||
|
this._requestFilter.set(manifest.id, fn);
|
||||||
|
return () => { try { this._requestFilter.clear(manifest.id); } catch {} };
|
||||||
|
},
|
||||||
|
// The active tab (id, url, host) and a change event — enough for a
|
||||||
|
// per-site view, nothing about page content.
|
||||||
|
tabs: {
|
||||||
|
active: () => {
|
||||||
|
if (!manifest.capabilities.includes("request-filter")) throw new Error(`add-on "${manifest.id}" must declare the "request-filter" capability to read tabs`);
|
||||||
|
return this._tabs ? this._tabs.active() : null;
|
||||||
|
},
|
||||||
|
onChange: (cb) => {
|
||||||
|
if (!manifest.capabilities.includes("request-filter")) throw new Error(`add-on "${manifest.id}" must declare the "request-filter" capability to read tabs`);
|
||||||
|
if (!this._tabs || typeof cb !== "function") return () => {};
|
||||||
|
const off = this._tabs.onChange(cb);
|
||||||
|
active.tabListeners.push(off);
|
||||||
|
return off;
|
||||||
|
},
|
||||||
|
},
|
||||||
setSessionProxy: async (rules) => {
|
setSessionProxy: async (rules) => {
|
||||||
if (!this._setSessionProxy) {
|
if (!this._setSessionProxy) {
|
||||||
this.log(`[${manifest.id}] setSessionProxy unavailable (host not wired)`);
|
this.log(`[${manifest.id}] setSessionProxy unavailable (host not wired)`);
|
||||||
|
|
|
||||||
11
bundled-addons/blocker/addon.json
Normal file
11
bundled-addons/blocker/addon.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"id": "blocker",
|
||||||
|
"name": "Shield",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Blocks third-party trackers and ads on every site, using the EasyList and EasyPrivacy rules. Counts what it stopped on the page you are on, lets you allow a site with one click, and refreshes its rules in the background.",
|
||||||
|
"author": "Silent Mode",
|
||||||
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA1djdjMCA1IDMuNSA5LjIgOCAxMCA0LjUtLjggOC01IDgtMTBWNWwtOC0zeiIgZmlsbD0iIzRjOWVmZiIvPjxwYXRoIGQ9Ik05LjIgOS4yIDE0LjggMTQuOE0xNC44IDkuMiA5LjIgMTQuOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==",
|
||||||
|
"main": "index.js",
|
||||||
|
"capabilities": ["sidebar-panel", "request-filter"],
|
||||||
|
"updateURL": "https://navigate.st/bns/theseus.x/extensions/blocker/updates.json"
|
||||||
|
}
|
||||||
213
bundled-addons/blocker/index.js
Normal file
213
bundled-addons/blocker/index.js
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Shield — tracker and ad blocking for every tab.
|
||||||
|
//
|
||||||
|
// The engine is Ghostery's adblocker (the same matcher uBlock-style lists
|
||||||
|
// are written for), fed with EasyList + EasyPrivacy. The lists ship inside
|
||||||
|
// the add-on so blocking works from the first launch, offline; a compiled
|
||||||
|
// copy of the engine is cached under the add-on's data dir so later starts
|
||||||
|
// load in milliseconds instead of re-parsing 3.7 MB of rules, and the lists
|
||||||
|
// are refreshed from their publishers about once a day in the background.
|
||||||
|
//
|
||||||
|
// Main owns the session's request hook and asks this add-on for a verdict
|
||||||
|
// on each subresource (never a top-level navigation). Network filters only:
|
||||||
|
// a blocked request never reaches the tracker at all. Cosmetic (element
|
||||||
|
// hiding) rules are not applied in this version.
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const fsp = require("node:fs/promises");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const LISTS = [
|
||||||
|
{ id: "easylist", file: "easylist.txt", url: "https://easylist.to/easylist/easylist.txt", title: "EasyList" },
|
||||||
|
{ id: "easyprivacy", file: "easyprivacy.txt", url: "https://easylist.to/easylist/easyprivacy.txt", title: "EasyPrivacy" },
|
||||||
|
];
|
||||||
|
const CACHE_SUBDIR = "blocker";
|
||||||
|
const ENGINE_FILE = "engine.bin";
|
||||||
|
const META_FILE = "engine.json";
|
||||||
|
const CACHE_FORMAT = 1;
|
||||||
|
const REFRESH_MS = 24 * 60 * 60_000;
|
||||||
|
const REFRESH_CHECK_MS = 60 * 60_000;
|
||||||
|
const FIRST_REFRESH_DELAY_MS = 90_000;
|
||||||
|
const FETCH_TIMEOUT_MS = 60_000;
|
||||||
|
// Electron resourceType → the request types the lists are written against.
|
||||||
|
const TYPE_MAP = {
|
||||||
|
mainFrame: "main_frame", subFrame: "sub_frame", stylesheet: "stylesheet", script: "script", image: "image",
|
||||||
|
font: "font", object: "object", xhr: "xmlhttprequest", ping: "ping", cspReport: "csp_report", media: "media",
|
||||||
|
webSocket: "websocket", other: "other",
|
||||||
|
};
|
||||||
|
|
||||||
|
let api = null;
|
||||||
|
let FiltersEngine = null, Request = null;
|
||||||
|
let engine = null;
|
||||||
|
let enabled = true;
|
||||||
|
let allow = new Set(); // hosts the user allowed
|
||||||
|
let counts = new Map(); // webContentsId -> { host, blocked, total }
|
||||||
|
let blockedTotal = 0;
|
||||||
|
let listMeta = { updatedAt: 0, source: "bundled", versions: {} };
|
||||||
|
let unregister = null, unsubTabs = null;
|
||||||
|
let refreshTimer = null, refreshing = false;
|
||||||
|
let emitTimer = null, saveTimer = null;
|
||||||
|
|
||||||
|
const hostOf = (u) => { try { return new URL(u).host; } catch { return ""; } };
|
||||||
|
const cacheDir = () => path.join(api.dataDir, CACHE_SUBDIR);
|
||||||
|
const listVersion = (text) => (/^!\s*Version:\s*(\S+)/im.exec(text) || [])[1] || "";
|
||||||
|
|
||||||
|
// ---- engine ------------------------------------------------------------------
|
||||||
|
function buildEngine(texts) {
|
||||||
|
const t0 = Date.now();
|
||||||
|
const e = FiltersEngine.parse(texts.join("\n"), { loadCosmeticFilters: false, loadNetworkFilters: true, enableCompression: false });
|
||||||
|
api.log(`engine built from ${texts.length} lists in ${Date.now() - t0} ms`);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
async function readBundledLists() {
|
||||||
|
const out = [];
|
||||||
|
for (const l of LISTS) out.push({ ...l, text: await fsp.readFile(path.join(api.folder, "lists", l.file), "utf8") });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
async function saveCache(e, meta) {
|
||||||
|
await fsp.mkdir(cacheDir(), { recursive: true });
|
||||||
|
await fsp.writeFile(path.join(cacheDir(), ENGINE_FILE), Buffer.from(e.serialize()));
|
||||||
|
await fsp.writeFile(path.join(cacheDir(), META_FILE), JSON.stringify({ format: CACHE_FORMAT, ...meta }));
|
||||||
|
}
|
||||||
|
async function loadCache() {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(await fsp.readFile(path.join(cacheDir(), META_FILE), "utf8"));
|
||||||
|
if (meta.format !== CACHE_FORMAT) return null;
|
||||||
|
const buf = await fsp.readFile(path.join(cacheDir(), ENGINE_FILE));
|
||||||
|
const e = FiltersEngine.deserialize(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
||||||
|
return { engine: e, meta };
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
function installEngine(e, meta) {
|
||||||
|
engine = e;
|
||||||
|
listMeta = { updatedAt: meta.updatedAt || 0, source: meta.source || "bundled", versions: meta.versions || {} };
|
||||||
|
api.storage.set("listMeta", listMeta);
|
||||||
|
pushState();
|
||||||
|
}
|
||||||
|
// Cached engine when its bundled-list versions still match what ships;
|
||||||
|
// otherwise parse the bundled lists (first run, or after an update of the
|
||||||
|
// add-on brought newer lists) and cache the result.
|
||||||
|
async function ensureEngine() {
|
||||||
|
const bundled = await readBundledLists();
|
||||||
|
const bundledVersions = Object.fromEntries(bundled.map((l) => [l.id, listVersion(l.text)]));
|
||||||
|
const cached = await loadCache();
|
||||||
|
if (cached && cached.meta.bundledVersions && LISTS.every((l) => cached.meta.bundledVersions[l.id] === bundledVersions[l.id])) {
|
||||||
|
installEngine(cached.engine, cached.meta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const e = buildEngine(bundled.map((l) => l.text));
|
||||||
|
const meta = { updatedAt: 0, source: "bundled", versions: bundledVersions, bundledVersions };
|
||||||
|
installEngine(e, meta);
|
||||||
|
saveCache(e, meta).catch((err) => api.log("cache write failed:", err?.message));
|
||||||
|
}
|
||||||
|
async function fetchText(url) {
|
||||||
|
const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const r = await fetch(url, { signal: ctl.signal, cache: "no-store" });
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
const text = await r.text();
|
||||||
|
if (!/^\[Adblock/im.test(text.slice(0, 200)) && !/^!\s*Title:/im.test(text.slice(0, 2000))) throw new Error("not a filter list");
|
||||||
|
return text;
|
||||||
|
} finally { clearTimeout(t); }
|
||||||
|
}
|
||||||
|
// Pull fresh lists from their publishers and rebuild. Keeps the current
|
||||||
|
// engine on any failure — a stale list beats no list.
|
||||||
|
async function refreshLists(reason) {
|
||||||
|
if (refreshing) return { ok: false, error: "already refreshing" };
|
||||||
|
refreshing = true; pushState();
|
||||||
|
try {
|
||||||
|
const texts = [], versions = {};
|
||||||
|
for (const l of LISTS) { const text = await fetchText(l.url); texts.push(text); versions[l.id] = listVersion(text); }
|
||||||
|
const bundled = await readBundledLists();
|
||||||
|
const e = buildEngine(texts);
|
||||||
|
const meta = { updatedAt: Date.now(), source: "online", versions, bundledVersions: Object.fromEntries(bundled.map((l) => [l.id, listVersion(l.text)])) };
|
||||||
|
installEngine(e, meta);
|
||||||
|
await saveCache(e, meta);
|
||||||
|
api.log(`lists refreshed (${reason}): ${Object.entries(versions).map(([k, v]) => k + " " + v).join(", ")}`);
|
||||||
|
return { ok: true, versions };
|
||||||
|
} catch (err) {
|
||||||
|
api.log(`list refresh failed (${reason}):`, err?.message);
|
||||||
|
return { ok: false, error: err?.message || "refresh failed" };
|
||||||
|
} finally { refreshing = false; pushState(); }
|
||||||
|
}
|
||||||
|
function scheduleRefresh() {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
const due = () => Date.now() - (listMeta.updatedAt || 0) > REFRESH_MS;
|
||||||
|
setTimeout(() => { if (due()) refreshLists("startup").catch(() => {}); }, FIRST_REFRESH_DELAY_MS);
|
||||||
|
refreshTimer = setInterval(() => { if (due()) refreshLists("interval").catch(() => {}); }, REFRESH_CHECK_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the verdict -----------------------------------------------------------------
|
||||||
|
function decide(r) {
|
||||||
|
const wc = r.webContentsId;
|
||||||
|
if (r.resourceType === "mainFrame") {
|
||||||
|
if (wc != null) counts.set(wc, { host: hostOf(r.url), blocked: 0, total: 0 });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!enabled || !engine) return false;
|
||||||
|
let entry = wc != null ? counts.get(wc) : null;
|
||||||
|
if (!entry && wc != null) { entry = { host: hostOf(r.frameUrl || r.initiator), blocked: 0, total: 0 }; counts.set(wc, entry); }
|
||||||
|
if (entry) entry.total++;
|
||||||
|
const site = entry ? entry.host : hostOf(r.frameUrl || r.initiator);
|
||||||
|
if (site && allow.has(site)) return false;
|
||||||
|
const req = Request.fromRawDetails({ url: r.url, type: TYPE_MAP[r.resourceType] || "other", sourceUrl: r.frameUrl || r.initiator || "" });
|
||||||
|
const m = engine.match(req);
|
||||||
|
if (!m.match) return false;
|
||||||
|
if (entry) entry.blocked++;
|
||||||
|
blockedTotal++;
|
||||||
|
scheduleSave(); pushStateSoon();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- state for the panel ---------------------------------------------------------
|
||||||
|
function snapshot() {
|
||||||
|
let tab = null; try { tab = api.tabs.active(); } catch {}
|
||||||
|
const host = tab ? tab.host : "";
|
||||||
|
const c = tab && tab.webContentsId != null ? counts.get(tab.webContentsId) : null;
|
||||||
|
const httpPage = !!tab && /^https?:/i.test(tab.url || "");
|
||||||
|
return {
|
||||||
|
enabled, refreshing, blockedTotal,
|
||||||
|
site: httpPage && host ? { host, allowed: allow.has(host), blocked: c ? c.blocked : 0, requests: c ? c.total : 0 } : null,
|
||||||
|
lists: { updatedAt: listMeta.updatedAt, source: listMeta.source, versions: listMeta.versions, titles: Object.fromEntries(LISTS.map((l) => [l.id, l.title])) },
|
||||||
|
ready: !!engine,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function pushState() { try { api.emit("state", snapshot()); } catch {} }
|
||||||
|
function pushStateSoon() { if (emitTimer) return; emitTimer = setTimeout(() => { emitTimer = null; pushState(); }, 250); }
|
||||||
|
function scheduleSave() { if (saveTimer) return; saveTimer = setTimeout(() => { saveTimer = null; api.storage.set("blockedTotal", blockedTotal); }, 2000); }
|
||||||
|
function persistAllow() { api.storage.set("allow", [...allow]); }
|
||||||
|
|
||||||
|
// ---- lifecycle -------------------------------------------------------------------
|
||||||
|
async function activate(a) {
|
||||||
|
api = a;
|
||||||
|
({ FiltersEngine, Request } = api.require("@ghostery/adblocker"));
|
||||||
|
enabled = api.storage.get("enabled", true) !== false;
|
||||||
|
allow = new Set((api.storage.get("allow", []) || []).map(String));
|
||||||
|
blockedTotal = Number(api.storage.get("blockedTotal", 0)) || 0;
|
||||||
|
listMeta = api.storage.get("listMeta", listMeta) || listMeta;
|
||||||
|
|
||||||
|
api.registerSidebarPanel({ id: "main", title: "Shield", page: "panel.html" });
|
||||||
|
api.onMessage("state", () => snapshot());
|
||||||
|
api.onMessage("set-enabled", (p) => { enabled = !!(p && p.enabled); api.storage.set("enabled", enabled); pushState(); return snapshot(); });
|
||||||
|
api.onMessage("allow-site", (p) => {
|
||||||
|
const host = String((p && p.host) || "").toLowerCase(); if (!host) return snapshot();
|
||||||
|
if (p.allowed) allow.add(host); else allow.delete(host);
|
||||||
|
persistAllow(); pushState(); return snapshot();
|
||||||
|
});
|
||||||
|
api.onMessage("refresh-lists", () => refreshLists("manual"));
|
||||||
|
|
||||||
|
// Filter first, engine second: requests made while the engine builds pass
|
||||||
|
// through (a few hundred milliseconds on first run), which beats holding
|
||||||
|
// the browser's startup for a parse.
|
||||||
|
unregister = api.registerRequestFilter(decide);
|
||||||
|
unsubTabs = api.tabs.onChange(() => pushState());
|
||||||
|
try { await ensureEngine(); } catch (err) { api.log("engine unavailable:", err?.message); }
|
||||||
|
scheduleRefresh();
|
||||||
|
api.log(`active — ${enabled ? "on" : "off"}, ${allow.size} allowed site(s), lists ${listMeta.source}`);
|
||||||
|
}
|
||||||
|
function deactivate() {
|
||||||
|
try { unregister && unregister(); } catch {}
|
||||||
|
try { unsubTabs && unsubTabs(); } catch {}
|
||||||
|
clearInterval(refreshTimer); clearTimeout(emitTimer); clearTimeout(saveTimer);
|
||||||
|
if (saveTimer) api.storage.set("blockedTotal", blockedTotal);
|
||||||
|
engine = null;
|
||||||
|
}
|
||||||
|
module.exports = { activate, deactivate };
|
||||||
85038
bundled-addons/blocker/lists/easylist.txt
Normal file
85038
bundled-addons/blocker/lists/easylist.txt
Normal file
File diff suppressed because it is too large
Load diff
56983
bundled-addons/blocker/lists/easyprivacy.txt
Normal file
56983
bundled-addons/blocker/lists/easyprivacy.txt
Normal file
File diff suppressed because one or more lines are too long
124
bundled-addons/blocker/panel.html
Normal file
124
bundled-addons/blocker/panel.html
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Shield</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark;
|
||||||
|
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
|
||||||
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#0ac18e; --blue:#4c9eff; --warn:#f5a524; --danger:#ff5b5b; }
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
|
||||||
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --blue:#2b6fd6; }
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; height: 100%; }
|
||||||
|
body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; }
|
||||||
|
header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||||
|
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||||
|
header .pill { margin-left: auto; }
|
||||||
|
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px;
|
||||||
|
font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
|
||||||
|
background: rgba(139,152,169,.14); color: var(--mut); }
|
||||||
|
.pill.on { color: #0b0e14; background: var(--acid); }
|
||||||
|
.pill.off { color: #fff; background: var(--danger); }
|
||||||
|
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||||
|
.body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.card { border: 1px solid var(--line); background: var(--panel); border-radius: 14px; padding: 16px; }
|
||||||
|
.site { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
|
||||||
|
.site .host { font: 600 13px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--mut); overflow-wrap: anywhere; }
|
||||||
|
.site .num { font-size: 40px; font-weight: 700; line-height: 1.1; color: var(--blue); font-variant-numeric: tabular-nums; }
|
||||||
|
.site .num.allowed { color: var(--dim); }
|
||||||
|
.site .lbl { color: var(--mut); }
|
||||||
|
.site .sub { color: var(--dim); font-size: 11.5px; }
|
||||||
|
.btn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink); padding: 9px 16px; border-radius: 10px;
|
||||||
|
font: inherit; font-weight: 600; cursor: pointer; }
|
||||||
|
.btn:hover { border-color: rgb(from var(--blue) r g b / .5); }
|
||||||
|
.btn.primary { background: var(--blue); color: #fff; border-color: transparent; }
|
||||||
|
.btn.quiet { background: transparent; }
|
||||||
|
.btn:disabled { opacity: .5; cursor: default; }
|
||||||
|
.btn.wide { margin-top: 10px; min-width: 200px; }
|
||||||
|
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.row + .row { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
|
||||||
|
.row .k { font-weight: 600; }
|
||||||
|
.row .d { color: var(--dim); font-size: 11.5px; }
|
||||||
|
.row .v { color: var(--mut); font-variant-numeric: tabular-nums; text-align: right; }
|
||||||
|
.switch { position: relative; width: 40px; height: 22px; border-radius: 11px; background: var(--dim); border: none; cursor: pointer; flex: none; transition: background .15s; }
|
||||||
|
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .15s; }
|
||||||
|
.switch.on { background: var(--acid); } .switch.on::after { left: 20px; }
|
||||||
|
.note { color: var(--dim); font-size: 11.5px; line-height: 1.45; }
|
||||||
|
.note.warn { color: var(--warn); }
|
||||||
|
h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><span>🛡</span><h1>Shield</h1><span class="pill" id="pill"><span class="dot"></span><span id="pillText">…</span></span></header>
|
||||||
|
<div class="body">
|
||||||
|
<div class="card site" id="siteCard">
|
||||||
|
<div class="lbl" id="siteLbl">Open a site to see what Shield stops there.</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div><div class="k">Block trackers and ads</div><div class="d">On every site, using EasyList and EasyPrivacy.</div></div>
|
||||||
|
<button class="switch" id="enabled" role="switch" aria-checked="true" title="Turn Shield on or off"></button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><div class="k">Blocked since install</div><div class="d">Requests that never left the browser.</div></div>
|
||||||
|
<div class="v" id="total">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Rules</h2>
|
||||||
|
<div id="lists"></div>
|
||||||
|
<div class="row" style="margin-top:12px; padding-top:12px; border-top:1px solid var(--line)">
|
||||||
|
<div class="note" id="updated"></div>
|
||||||
|
<button class="btn quiet" id="refresh">Update now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note">Shield stops requests to known tracking and advertising hosts before they leave Theseus. It does not hide leftover empty ad boxes yet. If a site misbehaves, allow it here and reload.</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
||||||
|
const ago = (t) => { if (!t) return "bundled with the add-on"; const m = Math.round((Date.now() - t) / 60000); if (m < 2) return "updated just now"; if (m < 90) return `updated ${m} min ago`; const h = Math.round(m / 60); if (h < 36) return `updated ${h} h ago`; return `updated ${Math.round(h / 24)} days ago`; };
|
||||||
|
let state = null, hintHost = null;
|
||||||
|
function render(s) {
|
||||||
|
state = s;
|
||||||
|
const on = s.enabled && s.ready;
|
||||||
|
$("pill").className = "pill " + (s.enabled ? (s.ready ? "on" : "") : "off");
|
||||||
|
$("pillText").textContent = !s.enabled ? "Off" : !s.ready ? "Loading" : "On";
|
||||||
|
$("enabled").classList.toggle("on", s.enabled); $("enabled").setAttribute("aria-checked", String(s.enabled));
|
||||||
|
$("total").textContent = Number(s.blockedTotal || 0).toLocaleString();
|
||||||
|
const card = $("siteCard");
|
||||||
|
if (!s.site) {
|
||||||
|
card.innerHTML = `<div class="lbl">Open a site to see what Shield stops there.</div>`;
|
||||||
|
} else if (s.site.allowed) {
|
||||||
|
card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num allowed">—</div><div class="lbl">Allowed: nothing is blocked on this site.</div>
|
||||||
|
<button class="btn primary wide" id="siteBtn">Block on this site</button>`;
|
||||||
|
} else {
|
||||||
|
card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num">${on ? Number(s.site.blocked).toLocaleString() : "—"}</div>
|
||||||
|
<div class="lbl">${on ? (s.site.blocked === 1 ? "request blocked on this page" : "requests blocked on this page") : "Shield is off"}</div>
|
||||||
|
<div class="sub">${esc(s.site.requests)} requests seen</div>
|
||||||
|
<button class="btn wide" id="siteBtn">Allow this site</button>`;
|
||||||
|
}
|
||||||
|
const btn = $("siteBtn");
|
||||||
|
if (btn) btn.onclick = async () => {
|
||||||
|
const next = !s.site.allowed;
|
||||||
|
hintHost = s.site.host;
|
||||||
|
await window.silentmode.invoke("allow-site", { host: s.site.host, allowed: next });
|
||||||
|
};
|
||||||
|
if (s.site && hintHost === s.site.host) card.insertAdjacentHTML("beforeend", `<div class="sub">Reload the page to apply.</div>`);
|
||||||
|
else hintHost = null;
|
||||||
|
const L = s.lists || { versions: {}, titles: {} };
|
||||||
|
$("lists").innerHTML = Object.keys(L.titles || {}).map((id) => `<div class="row"><div class="k">${esc(L.titles[id])}</div><div class="v">${esc(L.versions[id] || "—")}</div></div>`).join("");
|
||||||
|
$("updated").textContent = (L.source === "online" ? "Rules " + ago(L.updatedAt) : "Rules bundled with the add-on") + (s.refreshing ? " · refreshing…" : "");
|
||||||
|
$("refresh").disabled = !!s.refreshing;
|
||||||
|
}
|
||||||
|
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state && state.enabled) }).then(render);
|
||||||
|
$("refresh").onclick = () => { $("refresh").disabled = true; window.silentmode.invoke("refresh-lists").then((r) => { if (r && !r.ok) $("updated").textContent = "Update failed: " + (r.error || "unknown"); }); };
|
||||||
|
window.silentmode.on("state", render);
|
||||||
|
window.silentmode.invoke("state").then(render);
|
||||||
|
window.silentmode.onVisibility((v) => { if (v) window.silentmode.invoke("state").then(render); });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
main.js
36
main.js
|
|
@ -1891,6 +1891,30 @@ function seedBundledAddons() {
|
||||||
catch (err) { console.warn(`[addons] seed ${e.name} failed:`, err?.message); }
|
catch (err) { console.warn(`[addons] seed ${e.name} failed:`, err?.message); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- request filter (add-on capability) ----
|
||||||
|
// Chromium allows one onBeforeRequest listener per session, so main owns it
|
||||||
|
// and consults the add-ons' filters. Top-level navigations are never
|
||||||
|
// blocked here — a blocker hides trackers, it does not decide where the
|
||||||
|
// user can go — and only http(s) subresources are offered to filters.
|
||||||
|
const requestFilters = new Map(); // addonId -> (details) => boolean
|
||||||
|
const tabChangeListeners = new Set(); // add-on callbacks for api.tabs.onChange
|
||||||
|
function notifyTabChange() {
|
||||||
|
if (!tabChangeListeners.size) return;
|
||||||
|
const t = activeTab(); const info = t ? { id: t.id, url: t.url || "" } : null;
|
||||||
|
for (const cb of tabChangeListeners) { try { cb(info); } catch (e) { console.warn("[addons] tab listener failed:", e?.message); } }
|
||||||
|
}
|
||||||
|
function installRequestFilter() {
|
||||||
|
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
|
||||||
|
if (!requestFilters.size || !/^https?:/i.test(details.url)) return callback({});
|
||||||
|
let frameUrl = ""; try { frameUrl = (details.frame && details.frame.url) || details.referrer || ""; } catch { frameUrl = details.referrer || ""; }
|
||||||
|
const view = { url: details.url, resourceType: details.resourceType, method: details.method, frameUrl, initiator: details.initiatorOrigin || "", webContentsId: details.webContentsId ?? null };
|
||||||
|
let block = false;
|
||||||
|
for (const [id, fn] of requestFilters) {
|
||||||
|
try { if (fn(view)) { block = true; } } catch (e) { console.warn(`[addons] [${id}] request filter failed:`, e?.message); }
|
||||||
|
}
|
||||||
|
callback(block && details.resourceType !== "mainFrame" ? { cancel: true } : {});
|
||||||
|
});
|
||||||
|
}
|
||||||
function initAddons() {
|
function initAddons() {
|
||||||
// Promote any signed add-on update staged by a previous run BEFORE we
|
// Promote any signed add-on update staged by a previous run BEFORE we
|
||||||
// reseed from the bundle — a fresh install of a newer version from
|
// reseed from the bundle — a fresh install of a newer version from
|
||||||
|
|
@ -1908,6 +1932,14 @@ function initAddons() {
|
||||||
seedBundledAddons();
|
seedBundledAddons();
|
||||||
addonHost = new AddonHost({
|
addonHost = new AddonHost({
|
||||||
addonsDir: addonsUserDir(),
|
addonsDir: addonsUserDir(),
|
||||||
|
// request-filter capability: add-ons register a decision function;
|
||||||
|
// main owns the session's one onBeforeRequest listener (see
|
||||||
|
// installRequestFilter) and asks every registered filter.
|
||||||
|
requestFilter: { set: (id, fn) => requestFilters.set(id, fn), clear: (id) => requestFilters.delete(id) },
|
||||||
|
tabs: {
|
||||||
|
active: () => { const t = activeTab(); if (!t) return null; let wcId = null; try { wcId = t.view.webContents.id; } catch {} return { id: t.id, webContentsId: wcId, url: t.url || "", host: (() => { try { return new URL(t.url).host; } catch { return ""; } })() }; },
|
||||||
|
onChange: (cb) => { tabChangeListeners.add(cb); return () => tabChangeListeners.delete(cb); },
|
||||||
|
},
|
||||||
dataDir: addonsDataDir(),
|
dataDir: addonsDataDir(),
|
||||||
isDisabled: (id) => Array.isArray(settings.disabledAddons) && settings.disabledAddons.includes(id),
|
isDisabled: (id) => Array.isArray(settings.disabledAddons) && settings.disabledAddons.includes(id),
|
||||||
logger: (...a) => console.log("[addons]", ...a),
|
logger: (...a) => console.log("[addons]", ...a),
|
||||||
|
|
@ -2855,6 +2887,7 @@ function setActive(id) {
|
||||||
if (t?.prov) pushNav(t.prov);
|
if (t?.prov) pushNav(t.prov);
|
||||||
chrome.webContents.send("bcnr-offer", t?.bcnrOffer ? { host: t.bcnrOffer.host, tld: t.bcnrOffer.tld, registry: REGISTRY } : null);
|
chrome.webContents.send("bcnr-offer", t?.bcnrOffer ? { host: t.bcnrOffer.host, tld: t.bcnrOffer.tld, registry: REGISTRY } : null);
|
||||||
syncJsDialogVisibility();
|
syncJsDialogVisibility();
|
||||||
|
notifyTabChange();
|
||||||
emitTabs();
|
emitTabs();
|
||||||
}
|
}
|
||||||
function emitTabs() {
|
function emitTabs() {
|
||||||
|
|
@ -3121,6 +3154,8 @@ function createTab(initial, opts = {}) {
|
||||||
wc.on("did-navigate", () => { tab.webapp = null; });
|
wc.on("did-navigate", () => { tab.webapp = null; });
|
||||||
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
||||||
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
||||||
|
wc.on("did-navigate", () => { if (tab.id === activeId) notifyTabChange(); });
|
||||||
|
wc.on("did-navigate-in-page", () => { if (tab.id === activeId) notifyTabChange(); });
|
||||||
// Ctrl+wheel / pinch: Chromium only reports the intent on Windows and
|
// Ctrl+wheel / pinch: Chromium only reports the intent on Windows and
|
||||||
// Linux, the zoom itself is up to us.
|
// Linux, the zoom itself is up to us.
|
||||||
wc.on("zoom-changed", (_e, dir) => zoomStep(tab, dir === "in" ? 1 : -1));
|
wc.on("zoom-changed", (_e, dir) => zoomStep(tab, dir === "in" ? 1 : -1));
|
||||||
|
|
@ -6321,6 +6356,7 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
|
||||||
} catch (err) { console.warn("[bcnr] preload registration failed:", err?.message ?? err); }
|
} catch (err) { console.warn("[bcnr] preload registration failed:", err?.message ?? err); }
|
||||||
protocol.handle("bns", serveBns);
|
protocol.handle("bns", serveBns);
|
||||||
installDownloadTracker();
|
installDownloadTracker();
|
||||||
|
installRequestFilter();
|
||||||
migrateExtensionDirs();
|
migrateExtensionDirs();
|
||||||
initAddons();
|
initAddons();
|
||||||
webapps.init({
|
webapps.init({
|
||||||
|
|
|
||||||
102
package-lock.json
generated
102
package-lock.json
generated
|
|
@ -1,16 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.52",
|
"version": "0.3.57",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.52",
|
"version": "0.3.57",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bch-wc2/interfaces": "^0.0.16",
|
"@bch-wc2/interfaces": "^0.0.16",
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||||
|
"@ghostery/adblocker": "^2.18.2",
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@scure/bip32": "^2.0.1",
|
"@scure/bip32": "^2.0.1",
|
||||||
|
|
@ -501,6 +502,45 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@ghostery/adblocker": {
|
||||||
|
"version": "2.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ghostery/adblocker/-/adblocker-2.18.2.tgz",
|
||||||
|
"integrity": "sha512-x6zEkf1dHSN0t4ExLmuN6nIgH0oAi6laoKe5yGsiIB4jkEvutea1PJgxgRUIjWfNbDRmkf4GjUtuqQFZgvQG1w==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ghostery/adblocker-content": "^2.18.2",
|
||||||
|
"@ghostery/adblocker-extended-selectors": "^2.18.2",
|
||||||
|
"@ghostery/url-parser": "^1.3.1",
|
||||||
|
"@remusao/guess-url-type": "^2.1.0",
|
||||||
|
"@remusao/small": "^2.1.0",
|
||||||
|
"@remusao/smaz": "^2.2.0",
|
||||||
|
"tldts-experimental": "^7.4.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ghostery/adblocker-content": {
|
||||||
|
"version": "2.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ghostery/adblocker-content/-/adblocker-content-2.18.2.tgz",
|
||||||
|
"integrity": "sha512-TOe/b4KOBFEc9OjLG+wvgamGcSTIPSDBAL5YSa8lzjBsoSl8IDnfOjeJDldlLacsC/JEUMLkZIDDNknEolJVqg==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ghostery/adblocker-extended-selectors": "^2.18.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ghostery/adblocker-extended-selectors": {
|
||||||
|
"version": "2.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ghostery/adblocker-extended-selectors/-/adblocker-extended-selectors-2.18.2.tgz",
|
||||||
|
"integrity": "sha512-doKNShBk3+czQL08UkrysHkCPeG/kNPlXvz3DH7SXxD9Y78ovWkEAhjeR+upoan3fomJJuK3dD+IrlofnjckYw==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@ghostery/url-parser": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ghostery/url-parser/-/url-parser-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-QKqGi+7aDQ4RcyHyCwgEk6B9vWnsBP4Q7htaN0zPJV3ATqTKEQDtSTb9c/AN586oJUDs24YXKcwFYwNweY/YjQ==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-experimental": "^7.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
|
@ -761,6 +801,49 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@remusao/guess-url-type": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/guess-url-type/-/guess-url-type-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-zI3dlTUxpjvx2GCxp9nLOSK5yEIqDCpxlAVGwb2Y49RKkS72oeNaxxo+VWS5+XQ5+Mf8Zfp9ZXIlk+G5eoEN8A==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@remusao/small": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/small/-/small-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Y1kyjZp7JU7dXdyOdxHVNfoTr1XLZJTyQP36/esZUU/WRWq9XY0PV2HsE3CsIHuaTf4pvgWv2pvzvnZ//UHIJQ==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@remusao/smaz": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/smaz/-/smaz-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-eSd3Qs0ELP/e7tU1SI5RWXcCn9KjDgvBY+KtWbL4i2QvvHhJOfdIt4v0AA3S5BbLWAr5dCEC7C4LUfogDm6q/Q==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@remusao/smaz-compress": "^2.2.0",
|
||||||
|
"@remusao/smaz-decompress": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@remusao/smaz-compress": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/smaz-compress/-/smaz-compress-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-TXpTPgILRUYOt2rEe0+9PC12xULPvBqeMpmipzB9A7oM4fa9Ztvy9lLYzPTd7tiQEeoNa1pmxihpKfJtsxnM/w==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@remusao/trie": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@remusao/smaz-decompress": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/smaz-decompress/-/smaz-decompress-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ERAPwxPaA0/yg4hkNU7T2S+lnp9jj1sApcQMtOyROvOQyo+Zuh6Hn/oRcXr8mmjlYzyRaC7E6r3mT1nrdHR6pg==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@remusao/trie": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remusao/trie/-/trie-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Er3Q8q0/2OcCJPQYJOPLmCuqO0wu7cav3SPtpjlxSbjFi1x+A1pZkkLD6c9q2rGEkGW/tkrRzfrhNMt8VQjzXg==",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
||||||
|
|
@ -5267,6 +5350,21 @@
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "7.4.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz",
|
||||||
|
"integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tldts-experimental": {
|
||||||
|
"version": "7.4.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-experimental/-/tldts-experimental-7.4.16.tgz",
|
||||||
|
"integrity": "sha512-H1gI3Toi/7PoMRmBIX9K2cyWIEdxjW0F4ygsVRtsCM1thOZwa3zFc8SowUttgd0UhVUR2ahRbgUHEKrxj7WZ3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^7.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"@bch-wc2/interfaces": "^0.0.16",
|
"@bch-wc2/interfaces": "^0.0.16",
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||||
|
"@ghostery/adblocker": "^2.18.2",
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@scure/bip32": "^2.0.1",
|
"@scure/bip32": "^2.0.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue