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
103 lines
4.4 KiB
JavaScript
103 lines
4.4 KiB
JavaScript
// 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 };
|