// window.theseusId — Theseus ID for web pages (DESIGN-theseus-id.md §5.1). // // const r = await theseusId.signIn({ projectId: "hephaestus", nonce }); // // r = { v, projectId, origin, account: "tid:q…", message, signature, scheme: "bip137", move? } // // send { message, signature, move } to your server; verify with TheseusID/lib/verify.mjs // // Registered session-wide. The page passes fields, never message text; // Theseus writes the message, takes the origin from the frame it committed, // and answers only the top frame of a web tab. Failures reject with an Error // whose `code` is one of: no-vault, locked, denied, origin-not-listed, // origin-list-unavailable, bad-request, busy, not-top-frame, error. const { contextBridge, ipcRenderer } = require("electron"); if (/^(https?|bns):$/.test(location.protocol)) { // Errors lose custom properties crossing the bridge, so the code rides in // the message as "[code] text" and is copied back onto the Error here, in // the page's world. const call = async (channel, payload) => { const r = await ipcRenderer.invoke(channel, payload); if (r && r.ok) return r.result; const code = (r && r.error && r.error.code) || "error"; throw new Error(`[${code}] ${(r && r.error && r.error.message) || "Theseus ID failed"}`); }; const str = (v, max) => (v == null ? undefined : String(v).slice(0, max)); // A locked vault is only unlocked for a page the user just clicked or // typed in. navigator.userActivation is not enough: a page Theseus opens // with loadURL starts out "activated", so it could pop the vault prompt on // load. These listeners run in the preload's world and count only trusted // events, which a page cannot synthesise. let lastInput = 0; for (const type of ["pointerdown", "keydown"]) { window.addEventListener(type, (e) => { if (e.isTrusted) lastInput = Date.now(); }, true); } const recentInput = () => Date.now() - lastInput < 5000; const api = { version: 1, signIn: (opts = {}) => call("theseus-id:signIn", { projectId: str(opts.projectId, 300) ?? "", nonce: str(opts.nonce, 200) ?? "", statement: str(opts.statement, 200), requestId: str(opts.requestId, 64), resources: Array.isArray(opts.resources) ? opts.resources.slice(0, 8).map((x) => String(x).slice(0, 2048)) : undefined, expiresIn: Number.isFinite(Number(opts.expiresIn)) ? Number(opts.expiresIn) : undefined, // Read here, in the preload's world, so a page cannot claim a click it did not get. gesture: recentInput() && !!(navigator.userActivation && navigator.userActivation.isActive), }), // After the project's server accepted a move proof (§6.3). moved: (opts = {}) => call("theseus-id:moved", { projectId: str(opts.projectId, 300) ?? "", account: str(opts.account, 100) ?? "" }), }; try { contextBridge.exposeInMainWorld("theseusIdBridge", api); } catch {} // A thin wrapper in the page's own world turns "[code] text" into err.code. try { const { webFrame } = require("electron"); webFrame.executeJavaScript(`(() => { const b = window.theseusIdBridge; if (!b || window.theseusId) return; const wrap = (fn) => (...a) => fn(...a).catch((e) => { const m = /^\\[([a-z-]+)\\] ([\\s\\S]*)$/.exec(String(e && e.message || "")); const err = new Error(m ? m[2] : String(e && e.message || e)); err.code = m ? m[1] : "error"; throw err; }); Object.defineProperty(window, "theseusId", { value: Object.freeze({ version: b.version, signIn: wrap(b.signIn), moved: wrap(b.moved) }), enumerable: true }); try { delete window.theseusIdBridge; } catch {} })()`); } catch {} }