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).
175 lines
5.5 KiB
JavaScript
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);
|