verae-mcp-timestamping-server/site/explorer.html
George Lambert 1ca834900e feat: site WIP — downloads, explorer, hasher, TestFlight enroll
Commit restored mcp.verae.ai site pages (app, register, macos, windows,
explorer), hasher/download artifacts, index/sample/hasher updates, ledger
and global_client improvements, NATS stamp helper, and TestFlight enroll
service. Ignore debug-trace/ and editor backups. Standard Readme for
download and enroll docs.
2026-09-29 11:18:59 -04:00

316 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Explorer — verify receipt or file hash · Verae</title>
<meta name="description" content="Verae timestamp explorer: verify a receipt CID or a file SHA-256 (hash locally). Links to the on-chain blockchain explorer for merkle/block proofs.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
<style>
:root {
--ink: oklch(0.21 0.020 310); --ink-soft: oklch(0.42 0.018 310); --ink-mute: oklch(0.60 0.014 310);
--paper: oklch(0.992 0.004 90); --soft: oklch(0.972 0.008 85); --hair: oklch(0.90 0.010 80);
--purple: oklch(0.32 0.110 310); --purple-mid: oklch(0.42 0.130 312); --purple-wash: oklch(0.96 0.020 312);
--gold: oklch(0.80 0.090 85); --dark: oklch(0.19 0.022 312);
--ok: oklch(0.55 0.12 145); --bad: oklch(0.50 0.16 25); --warn: oklch(0.55 0.14 70);
}
* { box-sizing: border-box; }
body { margin: 0; font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; color: var(--ink); background: var(--paper); line-height: 1.55; }
a { color: var(--purple); }
header { display: flex; align-items: center; justify-content: space-between; max-width: 1100px; margin: 0 auto; padding: 28px 28px 0; flex-wrap: wrap; gap: 12px; }
.brand img { height: 46px; width: auto; display: block; }
nav { display: flex; gap: 18px; font-size: 14px; font-weight: 600; flex-wrap: wrap; }
nav a { color: var(--ink-soft); text-decoration: none; }
nav a:hover, nav a.active { color: var(--purple); }
.hero { max-width: 1100px; margin: 0 auto; padding: 40px 28px 8px; }
.eyebrow { font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--purple-mid); }
h1 { font-family: Fraunces, Georgia, serif; font-weight: 560; font-size: clamp(32px, 5vw, 50px); line-height: 1.08; letter-spacing: -0.03em; margin: 12px 0 14px; max-width: 20ch; }
.lede { max-width: 46rem; font-size: 17px; color: var(--ink-soft); margin: 0; }
.wrap { max-width: 1100px; margin: 0 auto; padding: 12px 28px 80px; }
.gold { height: 3px; width: 72px; background: var(--gold); border: 0; margin: 24px 0; }
.grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 18px; }
@media (max-width: 860px) { .grid { grid-template-columns: 1fr; } nav { gap: 12px; } }
.card { background: white; border: 1px solid var(--hair); border-radius: 16px; padding: 22px; }
.card h2 { font-size: 18px; margin: 0 0 8px; }
.card p.note { margin: 0 0 14px; color: var(--ink-soft); font-size: 14px; }
label.field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; font-weight: 600; color: var(--ink-soft); margin-top: 12px; }
input[type=text], textarea {
font: inherit; font-weight: 500; color: var(--ink);
border: 1px solid var(--hair); border-radius: 10px; padding: 10px 12px; background: var(--soft);
}
textarea { min-height: 64px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
.drop {
margin-top: 12px; border: 1.5px dashed var(--hair); border-radius: 12px; padding: 18px;
text-align: center; color: var(--ink-soft); font-size: 14px; background: var(--soft); cursor: pointer;
}
.drop.drag { border-color: var(--purple-mid); background: var(--purple-wash); }
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; align-items: center; }
.btn { display: inline-flex; align-items: center; gap: 8px; background: var(--purple); color: white; text-decoration: none; border: 0; border-radius: 999px; padding: 11px 16px; font: inherit; font-weight: 600; font-size: 14px; cursor: pointer; }
.btn.secondary { background: white; color: var(--purple); border: 1px solid var(--hair); }
.btn:disabled { opacity: 0.55; cursor: wait; }
.meta { font-size: 13px; color: var(--ink-mute); margin-top: 10px; }
pre.out {
background: var(--dark); color: oklch(0.95 0.01 90); border-radius: 14px;
padding: 14px 16px; overflow: auto; font-family: ui-monospace, Menlo, monospace; font-size: 12px; line-height: 1.45; margin: 0; min-height: 120px; white-space: pre-wrap; word-break: break-word;
}
.pill { display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; }
.pill.ok { background: color-mix(in oklch, var(--ok) 18%, white); color: var(--ok); }
.pill.bad { background: color-mix(in oklch, var(--bad) 16%, white); color: var(--bad); }
.pill.warn { background: color-mix(in oklch, var(--warn) 16%, white); color: var(--warn); }
.pill.idle { background: var(--soft); color: var(--ink-mute); }
footer { border-top: 1px solid var(--hair); padding: 28px; color: var(--ink-mute); font-size: 14px; }
footer .inner { max-width: 1100px; margin: 0 auto; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
code { font-family: ui-monospace, Menlo, monospace; font-size: 0.92em; }
h2.section { font-family: Fraunces, Georgia, serif; font-size: 26px; font-weight: 560; margin: 40px 0 8px; }
</style>
</head>
<body>
<header>
<a class="brand" href="https://www.verae.com/"><img src="assets/VeraeFullLogo.png" alt="Verae"></a>
<nav>
<a href="index.html">Install</a>
<a href="ways.html">Ways to use</a>
<a href="explorer.html" class="active">Explorer</a>
<a href="macos.html">macOS</a>
<a href="windows.html">Windows</a>
<a href="app.html">Browser app</a>
<a href="register.html">Register docs</a>
<a href="hasher.html">Hasher</a>
</nav>
</header>
<section class="hero">
<p class="eyebrow">Lookup · digests only</p>
<h1>Verify a receipt or a file hash</h1>
<p class="lede">Public timestamp explorer for Verae. Paste a <strong>receipt CID</strong>, a <strong>SHA-256</strong>, or drop a file (hashed in-browser — bytes never upload). Results come from the MCP at <code id="mcp-label">https://mcp.verae.ai/mcp</code>. On-chain merkle / block proofs open in the <a href="https://explorer.verae.ai/" target="_blank" rel="noopener">Verae blockchain explorer</a>.</p>
<hr class="gold">
</section>
<div class="wrap">
<div class="grid">
<div class="card">
<h2>Lookup</h2>
<p class="note">Provide a receipt id, a hex digest, and/or a file. File bytes stay in this tab.</p>
<label class="field">Receipt id (CID)
<input type="text" id="receipt" placeholder="Qm…" autocomplete="off" spellcheck="false">
</label>
<label class="field">SHA-256 (optional if you drop a file)
<input type="text" id="sha" placeholder="64 hex chars" autocomplete="off" spellcheck="false">
</label>
<div class="drop" id="drop" tabindex="0" role="button" aria-label="Drop a file to hash">Drop a file to hash locally, or click to choose</div>
<input type="file" id="file" hidden>
<p class="meta" id="file-meta"></p>
<div class="btn-row">
<button type="button" class="btn" id="go">Verify</button>
<a class="btn secondary" href="https://explorer.verae.ai/" target="_blank" rel="noopener">Open chain explorer</a>
<a class="btn secondary" href="app.html">Full browser app</a>
</div>
<p class="meta">Deep link: <code>?receipt=…</code> and/or <code>?sha256=…</code></p>
</div>
<div class="card">
<h2>Result <span class="pill idle" id="pill">Idle</span></h2>
<pre class="out" id="out">Awaiting input…</pre>
<div class="btn-row" id="chain-row" hidden>
<a class="btn" id="chain-link" href="https://explorer.verae.ai/" target="_blank" rel="noopener">View merkle / block on chain</a>
</div>
</div>
</div>
<h2 class="section">How this relates to the chain explorer</h2>
<div class="card">
<p class="note" style="margin:0">
<strong>This page</strong> checks VeraeTime timestamp certificates (receipt CIDs) and digests via MCP (<code>verify</code> / <code>hash_report</code>).<br>
<strong><a href="https://explorer.verae.ai/">explorer.verae.ai</a></strong> is the Substrate blockchain explorer (blocks, extrinsics, accounts). When a receipt includes a <code>merkle</code> / block reference (e.g. <code>113072</code>), use <em>View merkle / block on chain</em> to open it there.
</p>
</div>
</div>
<footer>
<div class="inner">
<span>Verae timestamping · <a href="https://www.verae.com/">verae.com</a></span>
<span><a href="ways.html">Ways to use</a> · <a href="explorer.html">Explorer</a> · <a href="https://explorer.verae.ai/">Chain</a> · <a href="macos.html">macOS</a> · <a href="windows.html">Windows</a></span>
</div>
</footer>
<script>
(function () {
const host = location.hostname || "mcp.verae.ai";
const mcpUrl = (location.origin && location.origin.indexOf("http") === 0)
? (location.origin + "/mcp")
: "https://mcp.verae.ai/mcp";
document.getElementById("mcp-label").textContent = mcpUrl;
const receiptEl = document.getElementById("receipt");
const shaEl = document.getElementById("sha");
const drop = document.getElementById("drop");
const fileEl = document.getElementById("file");
const fileMeta = document.getElementById("file-meta");
const go = document.getElementById("go");
const out = document.getElementById("out");
const pill = document.getElementById("pill");
const chainRow = document.getElementById("chain-row");
const chainLink = document.getElementById("chain-link");
function setPill(kind, text) {
pill.className = "pill " + kind;
pill.textContent = text;
}
function setOut(obj) {
out.textContent = typeof obj === "string" ? obj : JSON.stringify(obj, null, 2);
}
function showChain(merkle) {
if (!merkle) { chainRow.hidden = true; return; }
const m = String(merkle).trim();
let href = "https://explorer.verae.ai/";
if (/^\d+$/.test(m)) href = "https://explorer.verae.ai/block/" + encodeURIComponent(m);
else if (/^0x[0-9a-fA-F]+$/.test(m) || /^[0-9a-fA-F]{64}$/.test(m))
href = "https://explorer.verae.ai/search/hash/" + encodeURIComponent(m);
chainLink.href = href;
chainLink.textContent = "View merkle / block on chain (" + m + ")";
chainRow.hidden = false;
}
async function sha256Hex(buf) {
const dig = await crypto.subtle.digest("SHA-256", buf);
return [...new Uint8Array(dig)].map(b => b.toString(16).padStart(2, "0")).join("");
}
async function mcpSession() {
const initBody = {
jsonrpc: "2.0", id: 1, method: "initialize",
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "verae-explorer", version: "1" } }
};
const r = await fetch(mcpUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream" },
body: JSON.stringify(initBody)
});
const sid = r.headers.get("Mcp-Session-Id");
await r.text();
await fetch(mcpUrl, {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", "Mcp-Session-Id": sid || "" },
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
});
return sid;
}
async function mcpCall(sid, name, args, id) {
const r = await fetch(mcpUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Mcp-Session-Id": sid || ""
},
body: JSON.stringify({ jsonrpc: "2.0", id: id, method: "tools/call", params: { name: name, arguments: args } })
});
const text = await r.text();
// Handle SSE or plain JSON
let parsed = null;
try { parsed = JSON.parse(text); }
catch (_) {
const lines = text.split("\n").filter(l => l.startsWith("data:"));
for (const line of lines) {
try { parsed = JSON.parse(line.slice(5).trim()); break; } catch (_) {}
}
}
if (!parsed) throw new Error("Bad MCP response");
if (parsed.error) throw new Error(parsed.error.message || JSON.stringify(parsed.error));
const content = parsed.result && parsed.result.content;
if (Array.isArray(content) && content[0] && content[0].text) {
try { return JSON.parse(content[0].text); } catch (_) { return { raw: content[0].text }; }
}
return (parsed.result && parsed.result.structuredContent) || parsed.result || parsed;
}
async function hashFile(file) {
const buf = await file.arrayBuffer();
const hex = await sha256Hex(buf);
shaEl.value = hex;
fileMeta.textContent = file.name + " · " + file.size + " bytes · SHA-256 " + hex;
return hex;
}
drop.addEventListener("click", () => fileEl.click());
drop.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") fileEl.click(); });
["dragenter","dragover"].forEach(ev => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add("drag"); }));
["dragleave","drop"].forEach(ev => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove("drag"); }));
drop.addEventListener("drop", async (e) => {
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (f) await hashFile(f);
});
fileEl.addEventListener("change", async () => {
if (fileEl.files && fileEl.files[0]) await hashFile(fileEl.files[0]);
});
async function run() {
go.disabled = true;
setPill("warn", "Working");
setOut("Contacting MCP…");
chainRow.hidden = true;
try {
let receipt = (receiptEl.value || "").trim();
let sha = (shaEl.value || "").trim().toLowerCase();
if (sha && !/^[0-9a-f]{64}$/.test(sha)) throw new Error("SHA-256 must be 64 hex characters");
if (!receipt && !sha) throw new Error("Enter a receipt id, a SHA-256, or drop a file");
const sid = await mcpSession();
const result = { mcp: mcpUrl, receipt_id: receipt || null, sha256: sha || null };
if (receipt) {
const v = await mcpCall(sid, "verify", { receipt_id: receipt, want_scope: "global" }, 2);
result.verify = v;
const rec = (v && v.receipt) || v || {};
if (!sha && rec.hash) { sha = String(rec.hash).toLowerCase(); result.sha256 = sha; shaEl.value = sha; }
showChain(rec.merkle || (v && v.merkle));
if (v && v.valid === true) setPill("ok", "Valid receipt");
else if (v && v.valid === false) setPill("bad", "Invalid");
else setPill("warn", "Checked");
}
if (sha) {
const hr = await mcpCall(sid, "hash_report", { sha256: sha, want_scope: "global" }, 3);
result.hash_report = hr;
const list = (hr && hr.receipts) || (Array.isArray(hr) ? hr : []);
if (!receipt && list.length) {
const first = list[0];
receipt = first.receipt_id || receipt;
if (receipt) receiptEl.value = receipt;
showChain(first.merkle);
// also verify that receipt for full fields
const v2 = await mcpCall(sid, "verify", { receipt_id: receipt, want_scope: "global" }, 4);
result.verify = v2;
if (v2 && v2.valid === true) setPill("ok", "Found via hash");
} else if (!receipt && !list.length) {
if (!result.verify) setPill("warn", "No hash history");
result.note = "No receipts for this digest in VeraeTime Carfax (GET /api/hash). If you have a receipt CID, paste it above — verify by receipt still works.";
} else if (list.length) {
if (!result.verify || result.verify.valid !== true) setPill("ok", "Found via hash");
}
}
setOut(result);
if (result.verify && result.verify.valid === true && pill.textContent === "Checked") setPill("ok", "Valid");
} catch (err) {
setPill("bad", "Error");
setOut(String(err && err.message ? err.message : err));
} finally {
go.disabled = false;
}
}
go.addEventListener("click", run);
// Deep-link
const q = new URLSearchParams(location.search);
if (q.get("receipt")) receiptEl.value = q.get("receipt").trim();
if (q.get("sha256")) shaEl.value = q.get("sha256").trim().toLowerCase();
if (receiptEl.value || shaEl.value) run();
})();
</script>
</body>
</html>