Sign in with Verae on register/app pages (DEC-657): free anonymous lookup first, new documents stamped with a per-tab key from the shared device sign-in (sessionStorage only), price + balance confirmed; receipt links no longer call stamp; app.html old localStorage login removed; jsPDF/qrcode self-hosted; index snippets send a key

This commit is contained in:
Grok Bot 2026-10-10 13:38:35 -04:00
commit 8a532c6585
8 changed files with 948 additions and 212 deletions

View file

@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/VeraeFullLogo.png">
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js"></script>
<script src="/assets/vendor/jspdf.umd.min.js"></script>
<style>
:root {
--ink: oklch(0.21 0.020 310);
@ -179,24 +179,25 @@
This page hashes files in your browser (SHA-256 WASM or Web Crypto), then
calls the Verae MCP with digests only — file bytes never upload.
• Register = stamp digests via MCP and save a local receipt map in
localStorage (keyed by sha256), similar to ~/.verae/receipts on Mac.
• Register = look each digest up for free; documents already registered
show their original time. New documents need Sign in with Verae (the
same sign-in as the Verae apps); the price and your balance are shown
before anything is charged. Receipts are saved in a local map
(localStorage, keyed by sha256), similar to ~/.verae/receipts on Mac.
• Verify = look up local receipt → MCP verify by receipt_id; or use a
pasted receipt_id. Does NOT stamp. If no receipt is known → Not found.
• Green check = found / valid / registered · Red octagon = not found /
invalid / error. SHA-256 is always shown when hashed.
• Defaults + auth persist in localStorage (verae.client.defaults,
verae.auth). Browser storage is less safe than Keychain — useful for
demos; prefer a short-lived token in shared machines.
• MCP sends Authorization: Bearer &lt;token&gt; when a token is present.
• Sign-in keeps a per-tab Verae key in sessionStorage only (gone when the
tab closes; Sign out revokes it). No password or key is stored in
localStorage or cookies. Lookups and Verify never send the key.
MCP default: https://mcp.verae.ai/mcp
Auth API default: https://api.veraetime.net</pre>
MCP default: https://mcp.verae.ai/mcp</pre>
</details>
</section>
<section class="panel" id="defaults">
<h2>Defaults &amp; auth</h2>
<h2>Defaults &amp; Verae account</h2>
<div class="row">
<label class="field">MCP URL
<input type="text" id="mcpUrl" spellcheck="false" autocomplete="off">
@ -208,34 +209,13 @@ Auth API default: https://api.veraetime.net</pre>
<option value="local">local</option>
</select>
</label>
<label class="field">Auth API URL
<input type="text" id="authApiUrl" spellcheck="false" autocomplete="off">
</label>
</div>
<h3>Auth</h3>
<div class="row">
<label class="field">Username
<input type="text" id="username" autocomplete="username">
</label>
<label class="field">Password
<input type="password" id="password" autocomplete="current-password">
</label>
</div>
<div class="row" style="margin-top:12px">
<label class="field">Access token (JWT)
<textarea id="accessToken" spellcheck="false" placeholder="Paste existing JWT or generate below"></textarea>
</label>
</div>
<p class="meta">Token hint: <span class="token-hint" id="tokenHint">(none)</span>
· Expires: <span id="expiresAt">—</span>
· Session: <span id="sessionSummary">—</span></p>
<div class="btn-row">
<button type="button" id="saveDefaultsBtn">Save</button>
<button type="button" class="secondary" id="refreshTokenBtn">Generate / refresh token</button>
<button type="button" class="secondary" id="checkSessionBtn">Check session</button>
<button type="button" id="saveDefaultsBtn">Save defaults</button>
</div>
<h3>Verae account</h3>
<div id="veraeSignIn"></div>
<p class="status" id="authStatus"></p>
<p class="warn">Warning: username, password, and token are stored in <code>localStorage</code>. That is convenient but less safe than the Mac Keychain / <code>~/.verae/auth.properties</code> (0600). Clear auth on shared browsers.</p>
</section>
<section class="panel">
@ -298,8 +278,9 @@ Auth API default: https://api.veraetime.net</pre>
<span><a href="index.html">Install</a> · <a href="ways.html">Ways to use</a> · <a href="ways.html#testflight">TestFlight</a> · <a href="windows.html">Windows</a> · <a href="register.html">Register docs</a> · <a href="https://api.veraetime.net/docs/swagger/">API docs</a></span>
</footer>
<script type="module">
import QRCodeDefault, { toDataURL as qrToDataURL } from "https://cdn.jsdelivr.net/npm/qrcode@1.5.4/+esm";
import { jsPDF as JsPDFEsm } from "https://cdn.jsdelivr.net/npm/jspdf@2.5.2/+esm";
import QRCodeDefault, { toDataURL as qrToDataURL } from "/assets/vendor/qrcode.esm.js";
import { mountSignIn, currentKey, confirmCharge } from "/assets/verae-signin.js";
const JsPDFEsm = undefined; // jsPDF is self-hosted (UMD, window.jspdf); no third-party scripts on pages that hold a key
const QRCode = {
toDataURL: (typeof QRCodeDefault?.toDataURL === "function")
@ -310,23 +291,16 @@ const QRCode = {
function getJsPDF() {
if (window.jspdf && window.jspdf.jsPDF) return window.jspdf.jsPDF;
if (typeof JsPDFEsm === "function") return JsPDFEsm;
throw new Error("jsPDF failed to load (CDN blocked?). Try a refresh or allow cdn.jsdelivr.net.");
throw new Error("jsPDF failed to load. Try a refresh.");
}
const LS_DEFAULTS = "verae.client.defaults";
const LS_AUTH = "verae.auth";
const LS_AUTH_LEGACY = "verae.auth"; // old username/password/JWT store: removed on load (DEC-657)
const LS_RECEIPTS = "verae.client.receipts";
const $ = (id) => document.getElementById(id);
const mcpUrlEl = $("mcpUrl");
const scopeEl = $("scope");
const authApiUrlEl = $("authApiUrl");
const usernameEl = $("username");
const passwordEl = $("password");
const accessTokenEl = $("accessToken");
const tokenHintEl = $("tokenHint");
const expiresAtEl = $("expiresAt");
const sessionSummaryEl = $("sessionSummary");
const authStatusEl = $("authStatus");
const drop = $("drop");
const fileInput = $("fileInput");
@ -344,7 +318,6 @@ const host = location.hostname || "mcp.verae.ai";
const defaultMcp = (host && host !== "localhost" && host !== "127.0.0.1")
? ("https://" + host + "/mcp")
: "https://mcp.verae.ai/mcp";
const DEFAULT_AUTH_API = "https://api.veraetime.net";
/** @type {"register"|"verify"} */
let mode = "register";
@ -364,49 +337,18 @@ function lookupUrl(sha, receipt) {
}
lookupHint.textContent = publicBase() + "/app.html?sha256=…&receipt=…";
function tokenHint(tok) {
const t = String(tok || "").trim();
if (!t) return "(none)";
if (t.length <= 16) return t.slice(0, 4) + "…";
return t.slice(0, 8) + "…" + t.slice(-6);
}
function loadDefaults() {
let d = {};
try { d = JSON.parse(localStorage.getItem(LS_DEFAULTS) || "{}") || {}; } catch (_) {}
mcpUrlEl.value = d.mcpUrl || defaultMcp;
scopeEl.value = d.scope || "global";
authApiUrlEl.value = d.authApiUrl || DEFAULT_AUTH_API;
try { localStorage.removeItem(LS_AUTH_LEGACY); } catch (_) {}
}
function loadAuth() {
let a = {};
try { a = JSON.parse(localStorage.getItem(LS_AUTH) || "{}") || {}; } catch (_) {}
usernameEl.value = a.username || "";
passwordEl.value = a.password || "";
accessTokenEl.value = a.accessToken || a.access_token || "";
expiresAtEl.textContent = a.expiresAt || a.expires_at || "—";
sessionSummaryEl.textContent = a.sessionSummary || "—";
tokenHintEl.textContent = tokenHint(accessTokenEl.value);
}
function saveDefaultsAndAuth() {
const defaults = {
mcpUrl: mcpUrlEl.value.trim() || defaultMcp,
scope: scopeEl.value || "global",
authApiUrl: authApiUrlEl.value.trim() || DEFAULT_AUTH_API
};
const auth = {
username: usernameEl.value,
password: passwordEl.value,
accessToken: accessTokenEl.value.trim(),
expiresAt: expiresAtEl.textContent === "—" ? "" : expiresAtEl.textContent,
sessionSummary: sessionSummaryEl.textContent === "—" ? "" : sessionSummaryEl.textContent
};
function saveDefaults() {
const defaults = { mcpUrl: mcpUrlEl.value.trim() || defaultMcp, scope: scopeEl.value || "global" };
localStorage.setItem(LS_DEFAULTS, JSON.stringify(defaults));
localStorage.setItem(LS_AUTH, JSON.stringify(auth));
tokenHintEl.textContent = tokenHint(auth.accessToken);
setAuthStatus("Saved defaults + auth to localStorage.", false);
setAuthStatus("Saved defaults (MCP URL and scope only).", false);
}
function setAuthStatus(msg, isErr) {
@ -414,10 +356,6 @@ function setAuthStatus(msg, isErr) {
authStatusEl.className = "status" + (isErr ? " err" : " ok");
}
function getBearer() {
return (accessTokenEl.value || "").trim();
}
function loadReceiptMap() {
try { return JSON.parse(localStorage.getItem(LS_RECEIPTS) || "{}") || {}; }
catch (_) { return {}; }
@ -510,14 +448,13 @@ function rpc(id, method, params) {
return { jsonrpc: "2.0", id, method, params };
}
async function post(url, body, session) {
async function post(url, body, session, key) {
const headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"
};
if (session) headers["Mcp-Session-Id"] = session;
const tok = getBearer();
if (tok) headers["Authorization"] = "Bearer " + tok;
if (key) headers["Authorization"] = "Bearer " + key; // only for stamp (DEC-655/657); lookups stay anonymous
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
const text = await res.text();
const sessionId = res.headers.get("mcp-session-id") || session;
@ -546,25 +483,53 @@ function firstReceipt(payload) {
return null;
}
async function mcpOpen(url) {
async function mcpOpen(url, key) {
const init = await post(url, rpc(1, "initialize", {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "verae-browser-app", version: "1.0.0" }
}));
await post(url, { jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId);
clientInfo: { name: "verae-browser-app", version: "1.1.0" }
}), undefined, key);
await post(url, { jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId, key);
return init.sessionId;
}
function toolError(call) {
const p = call && call.parsed;
if (p && p.error) return p.error.message || JSON.stringify(p.error);
const r = p && p.result;
if (r && r.isError) return (r.content && r.content[0] && r.content[0].text) || "tool error";
return "";
}
/** Free, anonymous lookup (hash_report): the earliest receipt for the digest, or null. Never stamps. */
async function mcpLookup(url, sha, scope, sessionId) {
const sid = sessionId || await mcpOpen(url);
const call = await post(url, rpc(4, "tools/call", {
name: "hash_report",
arguments: { sha256: sha, want_scope: scope }
}), sid);
const err = toolError(call);
if (err) throw new Error(err);
const p = toolPayload(call.parsed);
const list = (p && Array.isArray(p.receipts)) ? p.receipts.filter((r) => r && r.receipt_id) : [];
if (!list.length) return null;
list.sort((a, b) => String(a.created_at || "~").localeCompare(String(b.created_at || "~")));
const r = Object.assign({}, list[0]);
if (!r.hash) r.hash = sha;
if (!r.scope) r.scope = scope;
return r;
}
async function mcpStamp(url, sha, scope) {
const sessionId = await mcpOpen(url);
const key = currentKey();
if (!key) throw new Error("Sign in with Verae to register new documents");
const sessionId = await mcpOpen(url, key);
const call = await post(url, rpc(2, "tools/call", {
name: "stamp",
arguments: { sha256: sha, scope }
}), sessionId);
if (call.parsed && call.parsed.error) {
throw new Error(call.parsed.error.message || JSON.stringify(call.parsed.error));
}
}), sessionId, key);
const err = toolError(call);
if (err) throw new Error(err);
const payload = toolPayload(call.parsed);
const receipt = firstReceipt(payload);
if (!receipt) throw new Error("No receipt in stamp response");
@ -572,7 +537,7 @@ async function mcpStamp(url, sha, scope) {
const v = await post(url, rpc(3, "tools/call", {
name: "verify",
arguments: { receipt_id: receipt.receipt_id, want_scope: scope }
}), sessionId);
}), sessionId, key);
const vp = toolPayload(v.parsed);
const vr = firstReceipt(vp) || (vp && vp.valid !== undefined ? Object.assign({}, receipt, vp) : null);
if (vr) {
@ -722,78 +687,9 @@ clearBtn.addEventListener("click", () => {
setStatus("Cleared.");
});
$("saveDefaultsBtn").addEventListener("click", () => saveDefaultsAndAuth());
$("saveDefaultsBtn").addEventListener("click", () => saveDefaults());
$("refreshTokenBtn").addEventListener("click", async () => {
const base = (authApiUrlEl.value || DEFAULT_AUTH_API).replace(/\/$/, "");
const username = usernameEl.value.trim();
const password = passwordEl.value;
if (!username || !password) {
setAuthStatus("Username and password required to generate a token.", true);
return;
}
setAuthStatus("Logging in…", false);
try {
const res = await fetch(base + "/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ username, password })
});
const text = await res.text();
let parsed = {};
try { parsed = JSON.parse(text); } catch (_) {}
if (!res.ok) {
throw new Error("login HTTP " + res.status + ": " + (parsed.error || text.slice(0, 200)));
}
const token = parsed.token || parsed.access_token || "";
if (!token) throw new Error("login response missing token");
accessTokenEl.value = token;
const exp = parsed.expiresAt || parsed.expires_at || "";
expiresAtEl.textContent = exp || "—";
const user = parsed.user && typeof parsed.user === "object" ? parsed.user : {};
sessionSummaryEl.textContent = (user.username || username) + (user.role ? " · " + user.role : "");
tokenHintEl.textContent = tokenHint(token);
saveDefaultsAndAuth();
setAuthStatus("Token refreshed and saved.", false);
} catch (err) {
setAuthStatus(String(err && err.message ? err.message : err), true);
}
});
$("checkSessionBtn").addEventListener("click", async () => {
const base = (authApiUrlEl.value || DEFAULT_AUTH_API).replace(/\/$/, "");
const tok = getBearer();
if (!tok) {
setAuthStatus("No access token to validate.", true);
return;
}
setAuthStatus("Checking session…", false);
try {
const res = await fetch(base + "/auth/validate", {
method: "GET",
headers: { "Authorization": "Bearer " + tok, "Accept": "application/json" }
});
const text = await res.text();
let parsed = {};
try { parsed = JSON.parse(text); } catch (_) {}
if (!res.ok || !parsed.valid) {
sessionSummaryEl.textContent = "invalid / expired";
setAuthStatus("Session invalid" + (parsed.error ? ": " + parsed.error : "."), true);
return;
}
const line = (parsed.username || "") + (parsed.role ? " · " + parsed.role : "");
sessionSummaryEl.textContent = line || "valid";
if (parsed.expiresAt) expiresAtEl.textContent = parsed.expiresAt;
saveDefaultsAndAuth();
setAuthStatus("Session valid" + (line ? ": " + line : "."), false);
} catch (err) {
setAuthStatus(String(err && err.message ? err.message : err), true);
}
});
accessTokenEl.addEventListener("input", () => {
tokenHintEl.textContent = tokenHint(accessTokenEl.value);
});
const signin = mountSignIn($("veraeSignIn"));
runBtn.addEventListener("click", async () => {
const url = mcpUrlEl.value.trim();
@ -802,6 +698,7 @@ runBtn.addEventListener("click", async () => {
runBtn.disabled = true;
exportBtn.disabled = true;
const pastedRid = (receiptPasteEl.value || "").trim();
const fresh = [];
try {
setStatus("Loading local WASM hasher…");
try { await loadHasher(); } catch (e) {
@ -820,13 +717,18 @@ runBtn.addEventListener("click", async () => {
try {
if (!it.sha256) it.sha256 = await hashFile(it.file);
if (mode === "register") {
setStatus("Registering " + it.name + " (scope=" + scope + ")…");
it.receipt = await mcpStamp(url, it.sha256, scope);
if (!it.receipt.hash) it.receipt.hash = it.sha256;
saveReceiptForSha(it.sha256, it.receipt, it.name);
it.state = "done";
it.kind = "registered";
it.statusLabel = "Registered";
setStatus("Looking up " + it.name + "…");
const found = await mcpLookup(url, it.sha256, scope);
if (found) {
it.receipt = found;
saveReceiptForSha(it.sha256, it.receipt, it.name);
it.state = "done";
it.kind = "found";
it.statusLabel = "Already registered";
} else {
it.state = "queued";
fresh.push(it);
}
} else {
// Verify: never stamp
setStatus("Verifying " + it.name + "…");
@ -866,6 +768,36 @@ runBtn.addEventListener("click", async () => {
}
renderTable();
}
if (mode === "register" && fresh.length) {
setStatus(fresh.length + " new document(s) to register. Sign in with Verae to continue…");
const acct = await signin.ensureSignedIn();
if (!acct) throw new Error("Sign in with Verae to register new documents");
if (!confirmCharge(acct, fresh.length, scope)) {
renderTable();
setStatus("Not registered: " + fresh.length + " new document(s) left unstamped (nothing was charged).");
return;
}
for (const it of fresh) {
it.state = "working";
renderTable();
try {
setStatus("Registering " + it.name + " (scope=" + scope + ")…");
it.receipt = await mcpStamp(url, it.sha256, scope);
if (!it.receipt.hash) it.receipt.hash = it.sha256;
saveReceiptForSha(it.sha256, it.receipt, it.name);
it.state = "done";
it.kind = "registered";
it.statusLabel = "Registered";
} catch (err) {
it.state = "error";
it.kind = "error";
it.statusLabel = "Error";
it.error = String(err && err.message ? err.message : err);
}
renderTable();
}
await signin.refresh();
}
const ok = items.filter((x) => x.state === "done").length;
const nf = items.filter((x) => x.state === "not_found").length;
const bad = items.filter((x) => x.state === "error").length;
@ -1097,12 +1029,17 @@ exportBtn.addEventListener("click", async () => {
items = [fake];
renderTable();
try {
const url = mcpUrlEl.value.trim();
loadDefaults(); // this runs before the bottom-of-script loadDefaults()
const url = mcpUrlEl.value.trim() || defaultMcp;
const scope = scopeEl.value || "global";
let rid = receipt;
if (!rid && sha) {
const local = getLocalReceipt(sha);
rid = (local && local.receipt_id) || "";
if (!rid) {
const found = await mcpLookup(url, sha, scope).catch(() => null); // free lookup, never a stamp
rid = (found && found.receipt_id) || "";
}
}
if (!rid) {
fake.state = "not_found";
@ -1144,7 +1081,6 @@ exportBtn.addEventListener("click", async () => {
})();
loadDefaults();
loadAuth();
setMode("register");
</script>
</body>

7
site/assets/vendor/dijkstrajs.esm.js vendored Normal file
View file

@ -0,0 +1,7 @@
/**
* Bundled by jsDelivr using Rollup v4.62.2 and esbuild v0.28.1.
* Original file: /npm/dijkstrajs@1.0.3/dijkstra.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var c={exports:{}},m;function j(){return m||(m=1,(function(x){var u={single_source_shortest_paths:function(s,r,t){var e={},i={};i[r]=0;var n=u.PriorityQueue.make();n.push(r,0);for(var p,h,o,v,a,d,f,l,y;!n.empty();){p=n.pop(),h=p.value,v=p.cost,a=s[h]||{};for(o in a)a.hasOwnProperty(o)&&(d=a[o],f=v+d,l=i[o],y=typeof i[o]>"u",(y||l>f)&&(i[o]=f,n.push(o,f),e[o]=h))}if(typeof t<"u"&&typeof i[t]>"u"){var k=["Could not find a path from ",r," to ",t,"."].join("");throw new Error(k)}return e},extract_shortest_path_from_predecessor_list:function(s,r){for(var t=[],e=r;e;)t.push(e),s[e],e=s[e];return t.reverse(),t},find_path:function(s,r,t){var e=u.single_source_shortest_paths(s,r,t);return u.extract_shortest_path_from_predecessor_list(e,t)},PriorityQueue:{make:function(s){var r=u.PriorityQueue,t={},e;s=s||{};for(e in r)r.hasOwnProperty(e)&&(t[e]=r[e]);return t.queue=[],t.sorter=s.sorter||r.default_sorter,t},default_sorter:function(s,r){return s.cost-r.cost},push:function(s,r){var t={value:s,cost:r};this.queue.push(t),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};x.exports=u})(c)),c.exports}var _=j(),q=_.PriorityQueue,P=_.extract_shortest_path_from_predecessor_list,w=_.find_path,g=_.single_source_shortest_paths;export{q as PriorityQueue,_ as default,P as extract_shortest_path_from_predecessor_list,w as find_path,g as single_source_shortest_paths};

397
site/assets/vendor/jspdf.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14
site/assets/vendor/qrcode.esm.js vendored Normal file

File diff suppressed because one or more lines are too long

177
site/assets/verae-signin.js Normal file
View file

@ -0,0 +1,177 @@
// 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);
}

View file

@ -427,12 +427,14 @@ curl -sS -D - -X POST https://mcp.verae.ai/mcp \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0.0"}}}'
# 2) notifications/initialized (include Mcp-Session-Id from step 1)
# 3) tools/call stamp
# 3) tools/call stamp (a write: needs your Verae API key and is charged to your account)
curl -sS -X POST https://mcp.verae.ai/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer YOUR_VERAE_API_KEY' \
-H 'Mcp-Session-Id: SESSION' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stamp","arguments":{"sha256":"DIGEST_HEX","scope":"global"}}}'</pre>
<p class="meta"><strong>Stamping needs your own Verae API key</strong> (<code>Authorization: Bearer xVerae-…</code>): get one with <code>verae login</code>, in the <a href="https://zap.verae.ai/portal/">Verae portal</a>, or with <strong>Sign in with Verae</strong> on the <a href="register.html">register page</a>. Each new document is charged to that account; a document you already registered is free. <code>verify</code> and <code>hash_report</code> stay anonymous (rate limited).</p>
<p class="meta">Python SDK shape (same as the official client): <code>Client(url)</code> then <code>await client.call_tool("stamp", {"sha256": digest, "scope": "global"})</code>.</p>
<h3 style="margin-top:36px;font-size:20px;">Embed in a web page (browser JavaScript)</h3>
@ -445,6 +447,7 @@ async function post(body, session) {
"Accept": "application/json, text/event-stream"
};
if (session) headers["Mcp-Session-Id"] = session;
headers["Authorization"] = "Bearer " + VERAE_API_KEY; // stamp needs your key; omit it for verify / hash_report
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
const text = await res.text();
const sessionId = res.headers.get("mcp-session-id") || session;

View file

@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/VeraeFullLogo.png">
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js"></script>
<script src="/assets/vendor/jspdf.umd.min.js"></script>
<style>
:root {
--ink: oklch(0.21 0.020 310);
@ -142,12 +142,18 @@
<h2>How it works</h2>
<ol class="steps">
<li>No assistant required — this is an embedded browser client of the MCP. Files stay on your device; only the SHA-256 digest is sent to <code>/mcp</code>.</li>
<li>Default scope is <strong>global</strong>. Calling <code>stamp</code> is idempotent: an existing certificate is returned with its original time.</li>
<li>Default scope is <strong>global</strong>. Each digest is first looked up for free (no account): a document that is already registered shows its original time and receipt.</li>
<li>Registering a <em>new</em> document needs a Verae account: <strong>Sign in with Verae</strong> (the same sign-in as the Verae apps). The price and your balance are shown before anything is charged. The sign-in key stays in this browser tab only.</li>
<li>Each row shows the original <code>created_at</code>, receipt id (CID), and optional merkle / block hash.</li>
<li>Export selected formats. PDF is a formal <em>Verae Timestamp Registration Document</em> (one page per file) with logo, ledger location, and a QR code that opens the public lookup for that digest.</li>
</ol>
</section>
<section class="panel">
<h2>Verae account</h2>
<div id="veraeSignIn"></div>
</section>
<section class="panel">
<h2>Options</h2>
<div class="row">
@ -210,8 +216,9 @@
<span><a href="index.html">Install &amp; connectors</a> · <a href="app.html">Full browser app</a> · <a href="sample.html">Raw MCP sample</a> · <a href="https://api.veraetime.net/docs/swagger/">API docs</a></span>
</footer>
<script type="module">
import QRCodeDefault, { toDataURL as qrToDataURL } from "https://cdn.jsdelivr.net/npm/qrcode@1.5.4/+esm";
import { jsPDF as JsPDFEsm } from "https://cdn.jsdelivr.net/npm/jspdf@2.5.2/+esm";
import QRCodeDefault, { toDataURL as qrToDataURL } from "/assets/vendor/qrcode.esm.js";
import { mountSignIn, currentKey, confirmCharge } from "/assets/verae-signin.js";
const JsPDFEsm = undefined; // jsPDF is self-hosted (UMD, window.jspdf); no third-party scripts on pages that hold a key
const QRCode = {
toDataURL: (typeof QRCodeDefault?.toDataURL === "function")
@ -222,7 +229,7 @@ const QRCode = {
function getJsPDF() {
if (window.jspdf && window.jspdf.jsPDF) return window.jspdf.jsPDF;
if (typeof JsPDFEsm === "function") return JsPDFEsm;
throw new Error("jsPDF failed to load (CDN blocked?). Try a refresh or allow cdn.jsdelivr.net.");
throw new Error("jsPDF failed to load. Try a refresh.");
}
const $ = (id) => document.getElementById(id);
@ -255,6 +262,7 @@ function lookupUrl(sha, receipt) {
return u.toString();
}
lookupHint.textContent = publicBase() + "/register.html?sha256=…&receipt=…";
const signin = mountSignIn($("veraeSignIn"));
/** @type {{name:string,size:number,file:File,sha256?:string,receipt?:object,error?:string,state:string}[]} */
let items = [];
@ -333,12 +341,13 @@ function rpc(id, method, params) {
return { jsonrpc: "2.0", id, method, params };
}
async function post(url, body, session) {
async function post(url, body, session, key) {
const headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"
};
if (session) headers["Mcp-Session-Id"] = session;
if (key) headers["Authorization"] = "Bearer " + key; // only for stamp (DEC-655/657); lookups stay anonymous
const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
const text = await res.text();
const sessionId = res.headers.get("mcp-session-id") || session;
@ -367,20 +376,54 @@ function firstReceipt(payload) {
return null;
}
async function mcpStamp(url, sha, scope) {
function toolError(call) {
const p = call && call.parsed;
if (p && p.error) return p.error.message || JSON.stringify(p.error);
const r = p && p.result;
if (r && r.isError) return (r.content && r.content[0] && r.content[0].text) || "tool error";
return "";
}
async function mcpInit(url, key, name) {
const init = await post(url, rpc(1, "initialize", {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "verae-register-page", version: "1.0.0" }
}));
await post(url, { jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId);
clientInfo: { name: name || "verae-register-page", version: "1.1.0" }
}), undefined, key);
await post(url, { jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId, key);
return init.sessionId;
}
/** Free, anonymous lookup (hash_report): the earliest receipt for the digest, or null. Never stamps. */
async function mcpLookup(url, sha, scope, sessionId) {
const sid = sessionId || await mcpInit(url, "", "verae-register-lookup");
const call = await post(url, rpc(4, "tools/call", {
name: "hash_report",
arguments: { sha256: sha, want_scope: scope }
}), sid);
const err = toolError(call);
if (err) throw new Error(err);
const p = toolPayload(call.parsed);
const list = (p && Array.isArray(p.receipts)) ? p.receipts.filter((r) => r && r.receipt_id) : [];
if (!list.length) return null;
list.sort((a, b) => String(a.created_at || "~").localeCompare(String(b.created_at || "~")));
const r = Object.assign({}, list[0]);
if (!r.hash) r.hash = sha;
if (!r.scope) r.scope = scope;
return r;
}
async function mcpStamp(url, sha, scope) {
const key = currentKey();
if (!key) throw new Error("Sign in with Verae to register new documents");
const sid = await mcpInit(url, key);
const call = await post(url, rpc(2, "tools/call", {
name: "stamp",
arguments: { sha256: sha, scope }
}), init.sessionId);
if (call.parsed && call.parsed.error) {
throw new Error(call.parsed.error.message || JSON.stringify(call.parsed.error));
}
}), sid, key);
const err = toolError(call);
if (err) throw new Error(err);
const init = { sessionId: sid };
const payload = toolPayload(call.parsed);
const receipt = firstReceipt(payload);
if (!receipt) throw new Error("No receipt in stamp response");
@ -389,7 +432,7 @@ async function mcpStamp(url, sha, scope) {
const v = await post(url, rpc(3, "tools/call", {
name: "verify",
arguments: { receipt_id: receipt.receipt_id, want_scope: scope }
}), init.sessionId);
}), init.sessionId, key);
const vp = toolPayload(v.parsed);
const vr = firstReceipt(vp) || (vp && vp.valid !== undefined ? Object.assign({}, receipt, vp) : null);
if (vr) {
@ -479,30 +522,65 @@ runBtn.addEventListener("click", async () => {
exportBtn.disabled = true;
try {
setStatus("Loading local WASM hasher…");
await loadHasher();
await loadHasher().catch(() => {});
// 1) hash + free lookup (no account needed)
const sid = await mcpInit(url, "", "verae-register-lookup");
const fresh = [];
for (let i = 0; i < items.length; i++) {
const it = items[i];
if (it.state === "done" && it.receipt) continue;
it.state = "working";
it.error = undefined;
renderTable();
setStatus("Hashing " + it.name + " (" + (i + 1) + "/" + items.length + ")…");
try {
it.sha256 = await hashFile(it.file);
setStatus("Stamping " + it.name + " (scope=" + scope + ")…");
it.receipt = await mcpStamp(url, it.sha256, scope);
it.state = "done";
setStatus("Hashing " + it.name + " (" + (i + 1) + "/" + items.length + ")…");
if (!it.sha256) it.sha256 = await hashFile(it.file);
setStatus("Looking up " + it.name + "…");
const found = await mcpLookup(url, it.sha256, scope, sid);
if (found) {
it.receipt = found;
it.state = "done";
} else {
it.state = "queued";
fresh.push(it);
}
} catch (err) {
it.state = "error";
it.error = String(err && err.message ? err.message : err);
}
renderTable();
}
// 2) new documents: signed-in, price and balance confirmed, then stamped with the user's key
if (fresh.length) {
setStatus(fresh.length + " new document(s) to register. Sign in with Verae to continue…");
const acct = await signin.ensureSignedIn();
if (!acct) throw new Error("Sign in with Verae to register new documents");
if (!confirmCharge(acct, fresh.length, scope)) {
for (const it of fresh) { it.state = "queued"; }
renderTable();
setStatus("Not registered: " + fresh.length + " new document(s) left unstamped (nothing was charged).");
return;
}
for (const it of fresh) {
it.state = "working";
renderTable();
try {
setStatus("Registering " + it.name + " (scope=" + scope + ")…");
it.receipt = await mcpStamp(url, it.sha256, scope);
it.state = "done";
} catch (err) {
it.state = "error";
it.error = String(err && err.message ? err.message : err);
}
renderTable();
}
await signin.refresh();
}
const ok = items.filter((x) => x.state === "done").length;
const bad = items.filter((x) => x.state === "error").length;
setStatus("Finished: " + ok + " ok" + (bad ? ", " + bad + " failed" : "") + ". Download exports when ready.");
} catch (err) {
setStatus(String(err), true);
setStatus(String(err && err.message ? err.message : err), true);
} finally {
runBtn.disabled = items.length === 0;
}
@ -730,19 +808,10 @@ exportBtn.addEventListener("click", async () => {
try {
const url = mcpUrlEl.value.trim();
const scope = "global";
const init = await post(url, rpc(1, "initialize", {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "verae-register-lookup", version: "1.0.0" }
}));
await post(url, { jsonrpc: "2.0", method: "notifications/initialized" }, init.sessionId);
const init = { sessionId: await mcpInit(url, "", "verae-register-lookup") };
let rec = null;
if (sha) {
const call = await post(url, rpc(2, "tools/call", {
name: "stamp",
arguments: { sha256: sha, scope }
}), init.sessionId);
rec = firstReceipt(toolPayload(call.parsed));
rec = await mcpLookup(url, sha, scope, init.sessionId); // free lookup, never a stamp
}
if (receipt) {
const v = await post(url, rpc(3, "tools/call", {

133
tests/web/signin-e2e.mjs Normal file
View file

@ -0,0 +1,133 @@
// E2E for DEC-657 against live mcp.verae.ai + zap.verae.ai, test customer cust_verae_test_billing.
// PRE=1 serves the new pages/assets from ../site via request interception (before deploying them).
import puppeteer from "puppeteer-core";
import fs from "fs";
import path from "path";
import crypto from "crypto";
const SITE = path.resolve("../site");
const PRE = process.env.PRE === "1";
const SESSION = process.env.PORTAL_SESSION; // temp portal session for the test customer (deleted afterwards)
const KNOWN = { sha: "e1dcc8e28577df3282d0d3f04038df1ff23fa462b7a6c29a9697e48393a4579c", rid: "QmS6NoQjR42YgRAmZJAwzQ2BQSycL2pKTATfr6dX1X2TTp" };
const out = { checks: [], keys: [] };
const ok = (name, cond, extra) => { out.checks.push({ name, ok: !!cond, extra }); console.log((cond ? "ok " : "FAIL ") + name + (extra ? " :: " + String(extra).slice(0, 200) : "")); if (!cond) process.exitCode = 1; };
const types = { ".html": "text/html", ".js": "text/javascript" };
const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: "new", args: ["--no-sandbox"] });
browser.on("targetcreated", async (t) => { if (t.type() === "page") { const p = await t.page(); if (p && p.url().includes("/portal/device.html")) { out.popup = p.url(); await p.close().catch(() => {}); } } });
async function page(url) {
const p = await browser.newPage();
const ext = [];
if (PRE) {
await p.setRequestInterception(true);
p.on("request", (r) => {
const u = new URL(r.url());
if (u.host === "mcp.verae.ai" && /^\/(register|app|index)\.html$|^\/assets\/(verae-signin\.js|vendor\/.+)$/.test(u.pathname)) {
const f = path.join(SITE, u.pathname);
return r.respond({ status: 200, contentType: types[path.extname(f)] || "application/octet-stream", body: fs.readFileSync(f) });
}
r.continue();
});
}
p.on("request", (r) => { const h = new URL(r.url()).host; if (r.resourceType() === "script" && !/(^|\.)verae\.ai$/.test(h)) ext.push(r.url()); });
p.on("console", (m) => { if (m.type() === "error") console.log(" [console]", m.text().slice(0, 200)); });
p.dialogs = [];
p.on("dialog", async (d) => { p.dialogs.push(d.message()); await d.accept(); });
await p.goto(url, { waitUntil: "networkidle2" });
p.ext = ext;
return p;
}
const status = (p) => p.$eval("#status", (e) => e.textContent);
async function waitStatus(p, re, ms = 180000) {
const t0 = Date.now();
while (Date.now() - t0 < ms) { const s = await status(p); if (re.test(s)) return s; await new Promise((r) => setTimeout(r, 500)); }
return "TIMEOUT: " + (await status(p));
}
async function approveWhenShown(p) {
await p.waitForSelector("#veraeUserCode", { visible: true, timeout: 60000 });
const code = await p.$eval("#veraeUserCode", (e) => e.textContent);
const href = await p.$eval("#veraeSignInLink", (e) => e.href);
ok("sign-in shows code + portal link", /^[A-Z]{4}-[A-Z]{4}$/.test(code) && href.startsWith("https://zap.verae.ai/portal/device.html?user_code="), code + " " + href);
const r = await fetch("https://zap.verae.ai/portal/api/device/approve", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + SESSION }, body: JSON.stringify({ user_code: code }) });
ok("approved in the portal as the test customer", r.ok, r.status);
}
async function storage(p) {
return p.evaluate(() => ({ ss: Object.assign({}, sessionStorage), ls: Object.assign({}, localStorage), cookie: document.cookie }));
}
function tmpFile(tag) {
const f = `/tmp/verae-e2e-${tag}-${Date.now()}.txt`;
fs.writeFileSync(f, `DEC-657 e2e ${tag} ${crypto.randomUUID()}\n`);
return { f, sha: crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex") };
}
// 1) register.html deep link: anonymous lookup, no stamp, no key
let p = await page(`https://mcp.verae.ai/register.html?sha256=${KNOWN.sha}`);
let s = await waitStatus(p, /Lookup complete|Error|error|No receipt/i, 60000);
const row = await p.$eval("#tbody", (e) => e.textContent);
ok("register.html receipt link: anonymous lookup works", /Lookup complete/.test(s) && row.includes(KNOWN.rid), s);
ok("no key used for the lookup", Object.keys((await storage(p)).ss).length === 0);
ok("signed-out hint shown", (await p.$eval("[data-testid=signin-status]", (e) => e.textContent)).includes("needs a Verae account"));
// 2) register a new document: sign in (device flow), price + balance confirmed, stamped with the key
const A = tmpFile("register");
await p.goto("https://mcp.verae.ai/register.html", { waitUntil: "networkidle2" });
await (await p.$("#fileInput")).uploadFile(A.f);
await p.click("#runBtn");
await approveWhenShown(p);
s = await waitStatus(p, /^Finished|Not registered|Error/);
ok("register.html: new document registered", /^Finished: 1 ok/.test(s), s);
ok("price + balance confirmed before charging", p.dialogs.length === 1 && /10 credits each, 10 credits in total/.test(p.dialogs[0]) && /Balance: \d+ credits/.test(p.dialogs[0]), p.dialogs[0]);
let st = await storage(p);
const keyA = st.ss["verae.signin.key"]; out.keys.push(keyA);
ok("key in sessionStorage only", /^xVerae-/.test(keyA || "") && !JSON.stringify(st.ls).includes("xVerae-") && !st.cookie.includes("xVerae"), Object.keys(st.ls).join(","));
ok("account line shows price and balance", /price per new document 10 credits · balance [\d,]+ credits/.test(await p.$eval("[data-testid=signin-status]", (e) => e.textContent)));
const ridA = await p.$eval("#tbody", (e) => e.textContent);
out.newA = A.sha;
// 3) same document again: found by the free lookup, no dialog, no charge
const d0 = p.dialogs.length;
await p.click("#clearBtn");
await (await p.$("#fileInput")).uploadFile(A.f);
await p.click("#runBtn");
s = await waitStatus(p, /^Finished|Error/);
ok("same document again: found, no confirmation, no stamp", /^Finished: 1 ok/.test(s) && p.dialogs.length === d0, s);
// 4) sign out revokes the key
await p.click("#veraeSignOutBtn");
await new Promise((r) => setTimeout(r, 1500));
st = await storage(p);
const who = await fetch("https://zap.verae.ai/v1/whoami", { headers: { authorization: "Bearer " + keyA } });
ok("sign out: key forgotten and revoked", !st.ss["verae.signin.key"] && who.status === 401, who.status);
ok("register.html loads no third-party scripts (fonts CSS only)", p.ext.length === 0, p.ext.join(" "));
await p.close();
// 5) app.html: legacy localStorage credentials are wiped; register flow with its own sign-in
p = await page("https://mcp.verae.ai/app.html");
await p.evaluate(() => localStorage.setItem("verae.auth", JSON.stringify({ username: "x", password: "y", accessToken: "z" })));
await p.reload({ waitUntil: "networkidle2" });
ok("app.html removes the old localStorage username/password/JWT", (await p.evaluate(() => localStorage.getItem("verae.auth"))) === null);
ok("app.html has no password/JWT fields", (await p.$("#password")) === null && (await p.$("#accessToken")) === null);
const B = tmpFile("app");
await (await p.$("#fileInput")).uploadFile(B.f);
await p.click("#runBtn");
await approveWhenShown(p);
s = await waitStatus(p, /^Finished|Not registered|Error/);
ok("app.html: new document registered", /^Finished: 1 ok/.test(s), s);
ok("app.html: price + balance confirmed", p.dialogs.length === 1 && /10 credits each/.test(p.dialogs[0]), p.dialogs[0]);
st = await storage(p);
const keyB = st.ss["verae.signin.key"]; out.keys.push(keyB);
ok("app.html key in sessionStorage only", /^xVerae-/.test(keyB || "") && !JSON.stringify(st.ls).includes("xVerae-") && !st.cookie.includes("xVerae"));
// verify mode stays anonymous
await p.click("#veraeSignOutBtn"); await new Promise((r) => setTimeout(r, 1500));
await p.goto(`https://mcp.verae.ai/app.html?sha256=${B.sha}`, { waitUntil: "networkidle2" });
s = await waitStatus(p, /Lookup complete|Not found|Error|Invalid/i, 60000);
ok("app.html receipt link (signed out): anonymous lookup + verify", /Lookup complete/.test(s), s);
ok("app.html loads no third-party scripts", p.ext.length === 0, p.ext.join(" "));
out.newB = B.sha;
await p.close();
// 6) index.html docs mention the key for stamp
p = await page("https://mcp.verae.ai/index.html");
ok("index.html stamp snippet sends a Verae API key", (await p.content()).includes("Authorization: Bearer YOUR_VERAE_API_KEY"));
await p.close();
await browser.close();
fs.writeFileSync("/tmp/verae-e2e-result.json", JSON.stringify(out, null, 2));
console.log(process.exitCode ? "E2E FAIL" : "E2E OK", "new digests:", out.newA, out.newB);