67 lines
3.6 KiB
JavaScript
67 lines
3.6 KiB
JavaScript
|
|
// 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 {}
|
||
|
|
}
|