PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs
George Lambert c6d998a16d fix: match Verae Timestamping.app registration PDF and receipt fields
Parse completedAt/certificate/result from Verae API; enrich merkle via
POST /api/verify (blockIndex). Generate certificate with the same jsPDF
layout as register.html export. Preserve original filename (e.g. .html).
2026-10-03 04:26:45 -04:00

175 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Official Verae Timestamp Registration Document — same layout as
* mcp.verae.ai register.html / app.html jsPDF export.
*
* Wording overrides (print helper):
* - no mcp.verae.ai URLs
* - "only the digest was submitted" (no "via MCP")
* - footer: Verae | verae.com | explorer.verae.ai
*
* Usage: node build.mjs <input.json> <output.pdf>
* input.json: { file, size_bytes, sha256, scope, receipt_id, created_at, merkle, lookup_url? }
*/
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const __dirname = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
async function loadJsPDF() {
try {
const { jsPDF } = await import("jspdf");
return jsPDF;
} catch {
// npx / global fallbacks not used — deps installed beside this script
throw new Error("jspdf not installed; run: npm install --prefix platform/registration-pdf");
}
}
async function loadQR() {
const QRCode = await import("qrcode");
return QRCode.default || QRCode;
}
function lookupURL(sha, receipt) {
let u = `https://explorer.verae.ai/?sha256=${encodeURIComponent(sha || "")}`;
if (receipt) u += `&receipt=${encodeURIComponent(receipt)}`;
return u;
}
function findLogo() {
const candidates = [
join(__dirname, "../../assets/VeraeFullLogo.png"),
join(__dirname, "VeraeFullLogo.png"),
"/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png",
"/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server/site/assets/VeraeFullLogo.png",
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return null;
}
async function buildPdf(row) {
const jsPDF = await loadJsPDF();
const QRCode = await loadQR();
const doc = new jsPDF({ unit: "pt", format: "letter" });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 48;
const logoPath = findLogo();
let logoData = null;
if (logoPath) {
const b64 = readFileSync(logoPath).toString("base64");
logoData = "data:image/png;base64," + b64;
}
const lookup =
row.lookup_url && !String(row.lookup_url).includes("mcp.verae.ai")
? row.lookup_url
: lookupURL(row.sha256, row.receipt_id);
doc.setDrawColor(42, 24, 56);
doc.setLineWidth(2);
doc.rect(28, 28, pageW - 56, pageH - 56);
doc.setLineWidth(0.6);
doc.setDrawColor(196, 168, 90);
doc.rect(34, 34, pageW - 68, pageH - 68);
if (logoData) {
try {
doc.addImage(logoData, "PNG", margin, 48, 120, 36);
} catch (_) {}
}
doc.setFont("times", "bold");
doc.setFontSize(18);
doc.setTextColor(42, 24, 56);
doc.text("Verae Timestamp Registration Document", pageW / 2, 110, { align: "center" });
doc.setFont("helvetica", "normal");
doc.setFontSize(10);
doc.setTextColor(90, 80, 100);
doc.text(
"Official certificate of digital content registration on the VeraeTime global ledger",
pageW / 2,
128,
{ align: "center" },
);
doc.setDrawColor(196, 168, 90);
doc.setLineWidth(1);
doc.line(margin, 142, pageW - margin, 142);
const fields = [
["Document name", row.file || row.document_name || "(digest only)"],
["Content SHA-256", row.sha256 || ""],
["Registration scope", String(row.scope || "global").toUpperCase()],
["Original timestamp (UTC)", row.created_at || "—"],
["Receipt ID (CID)", row.receipt_id || "—"],
["Merkle / block hash", row.merkle || "See ledger lookup via receipt CID"],
["Ledger location", "VeraeTime global ledger · api.veraetime.net"],
["Blockchain explorer", "https://explorer.verae.ai/"],
["Public lookup URL", lookup],
];
if (row.size_bytes != null && row.size_bytes !== "") {
fields.splice(1, 0, ["Document size", `${row.size_bytes} bytes`]);
}
let y = 168;
doc.setFontSize(10);
for (const [label, value] of fields) {
doc.setFont("helvetica", "bold");
doc.setTextColor(42, 24, 56);
doc.text(label, margin, y);
doc.setFont("helvetica", "normal");
doc.setTextColor(40, 40, 50);
const lines = doc.splitTextToSize(String(value ?? ""), pageW - margin * 2 - 10);
doc.text(lines, margin, y + 14);
y += 14 + lines.length * 12 + 10;
if (y > pageH - 220) break;
}
const qr = await QRCode.toDataURL(lookup, {
errorCorrectionLevel: "M",
margin: 1,
width: 280,
color: { dark: "#2a1838", light: "#ffffff" },
});
const qrSize = 110;
const qrX = pageW - margin - qrSize;
const qrY = pageH - margin - qrSize - 36;
doc.addImage(qr, "PNG", qrX, qrY, qrSize, qrSize);
doc.setFontSize(8);
doc.setTextColor(90, 80, 100);
doc.text("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, {
align: "center",
});
doc.setFontSize(8);
doc.text(
"This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.",
margin,
pageH - 52,
{ maxWidth: pageW - margin * 2 - qrSize - 16 },
);
doc.setFont("helvetica", "bold");
doc.setTextColor(42, 24, 56);
doc.text("Verae · verae.com · explorer.verae.ai", margin, pageH - 36);
return Buffer.from(doc.output("arraybuffer"));
}
const [,, inPath, outPath] = process.argv;
if (!inPath || !outPath) {
console.error("Usage: node build.mjs <input.json> <output.pdf>");
process.exit(2);
}
const row = JSON.parse(readFileSync(inPath, "utf8"));
const pdf = await buildPdf(row);
writeFileSync(outPath, pdf);
console.log(outPath);