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.
316 lines
16 KiB
HTML
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>
|