/** * Branded PDF retrieval receipt (no native deps). * @module lib/receiptPdf */ function pdfEscape(s) { return String(s).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); } function wrap(s, n) { const str = String(s || ''); const out = []; for (let i = 0; i < str.length; i += n) out.push(str.slice(i, i + n)); return out.length ? out : ['']; } /** * @param {object} receipt * @returns {Buffer} */ export function buildReceiptPdf(receipt) { const lines = []; const add = (label, value) => { lines.push({ kind: 'label', text: label }); wrap(value, 86).forEach((t) => lines.push({ kind: 'value', text: t })); lines.push({ kind: 'gap' }); }; add('Job ID', receipt.jobId); add('SHA-256', receipt.sha256 ?? ''); add('Original timestamp', receipt.timestamp ?? ''); add('Retrieved at', receipt.extraSeal?.retrievedAt ?? ''); add('Tenant', receipt.extraSeal?.tenantId ?? ''); add('Seal event', receipt.extraSeal?.event ?? ''); add('Certificate', String(receipt.certificate ?? '').slice(0, 240)); const ops = []; ops.push('0.192 0.180 0.506 rg'); ops.push('0 742 612 50 re f'); ops.push('1 1 1 rg'); ops.push('BT /F1 16 Tf 36 760 Td (Verae Time) Tj ET'); ops.push('BT /F1 9 Tf 36 746 Td (CERTIFIED RETRIEVAL RECEIPT) Tj ET'); ops.push('0.09 0.10 0.15 rg'); let y = 710; for (const line of lines) { if (line.kind === 'gap') { y -= 8; continue; } const size = line.kind === 'label' ? 8 : 11; ops.push(`BT /F1 ${size} Tf 36 ${y} Td (${pdfEscape(line.text)}) Tj ET`); y -= line.kind === 'label' ? 12 : 14; } ops.push('0.192 0.180 0.506 rg'); ops.push('36 48 540 0.8 re f'); ops.push('0.42 0.44 0.52 rg'); ops.push( 'BT /F1 8 Tf 36 34 Td (This is a certified retrieval receipt. The extra-seal event is recorded with the hash. It is not a substitute for the chain record.) Tj ET', ); ops.push( `BT /F1 8 Tf 36 22 Td (${pdfEscape('Verae Time x Zapier ยท type ' + (receipt.type || 'verae.retrieval-receipt'))}) Tj ET`, ); const stream = `${ops.join('\n')}\n`; const objects = [ '1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj', '2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj', '3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj', `4 0 obj << /Length ${Buffer.byteLength(stream)} >> stream\n${stream}endstream endobj`, '5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj', ]; let offset = '%PDF-1.4\n'.length; const xref = [0]; let body = '%PDF-1.4\n'; for (const obj of objects) { xref.push(offset); const chunk = `${obj}\n`; body += chunk; offset += Buffer.byteLength(chunk); } const xrefStart = offset; let xrefTable = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; for (let i = 1; i < xref.length; i += 1) { xrefTable += `${String(xref[i]).padStart(10, '0')} 00000 n \n`; } body += xrefTable; body += `trailer << /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`; return Buffer.from(body, 'utf8'); }