/** * Minimal PDF builder for retrieval receipts (no native deps). * @module lib/receiptPdf */ /** * @param {string} s * @returns {string} */ function pdfEscape(s) { return String(s).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); } /** * @param {object} receipt * @returns {Buffer} */ export function buildReceiptPdf(receipt) { const lines = [ 'Verae Time — certified retrieval receipt', `Type: ${receipt.type}`, `Job ID: ${receipt.jobId}`, `SHA256: ${receipt.sha256 ?? ''}`, `Original timestamp: ${receipt.timestamp ?? ''}`, `Retrieved at: ${receipt.extraSeal?.retrievedAt ?? ''}`, `Tenant: ${receipt.extraSeal?.tenantId ?? ''}`, `Seal event: ${receipt.extraSeal?.event ?? ''}`, `Certificate: ${String(receipt.certificate ?? '').slice(0, 80)}`, ]; const commands = lines .map((line, i) => { const y = 720 - i * 18; return `BT /F1 11 Tf 50 ${y} Td (${pdfEscape(line)}) Tj ET`; }) .join('\n'); const stream = `${commands}\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'); }