// Theseus ID — what a project's server checks (DESIGN §4.3, §6.3, §7). // // const v = createVerifier({ crypto, projectId: "hephaestus", // origins: ["https://code.silentmode.st"], providerOrigin: "https://accounts.silentmode.st", // consumeNonce: async (nonce) => true_if_issued_by_me_and_unused }); // const { account, origin } = await v.verifySignIn({ message, signature }); import { parseMessage, checkTime, buildMoveStatement, normOrigin, ID_CHAIN, WALLET_CHAINS } from "./message.mjs"; const bad = (code, msg) => Object.assign(new Error(msg || code), { code }); export function createVerifier({ crypto, projectId, origins = [], providerOrigin = null, consumeNonce, acceptWallets = false, now = () => Date.now() }) { if (!crypto || !projectId || typeof consumeNonce !== "function") throw new Error("createVerifier: crypto, projectId and consumeNonce are required"); const allowed = new Set(origins.map(normOrigin).filter(Boolean)); if (providerOrigin) allowed.add(normOrigin(providerOrigin)); async function check(message, signature, wantKind) { let f; try { f = parseMessage(message); } catch (e) { throw bad("malformed", e.message); } if (f.kind !== wantKind) throw bad("malformed", "wrong message kind"); if (f.projectId !== projectId) throw bad("wrong-project", `signed for ${f.projectId}`); if (!allowed.has(f.origin)) throw bad("wrong-origin", `origin ${f.origin} is not ${projectId}'s`); const late = checkTime(f, now()); if (late) throw bad(late); const isId = f.account.startsWith("tid:"); if (wantKind === "signin") { if (isId ? f.chainId !== ID_CHAIN : !(acceptWallets && WALLET_CHAINS.includes(f.chainId))) throw bad("wrong-chain"); } if (!crypto.verifyAddress(message, signature, f.account)) throw bad("bad-signature"); // Last, so a bad request cannot burn a good nonce. if (!(await consumeNonce(f.nonce, f))) throw bad("bad-nonce", "nonce unknown or already used"); return f; } return { async verifySignIn({ message, signature, move }) { const f = await check(message, signature, "signin"); const out = { account: f.account, projectId: f.projectId, origin: f.origin, kind: f.account.startsWith("tid:") ? "tid" : "wallet" }; if (move) { // §6.3: the new key signs in; a statement signed by old AND new moves the account. const text = buildMoveStatement({ projectId, from: move.from, to: f.account, origin: f.origin, nonce: f.nonce, issuedAt: move.issuedAt }); if (text !== move.message) throw bad("bad-move", "move statement does not match the sign-in"); if (!crypto.verifyAddress(text, move.signatures?.old, move.from)) throw bad("bad-move", "old key did not sign the move"); if (!crypto.verifyAddress(text, move.signatures?.new, f.account)) throw bad("bad-move", "new key did not sign the move"); out.movedFrom = move.from; } return out; }, // §7: { message, signatures: { id, wallet } } — both keys sign the same text. async verifyLink({ message, signatures }) { const f = await check(message, signatures?.id, "link"); if (!crypto.verifyAddress(message, signatures?.wallet, f.wallet)) throw bad("bad-signature", "wallet did not sign the link"); return { account: f.account, wallet: f.wallet, walletChainId: f.walletChainId, projectId: f.projectId, origin: f.origin }; }, }; }