verae-mcp-timestamping-server/site/index.html

500 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Install Verae timestamping</title>
<meta name="description" content="Download a local hasher, connect to the Verae MCP timestamping server, and stamp digests without uploading files.">
<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);
--ease: cubic-bezier(0.16, 1, 0.3, 1);
--ok: oklch(0.55 0.12 145);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
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;
}
.brand img { height: 46px; width: auto; display: block; }
nav { display: flex; gap: 22px; font-size: 14px; font-weight: 600; flex-wrap: wrap; }
nav a { color: var(--ink-soft); text-decoration: none; }
nav a:hover { color: var(--purple); }
.hero {
max-width: 1100px;
margin: 0 auto;
padding: 64px 28px 24px;
}
.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(40px, 6vw, 68px);
line-height: 1.05;
letter-spacing: -0.03em;
margin: 12px 0 18px;
max-width: 18ch;
}
.lede { max-width: 42rem; font-size: 18px; color: var(--ink-soft); margin: 0; }
.wrap { max-width: 1100px; margin: 0 auto; padding: 12px 28px 80px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 36px; }
.card {
background: white;
border: 1px solid var(--hair);
border-radius: 16px;
padding: 22px 22px 18px;
}
.card h2 { font-size: 18px; margin: 0 0 8px; }
.card p { margin: 0; color: var(--ink-soft); font-size: 15px; }
.step { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--purple-mid); }
h2.section {
font-family: Fraunces, Georgia, serif;
font-size: 34px;
font-weight: 560;
margin: 56px 0 8px;
}
.section-note { color: var(--ink-soft); max-width: 44rem; margin-top: 0; }
pre {
background: var(--dark);
color: oklch(0.95 0.01 90);
border-radius: 14px;
padding: 18px 20px;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13.5px;
line-height: 1.5;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
.path { background: var(--purple-wash); padding: 2px 6px; border-radius: 6px; }
ol.steps { padding-left: 1.2rem; max-width: 44rem; }
ol.steps li { margin: 0.55rem 0; }
.gold { height: 3px; width: 72px; background: var(--gold); border: 0; margin: 28px 0; }
.detect {
background: white;
border: 1px solid var(--hair);
border-radius: 18px;
padding: 22px;
margin-top: 22px;
}
.detect .status { font-size: 15px; color: var(--ink-soft); margin: 0 0 14px; }
.detect .status strong { color: var(--ink); }
.btn-row { display: flex; flex-wrap: wrap; gap: 10px; 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: 12px 18px;
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.5; cursor: not-allowed; }
.meta { font-size: 13px; color: var(--ink-mute); margin-top: 12px; }
.ok { color: var(--ok); font-weight: 600; }
table.artifacts {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
font-size: 14px;
}
table.artifacts th, table.artifacts td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--hair);
}
table.artifacts th { color: var(--ink-mute); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
footer {
border-top: 1px solid var(--hair);
padding: 28px;
color: var(--ink-mute);
font-size: 14px;
}
footer .wrap { padding: 0; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
header { padding-top: 18px; }
nav { gap: 14px; }
}
</style>
</head>
<body>
<header>
<a class="brand" href="https://www.verae.com/"><img src="assets/VeraeFullLogo.png" alt="Verae"></a>
<nav>
<a href="#hasher">Hasher</a>
<a href="#connect">Connect</a>
<a href="#register">Register</a>
<a href="ways.html">Ways to use</a>
<a href="developers.html">Developers</a>
<a href="sample.html">Try a stamp</a>
</nav>
</header>
<main>
<section class="hero">
<p class="eyebrow">Model Context Protocol</p>
<h1>Hash locally. Stamp remotely.</h1>
<p class="lede">Download a small <code>verae-sha256</code> binary for your OS (or use WASM on mobile/browser). Digests go to the MCP server. File bytes stay on your machine.</p>
<hr class="gold">
</section>
<div class="wrap">
<section id="hasher">
<h2 class="section">Get the local hasher</h2>
<p class="section-note">No repo clone. This page detects your platform, fetches the matching published binary from <code>/hasher/</code>, and falls back to WASM/JS when a native build is not available.</p>
<div class="detect" id="hasher-panel">
<p class="status" id="hasher-status">Detecting platform…</p>
<div class="btn-row" id="hasher-actions"></div>
<p class="meta" id="hasher-meta"></p>
<pre id="hasher-snippet" hidden></pre>
</div>
<h3 style="margin-top:36px;font-size:18px;">All published binaries</h3>
<table class="artifacts" id="artifact-table">
<thead><tr><th>Target</th><th>File</th><th>Size</th><th>SHA-256</th></tr></thead>
<tbody></tbody>
</table>
<p class="meta">Manifest: <a id="manifest-link" href="/hasher/latest.json">/hasher/latest.json</a> · Source rebuild: <code>./scripts/publish-hasher.sh</code> in the client repo.</p>
</section>
<section id="connect">
<h2 class="section">Connect an MCP client</h2>
<p class="section-note">Point Claude, Cursor, or another MCP client at this host. You do not need to run a local server.</p>
<pre id="mcp-config">{
"mcpServers": {
"verae-timestamping": {
"url": "https://HOST/mcp"
}
}
}</pre>
<p class="section-note">Public endpoints: <code id="mcp-url-primary">https://HOST/mcp</code>
· also <a href="https://mcp.veraetime.net/mcp">mcp.veraetime.net/mcp</a>
· <a href="https://mcp.georgelambert.org/mcp">mcp.georgelambert.org/mcp</a></p>
<h2 class="section">Hash a file, then stamp</h2>
<pre id="hash-flow"># after installing the binary as verae-sha256 on your PATH:
DIGEST=$(verae-sha256 ./document.pdf)
echo "$DIGEST"
# then ask your MCP client to call tool "stamp" with { "sha256": "$DIGEST", "scope": "global" }
# or try the browser sample:
# open sample.html and paste the digest</pre>
</section>
<section id="register">
<h2 class="section">Optional: register on your machine</h2>
<p class="section-note">For a local <code>uvx</code> server (or global stamps that need a VeraeTime account), register once. You still do not clone a repository.</p>
<pre>uvx verae-mcp-timestamping-server setup</pre>
<ol class="steps">
<li>Choose <strong>new</strong> to create a VeraeTime account, or <strong>existing</strong> to log in.</li>
<li>It writes <span class="path">~/.verae/auth.properties</span> with mode <code>600</code>. It does not print the token.</li>
<li>Check later with <code>uvx verae-mcp-timestamping-server session</code>.</li>
</ol>
<div class="grid">
<article class="card">
<p class="step">Claude Desktop</p>
<h2>macOS and Windows</h2>
<p>macOS: <span class="path">~/Library/Application Support/Claude/claude_desktop_config.json</span>. Windows: <span class="path">%APPDATA%\Claude\claude_desktop_config.json</span>.</p>
</article>
<article class="card">
<p class="step">Cursor</p>
<h2>Project or user</h2>
<p>User file: <span class="path">~/.cursor/mcp.json</span>. Project file: <span class="path">.cursor/mcp.json</span>.</p>
</article>
<article class="card">
<p class="step">Local uvx</p>
<h2>Run on this computer</h2>
<p>Use <code>command: uvx</code> / <code>args: ["verae-mcp-timestamping-server"]</code> instead of a remote URL when you want the process on your machine.</p>
</article>
</div>
</section>
<section id="token">
<h2 class="section">Where credentials live</h2>
<div class="grid">
<article class="card">
<p class="step">Your machine</p>
<h2>~/.verae/auth.properties</h2>
<p>Mode 600. Holds the JWT for local/global stamps. Do not commit this file.</p>
</article>
<article class="card">
<p class="step">Your assistant</p>
<h2>OAuth store</h2>
<p>Browser login on the public server keeps tokens in the assistant’s OAuth store, not in mcp.json.</p>
</article>
<article class="card">
<p class="step">Never</p>
<h2>Chat or mcp.json</h2>
<p>Do not paste tokens into a conversation or client config.</p>
</article>
</div>
</section>
</div>
</main>
<footer>
<div class="wrap">
<span>Verae timestamping · <a href="https://www.verae.com/">verae.com</a></span>
<span><a href="sample.html">Try a stamp</a> · <a href="/hasher/latest.json">hasher latest</a> · <a href="https://api.veraetime.net/docs/swagger/">API docs</a></span>
</div>
</footer>
<script>
(function () {
const HASHER_BASE = (location.origin || "https://mcp.georgelambert.org") + "/hasher";
// On veraetime host, hasher artifacts still live on georgelambert CDN-style path.
const ARTIFACT_BASES = [
HASHER_BASE,
"https://mcp.georgelambert.org/hasher"
];
const host = location.hostname || "mcp.georgelambert.org";
const mcpUrl = "https://" + host + "/mcp";
document.getElementById("mcp-config").textContent =
`{
"mcpServers": {
"verae-timestamping": {
"url": "${mcpUrl}"
}
}
}`;
document.getElementById("mcp-url-primary").textContent = mcpUrl;
function detectPlatform() {
const uaData = navigator.userAgentData;
let os = null, arch = null;
if (uaData && uaData.platform) {
const p = uaData.platform.toLowerCase();
if (p.includes("mac")) os = "darwin";
else if (p.includes("win")) os = "windows";
else if (p.includes("linux") || p.includes("chrome os")) os = "linux";
else if (p.includes("android")) os = "android";
else if (p.includes("ios") || p.includes("iphone") || p.includes("ipad")) os = "ios";
}
const ua = navigator.userAgent || "";
const plat = (navigator.platform || "").toLowerCase();
if (!os) {
if (/android/i.test(ua)) os = "android";
else if (/iphone|ipad|ipod/i.test(ua)) os = "ios";
else if (/mac/i.test(plat) || /mac os/i.test(ua)) os = "darwin";
else if (/win/i.test(plat) || /windows/i.test(ua)) os = "windows";
else if (/linux/i.test(plat) || /linux/i.test(ua)) os = "linux";
}
// arch hints
if (uaData && Array.isArray(uaData.architecture)) {
// not standard; ignore
}
if (/aarch64|arm64|windows arm/i.test(ua) || /arm64/i.test(plat)) arch = "aarch64";
else if (/x86_64|win64|wow64|amd64/i.test(ua) || /x86_64|macintel/i.test(plat)) arch = "x86_64";
// Apple Silicon often reports MacIntel in platform historically; prefer arm on modern Macs when possible
if (os === "darwin" && !arch) {
// Safari on Apple Silicon still often says MacIntel; default arm64 for darwin when unknown in 2026
arch = "aarch64";
}
if (os === "windows" && !arch) arch = "x86_64";
if (os === "linux" && !arch) arch = "x86_64";
// Optional URL override: ?libc=musl|gnu on Linux
const params = new URLSearchParams(location.search);
const libcPref = (params.get("libc") || "").toLowerCase();
const wantMusl = libcPref === "musl";
let key = null;
let label = "unknown";
let kind = "wasm";
if (os === "darwin" && arch === "aarch64") { key = "aarch64-apple-darwin"; label = "macOS Apple Silicon"; kind = "native"; }
else if (os === "darwin" && arch === "x86_64") { key = "x86_64-apple-darwin"; label = "macOS Intel"; kind = "native"; }
else if (os === "linux" && arch === "x86_64") {
key = wantMusl ? "x86_64-unknown-linux-musl" : "x86_64-unknown-linux-gnu";
label = wantMusl ? "Linux x86_64 (musl/static)" : "Linux x86_64 (glibc)";
kind = "native";
}
else if (os === "linux" && arch === "aarch64") {
key = wantMusl ? "aarch64-unknown-linux-musl" : "aarch64-unknown-linux-gnu";
label = wantMusl ? "Linux aarch64 (musl/static)" : "Linux aarch64 (glibc)";
kind = "native";
}
else if (os === "windows" && arch === "x86_64") { key = "x86_64-pc-windows-gnu"; label = "Windows x86_64"; kind = "native"; }
else if (os === "windows" && arch === "aarch64") { key = "aarch64-pc-windows-gnullvm"; label = "Windows ARM64"; kind = "native"; }
else if (os === "android" || os === "ios") { label = os === "ios" ? "iOS / iPadOS" : "Android"; kind = "wasm"; }
else { label = (os || "browser") + (arch ? (" / " + arch) : ""); kind = "wasm"; }
return { os, arch, key, label, kind };
}
function fmtSize(n) {
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
return (n / (1024 * 1024)).toFixed(2) + " MB";
}
async function fetchJson(urls) {
let lastErr;
for (const url of urls) {
try {
const res = await fetch(url, { cache: "no-cache" });
if (!res.ok) throw new Error(res.status + " " + url);
return { data: await res.json(), url };
} catch (e) { lastErr = e; }
}
throw lastErr || new Error("fetch failed");
}
function installSnippet(fileUrl, key, isWindows) {
if (isWindows) {
return `# PowerShell
Invoke-WebRequest -Uri "${fileUrl}" -OutFile verae-sha256.exe
Get-FileHash .\\verae-sha256.exe -Algorithm SHA256
.\\verae-sha256.exe .\\document.pdf`;
}
const name = "verae-sha256";
return `# curl
curl -fsSL -o ${name} "${fileUrl}"
chmod +x ${name}
shasum -a 256 ${name} # or: sha256sum ${name}
./${name} ./document.pdf
# wget
wget -O ${name} "${fileUrl}"
chmod +x ${name}
./${name} ./document.pdf`;
}
async function run() {
const status = document.getElementById("hasher-status");
const actions = document.getElementById("hasher-actions");
const meta = document.getElementById("hasher-meta");
const snippet = document.getElementById("hasher-snippet");
const det = detectPlatform();
let latest, manifest, baseUsed;
try {
const latestRes = await fetchJson(ARTIFACT_BASES.map(b => b + "/latest.json"));
latest = latestRes.data;
// Prefer manifest URL from latest, else construct
const manifestUrls = [];
if (latest.manifest) manifestUrls.push(latest.manifest);
for (const b of ARTIFACT_BASES) {
manifestUrls.push(b + "/" + latest.version + "/manifest.json");
}
const manRes = await fetchJson([...new Set(manifestUrls)]);
manifest = manRes.data;
baseUsed = (manifest.base_url || "").replace(/\/$/, "");
} catch (err) {
status.innerHTML = "Could not load hasher manifest: <code>" + String(err) + "</code>";
return;
}
document.getElementById("manifest-link").href = latest.manifest || (baseUsed + "/manifest.json");
document.getElementById("manifest-link").textContent = latest.manifest || (baseUsed + "/manifest.json");
// fill table
const tbody = document.querySelector("#artifact-table tbody");
tbody.innerHTML = "";
const arts = manifest.artifacts || {};
Object.keys(arts).sort().forEach(k => {
const a = arts[k];
const tr = document.createElement("tr");
const url = baseUsed + "/" + a.file;
tr.innerHTML = "<td><code>" + k + "</code></td><td><a href=\"" + url + "\">" + a.file + "</a></td><td>" + fmtSize(a.size) + "</td><td><code style=\"font-size:11px\">" + a.sha256.slice(0, 16) + "…</code></td>";
tbody.appendChild(tr);
});
if (manifest.wasm) {
const tr = document.createElement("tr");
tr.innerHTML = "<td><code>wasm32</code> (browser/mobile)</td><td><a href=\"" + baseUsed + "/" + manifest.wasm.module + "\">" + manifest.wasm.module + "</a> · <a href=\"" + baseUsed + "/" + manifest.wasm.js + "\">" + manifest.wasm.js + "</a></td><td>—</td><td><code style=\"font-size:11px\">" + (manifest.wasm.sha256 || "").slice(0, 16) + "…</code></td>";
tbody.appendChild(tr);
}
const art = det.key ? arts[det.key] : null;
if (det.kind === "native" && art) {
const fileUrl = baseUsed + "/" + art.file;
const isWin = det.key.indexOf("windows") !== -1;
status.innerHTML = "Detected <strong>" + det.label + "</strong> → <code>" + det.key + "</code> · hasher <strong>v" + manifest.version + "</strong>";
const a = document.createElement("a");
a.className = "btn";
a.href = fileUrl;
a.textContent = "Download " + art.file;
actions.appendChild(a);
const copy = document.createElement("button");
copy.className = "btn secondary";
copy.type = "button";
copy.textContent = "Show install commands";
copy.onclick = () => {
snippet.hidden = false;
snippet.textContent = installSnippet(fileUrl, det.key, isWin);
};
actions.appendChild(copy);
meta.innerHTML = "SHA-256 <code>" + art.sha256 + "</code> · " + fmtSize(art.size) + " · verify against <a href=\"" + baseUsed + "/SHA256SUMS\">SHA256SUMS</a>";
snippet.hidden = false;
snippet.textContent = installSnippet(fileUrl, det.key, isWin);
} else {
status.innerHTML = "Detected <strong>" + det.label + "</strong>. No native binary for this environment — use the <strong>WASM/JS</strong> hasher (browser/mobile).";
if (manifest.wasm) {
const wa = document.createElement("a");
wa.className = "btn";
wa.href = baseUsed + "/" + manifest.wasm.module;
wa.textContent = "Download " + manifest.wasm.module;
actions.appendChild(wa);
const ja = document.createElement("a");
ja.className = "btn secondary";
ja.href = baseUsed + "/" + manifest.wasm.js;
ja.textContent = manifest.wasm.js;
actions.appendChild(ja);
const tryBtn = document.createElement("a");
tryBtn.className = "btn secondary";
tryBtn.href = "sample.html";
tryBtn.textContent = "Open stamp sample";
actions.appendChild(tryBtn);
meta.innerHTML = "WASM SHA-256 <code>" + manifest.wasm.sha256 + "</code>. Import <code>" + manifest.wasm.js + "</code> in a page or worker; hash bytes locally, then call MCP <code>stamp</code> with the hex digest only.";
snippet.hidden = false;
snippet.textContent =
`import init, { sha256_hex_js, Sha256Stream } from "${baseUsed}/${manifest.wasm.js}";
await init();
const digest = sha256_hex_js(new TextEncoder().encode("hello"));
// or stream a File via Sha256Stream.update(chunk) / .finish()
// then POST stamp { sha256: digest } to ${mcpUrl}`;
}
}
}
run();
})();
</script>
</body>
</html>