// origin.js — compute the "permission origin" for a page. // // Permissions are keyed off the eTLD+1 of the page's URL, not its full origin: // pay.merchant.com → permission origin merchant.com // blog.merchant.com → permission origin merchant.com (same as pay.) // evil.com → permission origin evil.com (different) // pay.silentmode.bch → permission origin silentmode.bch (BNS root) // // This matches how MetaMask, browser cookies (SameSite), and CORS all think of // origins — the boundary of trust is the registrable domain, not the specific // subdomain. A dApp that already got "always allow signMessage" on // checkout.merchant.com should NOT need to re-approve when it navigates to // account.merchant.com. But evil.com cannot piggyback on merchant.com's grant. // // For ICANN TLDs we use the Public Suffix List via the `psl` package — the // same list Chromium uses — which handles the multi-part cases (.co.uk, // .github.io, ...). For BNS we key off the passed-in bcnrTlds array so a // name like foo.wallet gets treated as a public suffix once the on-chain TLD // list includes "wallet". // // The function is pure: no I/O, no imports of Electron. Tested in isolation // by dev/origin-selftest.mjs. const psl = require("psl"); // Opaque or non-webby origins that never hold permissions. Returned as // literal strings so calling code can compare and treat them uniformly. const OPAQUE = new Set(["data:", "blob:", "javascript:"]); function isIpLiteral(hostname) { // Bracketed IPv6 (URL.hostname strips brackets — the check on ':' inside // catches those) or a bare IPv4. if (!hostname) return false; if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return true; if (hostname.includes(":")) return true; return false; } // Given "pay.silentmode.bch" + bcnrTlds ["bch","wallet"] → "silentmode.bch". // Given "silentmode.bch" alone → "silentmode.bch". A bare TLD ("bch") returns // null — no registrable label to key permissions off. function bnsEtldPlusOne(hostname, bcnrTlds) { const labels = String(hostname).toLowerCase().split(".").filter(Boolean); if (labels.length < 2) return null; const tld = labels[labels.length - 1]; if (!bcnrTlds.includes(tld)) { // Not a BCNR-native TLD — treat the whole hostname as the origin. Safer // than pretending we know the suffix; a name like foo.privateTld will // land here until the TLD list catches up. return labels.join("."); } // BCNR names are one label below the TLD: `{label}.{tld}` is the root // identity, subdomains extend it. Registrable = label + tld. return `${labels[labels.length - 2]}.${tld}`; } // Main entry. Returns a string origin key, or null for un-resolvable input. // Strings intentionally include the scheme when it matters (`about:blank`, // `file://`) so an ICANN eTLD+1 can never collide with a special origin. function originOf(urlString, { bcnrTlds = ["bch"] } = {}) { if (!urlString || typeof urlString !== "string") return null; let u; try { u = new URL(urlString); } catch { return null; } if (OPAQUE.has(u.protocol)) return null; // opaque — never gets permissions if (u.protocol === "about:") { // about:blank, about:srcdoc — normalize to their canonical form. return `about:${u.pathname || "blank"}`; } if (u.protocol === "file:") return "file://"; // one bucket for all local files const host = u.hostname.toLowerCase(); if (!host) return null; // IP literals and localhost: key by scheme + host + port. Common for dev // servers. A permission granted to http://localhost:3000 does NOT extend // to :3001 or to another IP. if (host === "localhost" || isIpLiteral(host)) { const port = u.port ? `:${u.port}` : ""; return `${u.protocol}//${host}${port}`; } if (u.protocol === "bns:") { return bnsEtldPlusOne(host, bcnrTlds); } if (u.protocol === "http:" || u.protocol === "https:") { const parsed = psl.parse(host); if (parsed.error || !parsed.domain) { // Bare TLD, invalid, or a listed public suffix with no registrable // label above it — no permission origin. Fall back to full host so // odd cases don't silently share a bucket. return host; } return parsed.domain; // e.g. merchant.com, foo.co.uk, user.github.io } // Any other scheme (chrome://, chrome-extension://, ...) — key by scheme + // host so it doesn't collide with a real origin. return `${u.protocol}//${host}`; } module.exports = { originOf, bnsEtldPlusOne };