- certificate.html brand kit preview with multihash when present - PACKAGING.md CERT section residual
413 lines
19 KiB
HTML
413 lines
19 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||
<title>Verae Timestamp Certificate · CERT-01 · PrintIt</title>
|
||
<meta name="description" content="CERT-01 branded timestamp certificate viewer for PrintIt. Digests only — final receipt is evidence."/>
|
||
<link rel="canonical" href="https://printit.verae.ai/certificate.html"/>
|
||
<link rel="stylesheet" href="brand/tokens.css"/>
|
||
<link rel="stylesheet" href="verae-ui-kit.css"/>
|
||
<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,600;9..144,700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet"/>
|
||
<style>
|
||
body{margin:0;background:var(--canvas,#E9E6EE);color:var(--ink,#1D1534);font-family:var(--font-sans),system-ui,-apple-system,Segoe UI,sans-serif}
|
||
.toolbar{padding:12px 20px;background:var(--card,#fff);border-bottom:1px solid var(--line,#E3DEF0);display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||
.toolbar b{color:var(--plum,#531B59)}
|
||
.toolbar .help{font-size:12px;color:var(--muted,#5E5470)}
|
||
.toolbar .spacer{flex:1}
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;font-weight:600;font-size:13px;border-radius:8px;padding:0 14px;height:36px;border:1px solid transparent;text-decoration:none;cursor:pointer;font-family:inherit}
|
||
.btn.secondary{background:var(--card,#fff);color:var(--plum,#531B59);border-color:#CDBFD4}
|
||
.btn.primary{background:var(--plum,#531B59);color:#fff}
|
||
.btn.tonal{background:var(--plum-100,#EBD9EE);color:var(--plum-d,#3E1343);border:0}
|
||
.wrap{max-width:900px;margin:0 auto;padding:24px 16px 48px}
|
||
/* CERT-01 paper sheet — plum outer + gold inner frame */
|
||
.sheet{
|
||
background:#fff;position:relative;border-radius:4px;
|
||
box-shadow:0 12px 40px rgba(30,10,40,.18),0 2px 8px rgba(30,10,40,.08);
|
||
padding:48px 56px 56px;min-height:720px;
|
||
box-shadow:
|
||
0 12px 40px rgba(30,10,40,.18),
|
||
inset 0 0 0 1.5px var(--plum,#531B59),
|
||
inset 0 0 0 6px #fff,
|
||
inset 0 0 0 6.5px var(--gold,#C4A85A);
|
||
}
|
||
.head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
|
||
.logo-row{display:flex;align-items:center;gap:10px}
|
||
.logo-row img{height:36px;width:auto;display:block}
|
||
.mark{width:36px;height:36px;border-radius:9px;background:linear-gradient(140deg,var(--plum,#531B59),var(--violet,#6B3FD6));display:grid;place-items:center;color:#fff;font-weight:800;font-size:13px;flex:none}
|
||
.cert-meta{text-align:right;font-size:11px;color:var(--muted,#5E5470);line-height:1.45}
|
||
.title{
|
||
text-align:center;margin:28px 0 0;
|
||
font-family:var(--font-serif),Fraunces,Georgia,serif;
|
||
font-weight:700;font-size:clamp(22px,3.2vw,26px);line-height:1.25;letter-spacing:-0.01em;color:var(--ink,#1D1534);
|
||
}
|
||
.subtitle{text-align:center;font-size:12.5px;color:var(--muted,#5E5470);margin:8px auto 0;max-width:34rem;line-height:1.45}
|
||
.rule{height:2px;background:var(--gold,#C4A85A);width:120px;margin:14px auto 0;border:0}
|
||
.banner{
|
||
margin-top:20px;border:2px solid var(--ok,#0E6B3A);background:var(--ok-bg,#E3F5EA);
|
||
border-radius:10px;padding:12px 16px;display:flex;gap:12px;align-items:center;
|
||
}
|
||
.banner.provisional{border-color:var(--pend-bd,#D5DAE1);background:var(--pend-bg,#EEF0F3)}
|
||
.banner .check{font-size:28px;line-height:1;color:var(--ok,#0E6B3A);font-weight:700}
|
||
.banner.provisional .check{color:var(--pend,#475467)}
|
||
.banner .bt{font:700 15px/1.3 var(--font-sans),system-ui;color:var(--ok,#0E6B3A)}
|
||
.banner.provisional .bt{color:var(--pend,#475467)}
|
||
.banner .bs{font-size:12px;color:#1D4D33;margin-top:2px}
|
||
.banner.provisional .bs{color:var(--muted,#5E5470)}
|
||
.kv{display:grid;grid-template-columns:150px 1fr;gap:10px 16px;margin-top:22px;font-size:13px}
|
||
.kv dt{color:var(--muted,#5E5470);font-weight:600;font-size:12px;padding-top:2px}
|
||
.kv dd{margin:0;word-break:break-all;line-height:1.45}
|
||
.mono{font-family:var(--font-mono),'JetBrains Mono',ui-monospace,SF Mono,Menlo,monospace;font-size:11px;letter-spacing:-0.01em}
|
||
.mh{display:inline-flex;align-items:flex-start;gap:4px;flex-wrap:wrap;background:var(--surface-2,#F4F1F8);border:1px solid var(--line,#E3DEF0);border-radius:6px;padding:4px 8px}
|
||
.mh .pfx{color:var(--plum,#531B59);font-weight:600}
|
||
.verify-box{
|
||
display:flex;gap:18px;align-items:flex-start;margin-top:24px;padding:16px;
|
||
border:1px solid var(--line,#E3DEF0);border-radius:10px;background:var(--bg,#FBFAFD);
|
||
}
|
||
.qr{
|
||
width:112px;height:112px;flex:none;border:2px solid var(--ink,#1D1534);border-radius:8px;
|
||
background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath fill='%231D1534' d='M1 1h7v7H1zm2 2v3h3V3zm10 0h7v7h-7zm2 2v3h3V3zM1 13h7v7H1zm2 2v3h3v-3zm18-14h7v7h-7zm2 2v3h3V3zm0 10h3v2h-3zm-4 0h2v2h-2zm4 4h3v3h-3zm-4 2h2v3h-2zm-2-4h2v2h-2zm6 6h3v2h-3zm-8 0h2v2h-2zm4-2h2v4h-2zm-6 0h2v2h-2z'/%3E%3C/svg%3E") center/72% no-repeat;
|
||
position:relative;
|
||
}
|
||
.qr span{position:absolute;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59)}
|
||
.verify-box ol{margin:6px 0 0 16px;padding:0;line-height:1.6;color:var(--ink-soft,#3B3250);font-size:12.5px}
|
||
.verify-box a{color:var(--info,#3B2A8C);font-size:10px;word-break:break-all}
|
||
.foot{
|
||
margin-top:28px;padding-top:14px;border-top:1px solid var(--line,#E3DEF0);
|
||
font-size:10px;color:var(--muted,#5E5470);line-height:1.5;
|
||
}
|
||
.load{margin-top:18px;padding:14px 16px;background:var(--card,#fff);border:1px solid var(--line,#E3DEF0);border-radius:12px}
|
||
.load label{display:block;font-size:12px;font-weight:600;color:var(--muted,#5E5470);margin-bottom:6px}
|
||
.load input[type=file]{font-size:13px}
|
||
.note{font-size:12px;color:var(--muted,#5E5470);margin:8px 0 0;line-height:1.4}
|
||
.hidden{display:none!important}
|
||
@media print{
|
||
.toolbar,.load,.no-print{display:none!important}
|
||
html,body{background:#fff!important;margin:0;padding:0}
|
||
.wrap{padding:0;max-width:none}
|
||
.sheet{
|
||
box-shadow:none!important;
|
||
border:2px solid #531B59;
|
||
outline:1px solid #C4A85A;outline-offset:-5px;
|
||
margin:0;border-radius:0;min-height:0;padding:28px 36px 36px;
|
||
-webkit-print-color-adjust:exact;print-color-adjust:exact;
|
||
}
|
||
.rule,.banner,.mh,.qr{ -webkit-print-color-adjust:exact;print-color-adjust:exact }
|
||
.rule{background:#C4A85A!important}
|
||
a{color:inherit;text-decoration:none}
|
||
}
|
||
@page{size:letter;margin:12mm}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="toolbar no-print">
|
||
<b>CERT-01</b>
|
||
<span class="help">PrintIt · Verae Timestamp Certificate (web preview)</span>
|
||
<span class="spacer"></span>
|
||
<button class="btn secondary" type="button" id="btn-sample">Sample data</button>
|
||
<button class="btn primary" type="button" onclick="window.print()">Print</button>
|
||
<a class="btn secondary" href="index.html">printit.verae.ai</a>
|
||
<a class="btn secondary" href="queue.html">Queue</a>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
<article class="sheet" id="cert" aria-label="Verae Timestamp Certificate">
|
||
<div class="head">
|
||
<div class="logo-row">
|
||
<img src="brand/logo/verae-mark.svg" alt="Verae" width="36" height="36"
|
||
onerror="this.style.display='none';this.nextElementSibling.style.display='grid'"/>
|
||
<span class="mark" style="display:none" aria-hidden="true">V</span>
|
||
<span style="font-weight:700;font-size:14px;color:var(--plum,#531B59)">Verae</span>
|
||
</div>
|
||
<div class="cert-meta">
|
||
<div id="cert-no">Certificate preview</div>
|
||
<div>verify.verae.ai · explorer.verae.ai/time</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h1 class="title">Verae Timestamp Certificate</h1>
|
||
<p class="subtitle">Proof that this exact document existed at the time below, recorded on the Verae Substrate blockchain</p>
|
||
<hr class="rule"/>
|
||
|
||
<div class="banner" id="banner" role="status">
|
||
<div class="check" aria-hidden="true">✓</div>
|
||
<div>
|
||
<div class="bt" id="banner-title">FINAL — Substrate block finalized</div>
|
||
<div class="bs" id="banner-sub">GRANDPA-finalized on VeraeNet · digests only</div>
|
||
</div>
|
||
</div>
|
||
|
||
<dl class="kv" id="fields">
|
||
<dt>Document</dt>
|
||
<dd id="f-doc">—</dd>
|
||
<dt id="l-mh">Content multihash</dt>
|
||
<dd id="f-mh"><span class="mono">—</span></dd>
|
||
<dt>SHA-256 (hex)</dt>
|
||
<dd id="f-sha"><span class="mono">—</span></dd>
|
||
<dt id="l-b3" class="hidden">BLAKE3 (hex)</dt>
|
||
<dd id="f-b3" class="hidden">—</dd>
|
||
<dt id="l-b3mh" class="hidden">BLAKE3 multihash</dt>
|
||
<dd id="f-b3mh" class="hidden">—</dd>
|
||
<dt>Server time</dt>
|
||
<dd id="f-time">—</dd>
|
||
<dt>VeraeTime receipt</dt>
|
||
<dd id="f-receipt"><span class="mono">—</span></dd>
|
||
<dt>Ledger / merkle</dt>
|
||
<dd id="f-merkle">—</dd>
|
||
<dt>Issued by</dt>
|
||
<dd id="f-issued">Verae Print Stamp (printit)</dd>
|
||
</dl>
|
||
|
||
<div class="verify-box">
|
||
<div class="qr" aria-hidden="true"><span>Verae</span></div>
|
||
<div>
|
||
<b style="font-size:12.5px">Verify this certificate</b>
|
||
<ol>
|
||
<li>Open the explorer link (or scan the QR on a printed PDF).</li>
|
||
<li>Compare the local file’s SHA-256 to the multihash below — file never uploaded.</li>
|
||
<li>Only a <b>final</b> / already-registered receipt is evidence. Hold ids are not proof.</li>
|
||
</ol>
|
||
<div style="margin-top:8px"><a id="verify-link" href="https://explorer.verae.ai/time" target="_blank" rel="noopener">https://explorer.verae.ai/time</a></div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="foot" id="footer-note">
|
||
Only the document’s SHA-256 digest was submitted; the file was not uploaded.
|
||
Hashes are multihash (sha2-256, code 0x12, length 0x20 → <span class="mono">f1220</span>…).
|
||
Hold / pending / jobId values are never evidence.
|
||
Verae LLC · verae.ai · explorer.verae.ai
|
||
</p>
|
||
</article>
|
||
|
||
<div class="load no-print">
|
||
<label for="sidecar">Load a PrintIt <code>.verae.json</code> sidecar (optional)</label>
|
||
<input type="file" id="sidecar" accept=".json,application/json"/>
|
||
<p class="note">
|
||
Query params also work:
|
||
<span class="mono">?document=&sha256=&receipt=&created_at=&merkle=&blake3=&block=&helper_version=</span>
|
||
Multihash / BLAKE3 lines appear <b>only when data is present</b> — never invented.
|
||
Full CERT-01.html Fraunces pixel match is still a design residual; this page uses brand tokens + plum/gold frame.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var SAMPLE = {
|
||
document_name: 'Purchase-Order-7781.pdf',
|
||
sha256: '20515740e1051f69c552b83ff1a919398feca768090930a4a502f9335273f279',
|
||
multihash: 'f122020515740e1051f69c552b83ff1a919398feca768090930a4a502f9335273f279',
|
||
receipt_id: 'bafkreihq3examplev7xq',
|
||
created_at: '2026-10-09T05:20:28Z',
|
||
merkle: 'Block #1,284,530 · extrinsic 1284530-1',
|
||
block: '1284530',
|
||
helper_version: '0.3.6',
|
||
status: 'final',
|
||
byte_length: 184220
|
||
};
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
function isHex64(s) {
|
||
return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s.replace(/^0x/i, ''));
|
||
}
|
||
|
||
function cleanHex(s) {
|
||
if (!s) return '';
|
||
s = String(s).trim().replace(/^0x/i, '').toLowerCase();
|
||
return isHex64(s) ? s : '';
|
||
}
|
||
|
||
function contentMultihash(sha) {
|
||
var h = cleanHex(sha);
|
||
return h ? 'f1220' + h : '';
|
||
}
|
||
|
||
function blake3Multihash(b3) {
|
||
var h = cleanHex(b3);
|
||
return h ? 'f1e20' + h : '';
|
||
}
|
||
|
||
function fmtBytes(n) {
|
||
if (n == null || n === '' || isNaN(Number(n))) return '';
|
||
n = Number(n);
|
||
return n.toLocaleString('en-US') + ' bytes';
|
||
}
|
||
|
||
function fmtBlock(n) {
|
||
if (n == null || n === '') return '';
|
||
var s = String(n).replace(/,/g, '');
|
||
if (!/^\d+$/.test(s)) return String(n);
|
||
return Number(s).toLocaleString('en-US');
|
||
}
|
||
|
||
function setRow(labelId, valueId, show, html) {
|
||
var l = labelId ? $(labelId) : null;
|
||
var v = $(valueId);
|
||
if (!v) return;
|
||
if (show) {
|
||
if (l) l.classList.remove('hidden');
|
||
v.classList.remove('hidden');
|
||
v.innerHTML = html;
|
||
} else {
|
||
if (l) l.classList.add('hidden');
|
||
v.classList.add('hidden');
|
||
v.textContent = '';
|
||
}
|
||
}
|
||
|
||
function mhSpan(full) {
|
||
if (!full) return '—';
|
||
var pfx = full.slice(0, 5);
|
||
var rest = full.slice(5);
|
||
return '<span class="mh mono"><span class="pfx">' + escapeHtml(pfx) + '</span> ' + escapeHtml(rest) + '</span>';
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, function (c) {
|
||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||
});
|
||
}
|
||
|
||
function lookupURL(sha, receipt) {
|
||
var u = 'https://explorer.verae.ai/time';
|
||
var sep = '?';
|
||
if (receipt) { u += sep + 'receipt=' + encodeURIComponent(receipt); sep = '&'; }
|
||
if (sha) { u += sep + 'sha256=' + encodeURIComponent(sha); }
|
||
return u;
|
||
}
|
||
|
||
function apply(data) {
|
||
data = data || {};
|
||
var doc = data.document_name || data.document || data.file || '';
|
||
var sha = cleanHex(data.sha256 || data.hash || '');
|
||
var mh = (data.multihash || data.content_multihash || '').trim() || contentMultihash(sha);
|
||
if (mh && !/^f1220/i.test(mh) && isHex64(mh)) mh = 'f1220' + mh.toLowerCase();
|
||
var b3 = cleanHex(data.blake3 || '');
|
||
var b3mh = (data.blake3_multihash || '').trim() || blake3Multihash(b3);
|
||
var receipt = (data.receipt_id || data.receipt || data.certificate || '').trim();
|
||
var created = (data.created_at || data.timestamp || data.server_time || '').trim();
|
||
var merkle = (data.merkle || data.ledger || '').trim();
|
||
var block = data.block || data.block_number || data.blockNumber || '';
|
||
var helper = data.helper_version || data.helperVersion || '';
|
||
var bytes = data.byte_length || data.bytes || data.size_bytes;
|
||
var status = (data.status || data.state || 'final').toLowerCase();
|
||
var evidence = status === 'final' || status === 'existing' || status === 'completed' || status === 'ok';
|
||
|
||
var docLine = doc || '(digest only)';
|
||
if (bytes) docLine += ' · ' + fmtBytes(bytes);
|
||
|
||
$('f-doc').textContent = docLine;
|
||
setRow('l-mh', 'f-mh', !!mh, mhSpan(mh));
|
||
$('f-sha').innerHTML = sha
|
||
? '<span class="mono">' + escapeHtml(sha) + '</span>'
|
||
: '<span class="mono">—</span>';
|
||
|
||
setRow('l-b3', 'f-b3', !!b3, '<span class="mono">' + escapeHtml(b3) + '</span>');
|
||
setRow('l-b3mh', 'f-b3mh', !!b3mh, mhSpan(b3mh));
|
||
|
||
if (created) {
|
||
$('f-time').textContent = created + ' (VeraeTime) — never the local clock';
|
||
} else {
|
||
$('f-time').textContent = '— (server time only; never invent local clock)';
|
||
}
|
||
$('f-receipt').innerHTML = receipt
|
||
? '<span class="mono">' + escapeHtml(receipt) + '</span>'
|
||
: '<span class="mono">—</span>';
|
||
var merkleLine = merkle || (block ? ('Block #' + fmtBlock(block)) : '—');
|
||
$('f-merkle').textContent = merkleLine;
|
||
|
||
var issued = 'Verae Print Stamp';
|
||
if (helper) issued += ' ' + helper;
|
||
issued += ' (printit)';
|
||
$('f-issued').textContent = issued;
|
||
|
||
var banner = $('banner');
|
||
if (evidence) {
|
||
banner.classList.remove('provisional');
|
||
var blk = fmtBlock(block);
|
||
$('banner-title').textContent = blk
|
||
? ('FINAL — Substrate block #' + blk + ', finalized')
|
||
: (status === 'existing' ? 'Already registered — final evidence' : 'FINAL — Substrate block finalized');
|
||
$('banner-sub').textContent = 'GRANDPA-finalized on VeraeNet (rpc.verae.ai)' +
|
||
(created ? (' · ' + created) : '');
|
||
} else {
|
||
banner.classList.add('provisional');
|
||
$('banner-title').textContent = 'Not a certificate yet — pending finality';
|
||
$('banner-sub').textContent = 'Hold / pending / included states are not evidence. statuslang: only final & already registered.';
|
||
}
|
||
|
||
$('cert-no').textContent = receipt
|
||
? ('Receipt ' + (receipt.length > 28 ? receipt.slice(0, 12) + '…' + receipt.slice(-8) : receipt))
|
||
: 'Certificate preview';
|
||
|
||
var url = lookupURL(sha, receipt);
|
||
var a = $('verify-link');
|
||
a.href = url;
|
||
a.textContent = url;
|
||
}
|
||
|
||
function fromQuery() {
|
||
var q = new URLSearchParams(location.search);
|
||
if (![...q.keys()].length) return null;
|
||
return {
|
||
document_name: q.get('document') || q.get('file') || '',
|
||
sha256: q.get('sha256') || q.get('hash') || '',
|
||
multihash: q.get('multihash') || '',
|
||
blake3: q.get('blake3') || '',
|
||
blake3_multihash: q.get('blake3_multihash') || '',
|
||
receipt_id: q.get('receipt') || q.get('receipt_id') || q.get('certificate') || '',
|
||
created_at: q.get('created_at') || q.get('timestamp') || '',
|
||
merkle: q.get('merkle') || '',
|
||
block: q.get('block') || q.get('block_number') || '',
|
||
helper_version: q.get('helper_version') || '',
|
||
byte_length: q.get('bytes') || q.get('byte_length') || '',
|
||
status: q.get('status') || 'final'
|
||
};
|
||
}
|
||
|
||
function normalizeSidecar(j) {
|
||
return {
|
||
document_name: j.document_name || j.document || j.file || j.original_file || '',
|
||
sha256: j.sha256 || j.hash || '',
|
||
multihash: j.multihash || j.content_multihash || '',
|
||
blake3: j.blake3 || '',
|
||
blake3_multihash: j.blake3_multihash || '',
|
||
receipt_id: j.receipt_id || j.receipt || j.certificate || '',
|
||
created_at: j.created_at || j.timestamp || '',
|
||
merkle: j.merkle || '',
|
||
block: j.block_number || j.block || (j.chain && j.chain.block) || '',
|
||
helper_version: j.helper_version || '',
|
||
byte_length: j.byte_length || j.size_bytes || '',
|
||
status: j.status || 'final'
|
||
};
|
||
}
|
||
|
||
var q = fromQuery();
|
||
apply(q || SAMPLE);
|
||
|
||
$('btn-sample').addEventListener('click', function () {
|
||
history.replaceState(null, '', location.pathname);
|
||
apply(SAMPLE);
|
||
});
|
||
|
||
$('sidecar').addEventListener('change', function (ev) {
|
||
var f = ev.target.files && ev.target.files[0];
|
||
if (!f) return;
|
||
var r = new FileReader();
|
||
r.onload = function () {
|
||
try {
|
||
var j = JSON.parse(r.result);
|
||
apply(normalizeSidecar(j));
|
||
} catch (e) {
|
||
alert('Could not parse JSON sidecar: ' + e.message);
|
||
}
|
||
};
|
||
r.readAsText(f);
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|