theseus/bcnr-origin.js

104 lines
4.4 KiB
JavaScript
Raw Normal View History

Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins Merges a parallel session's work with the multi-source BNS story from 0.0.7. The dApp side (parallel session) -------------------------------- * bcnr-preload.js — installs `window.bcnr` on every page via contextBridge. Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(), getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All Promises; a missing name returns null (not throw). No signing, no wallet unlock — that surface is designed but deliberately out of scope for 0.0.8 (see TheseusNavigator/DESIGN-integrated-wallet.md). * bcnr-origin.js — pure function that computes the eTLD+1 permission origin for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles .co.uk / .github.io / etc); BNS names key off the on-chain TLD list so foo.wallet becomes a public suffix as soon as `wallet` appears there. Match browser cookie / MetaMask semantics: a grant on pay.merchant.com covers account.merchant.com but not evil.com. * dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O. * main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers. * preload.js + chrome.html — small hooks so the shell picks up window.bcnr the same way regular content does. * package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`. Also included ------------- * AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse picked it up), formalising the source now. * TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled starter (73 beacon txs, root c37b8596…c54e414ba). * Site pages + manifest updated to point at 0.0.8. TheseusNavigator-Setup-0.0.8.exe 95.4 MB 21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02 TheseusNavigator-0.0.8-portable.exe 92.7 MB 2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00
// 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 };