PrintToVeraeTimeStamp/site/printit.verae.ai/certificate.html
George Lambert f766e537ba feat: WiX ProductVersion from HelperVersion + build-tray-msi + CERT live QR
- build-tray-msi.ps1 candle/light or tip; version from common.ps1
- certificate.html final QR via qrcode CDN when digest present
2026-10-13 23:08:55 -04:00

495 lines
22 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>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&amp;family=JetBrains+Mono:wght@500&amp;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: Fraunces via Google Fonts CDN; Georgia/Times offline-ish fallback */
.title{
text-align:center;margin:28px 0 0;
font-family:'Fraunces',var(--font-serif,Georgia,serif),Georgia,'Times New Roman',Times,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;
/* Offline / provisional placeholder — print-safe without CDN */
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;overflow:hidden;
}
.qr .qr-fallback{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59);pointer-events:none}
.qr img.qr-bitmap,.qr canvas.qr-bitmap{width:100%;height:100%;display:block;border-radius:6px;object-fit:contain;background:#fff}
.qr.has-qr{background:#fff}
.qr.has-qr .qr-fallback{display:none}
.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;
}
/* CDN may be absent offline; keep plum/gold frame + system serif title */
.title{font-family:'Fraunces',Georgia,'Times New Roman',Times,serif!important}
.rule,.banner,.mh,.qr,.qr img.qr-bitmap,.qr canvas.qr-bitmap{
-webkit-print-color-adjust:exact;print-color-adjust:exact
}
.rule{background:#C4A85A!important}
.qr{width:96px;height:96px;border-color:#1D1534!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" id="qr" role="img" aria-label="Verify QR — explorer lookup when final">
<span class="qr-fallback">Verae</span>
</div>
<div>
<b style="font-size:12.5px">Verify this certificate</b>
<ol>
<li>Open the explorer link (or scan the QR when final).</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=&amp;sha256=&amp;receipt=&amp;created_at=&amp;merkle=&amp;blake3=&amp;block=&amp;helper_version=</span>
Multihash / BLAKE3 lines appear <b>only when data is present</b> — never invented.
Title uses Fraunces (Google Fonts CDN) with Georgia/serif offline fallback; plum/gold frame kept.
Final certificates paint a live QR (qrcode.js CDN → data-URL) from the explorer lookup; offline / provisional keeps the SVG placeholder so print still works.
Residual: full CERT-01.html self-hosted Fraunces/Cantarell pixel match (design-kit fonts).
</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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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;
}
/** Live QR when final + payload; CDN fail / offline / provisional → SVG placeholder (print-safe). */
function clearQR(el) {
if (!el) return;
el.classList.remove('has-qr');
var bit = el.querySelectorAll('img.qr-bitmap, canvas.qr-bitmap');
for (var i = 0; i < bit.length; i++) bit[i].remove();
}
function paintQRDataUrl(el, url) {
if (!el || !url) return;
clearQR(el);
var img = document.createElement('img');
img.className = 'qr-bitmap';
img.alt = '';
img.width = 112;
img.height = 112;
img.decoding = 'async';
img.onload = function () { el.classList.add('has-qr'); };
img.onerror = function () { img.remove(); /* keep placeholder */ };
img.src = url;
el.appendChild(img);
}
function withQRCodeLib(fn) {
if (window.QRCode && typeof window.QRCode.toDataURL === 'function') {
fn(window.QRCode);
return;
}
var existing = document.querySelector('script[data-verae-qrcode-lib]');
if (existing) {
existing.addEventListener('load', function () {
if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode);
});
return;
}
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js';
s.async = true;
s.setAttribute('data-verae-qrcode-lib', '1');
s.onload = function () {
if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode);
};
/* onerror: leave placeholder — offline print fallback */
document.head.appendChild(s);
}
function setVerifyQR(payload, isFinal) {
var el = $('qr');
if (!el) return;
clearQR(el);
if (!isFinal || !payload) return;
withQRCodeLib(function (QRCode) {
QRCode.toDataURL(String(payload), {
errorCorrectionLevel: 'M',
margin: 1,
width: 224,
color: { dark: '#1D1534', light: '#FFFFFF' }
}, function (err, dataUrl) {
if (err || !dataUrl) return;
paintQRDataUrl(el, dataUrl);
});
});
}
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;
/* Real QR only when final evidence + receipt or digest; else placeholder. */
var qrPayload = (sha || receipt || mh) ? url : '';
setVerifyQR(qrPayload, evidence && !!qrPayload);
}
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>