verae-mcp-timestamping-server/site/assets/verae-signin.js

177 lines
8.6 KiB
JavaScript

// Sign in with Verae for the mcp.verae.ai pages (DEC-657).
//
// Uses the shared Verae sign-in (portal device grant, RFC 8628 - the same flow as `verae login` and the apps):
// POST zap /portal/api/device/authorize -> the user approves the code at zap.verae.ai/portal/device.html
// POST zap /portal/api/device/token (poll) -> an xVerae- API key for this browser tab.
// The key is kept in sessionStorage only (this tab, this origin; gone when the tab closes). Never localStorage,
// never a cookie. Stamps send it as Authorization: Bearer to /mcp and are charged to the signed-in account.
// Verify and lookups never need it.
const ZAP = "https://zap.verae.ai";
const SS_KEY = "verae.signin.key";
export function currentKey() {
try { return sessionStorage.getItem(SS_KEY) || ""; } catch (_) { return ""; }
}
function setKey(k) {
try { k ? sessionStorage.setItem(SS_KEY, k) : sessionStorage.removeItem(SS_KEY); } catch (_) {}
}
async function zap(path, init = {}, key = currentKey()) {
const headers = Object.assign({ "Accept": "application/json" }, init.headers || {});
if (init.body) headers["Content-Type"] = "application/json";
if (key) headers["Authorization"] = "Bearer " + key;
const res = await fetch(ZAP + path, { method: init.method || "GET", headers, body: init.body, credentials: "omit", cache: "no-store" });
let body = {};
try { body = await res.json(); } catch (_) {}
return { status: res.status, ok: res.ok, body };
}
/** Price, balance and account for the signed-in key; null (and the key dropped) when it is no longer valid. */
export async function account() {
const key = currentKey();
if (!key) return null;
const r = await zap("/v1/account");
if (r.status === 401 || r.status === 403) { setKey(""); return null; }
if (!r.ok) throw new Error("Could not read your Verae account (" + r.status + ")");
return r.body;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/**
* Run the device sign-in. onCode({user_code, url}) is called once the code exists so the page can show it with a
* link (a popup opened after an await is often blocked). Resolves with the account, or throws on deny/expiry/cancel.
*/
export async function signIn({ onCode, signal, clientName } = {}) {
const start = await zap("/portal/api/device/authorize", {
method: "POST",
body: JSON.stringify({ client_name: clientName || ("Verae web (" + location.host + ")"), device_name: "Web browser" }),
}, "");
if (!start.ok) throw new Error((start.body && (start.body.error_description || start.body.error)) || ("Sign-in could not start (" + start.status + ")"));
const { device_code, user_code, verification_uri_complete, interval, expires_in } = start.body;
if (onCode) onCode({ user_code, url: verification_uri_complete });
let wait = Math.max(5, Number(interval) || 5) * 1000;
const until = Date.now() + (Number(expires_in) || 600) * 1000;
while (Date.now() < until) {
await sleep(wait);
if (signal && signal.aborted) throw new Error("Sign-in cancelled");
const t = await zap("/portal/api/device/token", {
method: "POST",
body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code }),
}, "");
if (t.ok && t.body.api_key) {
setKey(t.body.api_key);
return account();
}
const e = t.body && t.body.error;
if (e === "authorization_pending") continue;
if (e === "slow_down") { wait = Math.max(wait + 5000, (Number(t.body.interval) || 0) * 1000); continue; }
if (e === "access_denied") throw new Error("Sign-in was denied");
if (e === "expired_token") throw new Error("Sign-in code expired - try again");
throw new Error("Sign-in failed (" + (e || t.status) + ")");
}
throw new Error("Sign-in code expired - try again");
}
/** Revoke this tab's key and forget it. */
export async function signOut() {
const key = currentKey();
setKey("");
if (key) { try { await zap("/v1/tokens/self", { method: "DELETE" }, key); } catch (_) {} }
}
function el(tag, attrs = {}, text) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
if (text !== undefined) e.textContent = text;
return e;
}
/**
* Mount the sign-in box into `host`. Returns { ensureSignedIn(), refresh(), account() }.
* ensureSignedIn() resolves with the account once signed in (starting the sign-in if needed).
*/
export function mountSignIn(host, opts = {}) {
if (!document.getElementById("verae-signin-css")) {
const css = el("style", { id: "verae-signin-css" });
css.textContent = ".verae-signin [hidden]{display:none!important}" +
".verae-code{margin:10px 0;padding:10px 14px;border-radius:10px;background:rgba(127,127,127,.12);display:flex;flex-wrap:wrap;gap:6px;align-items:center}" +
"#veraeUserCode{font-family:ui-monospace,Menlo,monospace;font-size:18px;letter-spacing:.08em}";
document.head.appendChild(css);
}
host.textContent = "";
host.classList.add("verae-signin");
const line = el("p", { class: "meta", "data-testid": "signin-status" });
const btnIn = el("button", { type: "button", id: "veraeSignInBtn" }, "Sign in with Verae");
const btnOut = el("button", { type: "button", class: "secondary", id: "veraeSignOutBtn" }, "Sign out");
const codeBox = el("div", { class: "verae-code", hidden: "" });
const row = el("div", { class: "btn-row" });
row.append(btnIn, btnOut);
host.append(line, codeBox, row);
let acct = null;
let pending = null;
let abort = null;
function fmt(n) { return (typeof n === "number" ? n : 0).toLocaleString() + " credits"; }
function render() {
btnIn.hidden = !!acct;
btnOut.hidden = !acct;
if (acct) {
line.textContent = "Signed in as " + (acct.email || acct.name || acct.customer_id) +
" · price per new document " + (acct.stamp_price_credits == null ? "n/a" : fmt(acct.stamp_price_credits)) +
" · balance " + fmt(acct.balance_credits) +
" · this month " + fmt(acct.period_spend_credits);
} else if (!pending) {
line.textContent = "Checking and looking up documents is free and needs no account. Registering a new document needs a Verae account; it is charged to that account.";
}
if (opts.onChange) opts.onChange(acct);
}
async function refresh() {
try { acct = await account(); } catch (e) { line.textContent = String(e.message || e); }
render();
return acct;
}
function start() {
if (pending) return pending;
abort = new AbortController();
btnIn.disabled = true;
line.textContent = "Starting Verae sign-in…";
pending = signIn({
signal: abort.signal,
onCode: ({ user_code, url }) => {
codeBox.hidden = false;
codeBox.textContent = "";
const a = el("a", { href: url, target: "_blank", rel: "noopener noreferrer", id: "veraeSignInLink" }, "Open the Verae sign-in page");
const cancel = el("button", { type: "button", class: "secondary" }, "Cancel");
cancel.addEventListener("click", () => abort && abort.abort());
codeBox.append(el("span", {}, "Confirm code "), el("strong", { id: "veraeUserCode" }, user_code), el("span", {}, " at zap.verae.ai: "), a, el("span", {}, " "), cancel);
line.textContent = "Waiting for you to approve the sign-in in the Verae portal…";
try { window.open(url, "_blank", "noopener"); } catch (_) {}
},
}).then((a) => { acct = a; return a; }).finally(() => {
pending = null; abort = null; btnIn.disabled = false; codeBox.hidden = true; render();
});
return pending;
}
btnIn.addEventListener("click", () => { start().catch((e) => { line.textContent = String(e.message || e); }); });
btnOut.addEventListener("click", async () => { await signOut(); acct = null; render(); });
refresh();
return {
account: () => acct,
refresh,
async ensureSignedIn() { return acct || (await refresh()) || start(); },
};
}
/** Confirm a charge before stamping. Returns true when the user agrees. */
export function confirmCharge(acct, count, scope = "global") {
const price = scope !== "global" ? 0 : (acct && typeof acct.stamp_price_credits === "number" ? acct.stamp_price_credits : null);
const total = price == null ? null : price * count;
const msg = "Register " + count + " new document" + (count === 1 ? "" : "s") + " with Verae?\n\n" +
(price == null ? "Price: see your Verae plan" : ("Price: " + price + " credits each, " + total + " credits in total")) +
"\nBalance: " + ((acct && acct.balance_credits) || 0) + " credits" +
"\nCharged to: " + ((acct && (acct.email || acct.name || acct.customer_id)) || "your Verae account") +
"\n\nDocuments already registered are not charged again.";
return window.confirm(msg);
}