diff --git a/assets/VeraeFullLogo.png b/assets/VeraeFullLogo.png new file mode 100644 index 0000000..4c011e8 Binary files /dev/null and b/assets/VeraeFullLogo.png differ diff --git a/go.mod b/go.mod index 8727af7..77c9992 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module github.com/VeraeLLC/PrintToVeraeTimeStamp go 1.26.1 + +require ( + github.com/phpdave11/gofpdf v1.4.2 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ec56742 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/job/job.go b/internal/job/job.go index 31e93b2..4034178 100644 --- a/internal/job/job.go +++ b/internal/job/job.go @@ -127,20 +127,22 @@ func Run(opt Options) (Result, error) { safeID := SanitizeReceiptID(rec.ReceiptID) certPath := filepath.Join(outDir, fmt.Sprintf("%s-verae-timestamp-%s.pdf", stem, safeID)) - pdfBytes := rec.PDF - if len(pdfBytes) == 0 { - pdfBytes, err = pdfcert.Build(pdfcert.Input{ - DocumentName: stem + ".pdf", - SHA256: digest, - ReceiptID: rec.ReceiptID, - CreatedAt: rec.CreatedAt, - Merkle: rec.Merkle, - Channel: string(opt.Channel), - }) - if err != nil { - return Result{}, err - } + // Always prefer the branded Registration Document (digests-only wording, + // no mcp.verae.ai). Edge attachPdf is optional and may still say MCP. + pdfBytes, err := pdfcert.Build(pdfcert.Input{ + DocumentName: stem + ".pdf", + SHA256: digest, + ReceiptID: rec.ReceiptID, + CreatedAt: rec.CreatedAt, + Merkle: rec.Merkle, + Scope: "global", + Channel: string(opt.Channel), + ByteLength: int64(len(srcBytes)), + }) + if err != nil { + return Result{}, err } + _ = rec.PDF // upstream PDF ignored in favor of branded local certificate if err := os.WriteFile(certPath, pdfBytes, 0o600); err != nil { return Result{}, err } diff --git a/internal/job/job_test.go b/internal/job/job_test.go index 1dec275..2012b2e 100644 --- a/internal/job/job_test.go +++ b/internal/job/job_test.go @@ -60,8 +60,14 @@ func TestRunZapierWritesNamedFiles(t *testing.T) { t.Fatal(err) } got, _ := os.ReadFile(res.CertificatePDF) - if string(got) != string(pdf) { - t.Fatalf("cert bytes mismatch") + if !strings.HasPrefix(string(got), "%PDF") { + t.Fatal("certificate is not a PDF") + } + if !strings.Contains(string(got), "Verae Timestamp Registration Document") { + t.Fatal("missing branded title") + } + if strings.Contains(string(got), "mcp.verae.ai") || strings.Contains(string(got), "via MCP") { + t.Fatal("certificate must not mention mcp.verae.ai / via MCP") } if _, err := os.Stat(res.ReceiptJSON); err != nil { t.Fatal(err) diff --git a/internal/pdfcert/logo.png b/internal/pdfcert/logo.png new file mode 100644 index 0000000..4c011e8 Binary files /dev/null and b/internal/pdfcert/logo.png differ diff --git a/internal/pdfcert/pdfcert.go b/internal/pdfcert/pdfcert.go index ec5f23f..5d99749 100644 --- a/internal/pdfcert/pdfcert.go +++ b/internal/pdfcert/pdfcert.go @@ -1,103 +1,218 @@ package pdfcert import ( + "bytes" + _ "embed" "fmt" + "os" + "path/filepath" "strings" "time" + + "github.com/phpdave11/gofpdf" + qrcode "github.com/skip2/go-qrcode" ) -// Input for a simple one-page Verae registration certificate PDF. +//go:embed logo.png +var embeddedLogo []byte + +// Input for the official Verae Timestamp Registration Document. type Input struct { DocumentName string SHA256 string ReceiptID string CreatedAt string Merkle string - VerifyURL string + Scope string + VerifyURL string // optional override; must not be mcp.verae.ai Channel string + ByteLength int64 } -// Build returns a minimal PDF (no external deps) describing the stamp. +// LookupURL builds a public lookup link without mcp.verae.ai. +func LookupURL(sha256, receiptID string) string { + sha256 = strings.TrimSpace(sha256) + receiptID = strings.TrimSpace(receiptID) + // Explorer is the public surface; include digest (+ receipt when present). + u := "https://explorer.verae.ai/?sha256=" + sha256 + if receiptID != "" { + u += "&receipt=" + receiptID + } + return u +} + +// Build returns the branded one-page registration PDF (US Letter). func Build(in Input) ([]byte, error) { created := in.CreatedAt if created == "" { - created = time.Now().UTC().Format(time.RFC3339) + created = time.Now().UTC().Format(time.RFC3339Nano) } - local := "" - if t, err := time.Parse(time.RFC3339, created); err == nil { - local = t.Local().Format(time.RFC1123) + scope := strings.ToUpper(strings.TrimSpace(in.Scope)) + if scope == "" { + scope = "GLOBAL" } - verify := in.VerifyURL - if verify == "" && in.SHA256 != "" { - verify = fmt.Sprintf("https://mcp.verae.ai/register.html?sha256=%s&receipt=%s", in.SHA256, in.ReceiptID) + lookup := in.VerifyURL + if lookup == "" || strings.Contains(strings.ToLower(lookup), "mcp.verae.ai") { + lookup = LookupURL(in.SHA256, in.ReceiptID) + } + merkle := in.Merkle + if merkle == "" { + merkle = "See ledger lookup via receipt CID" + } + docName := in.DocumentName + if docName == "" { + docName = "(digest only)" } - lines := []string{ - "Verae Timestamp Registration", - "", - "Document: " + sanitize(in.DocumentName), - "SHA-256: " + in.SHA256, - "Receipt ID: " + in.ReceiptID, - "UTC: " + created, - } - if local != "" { - lines = append(lines, "Local: "+local) - } - if in.Merkle != "" { - lines = append(lines, "Merkle/block: "+in.Merkle) - } - if in.Channel != "" { - lines = append(lines, "Channel: "+in.Channel) - } - lines = append(lines, "Verify: "+verify) - lines = append(lines, "", "Digests only — document bytes were not uploaded.") + pdf := gofpdf.New("P", "pt", "Letter", "") + pdf.SetCompression(false) // keep certificate text extractable / greppable + pdf.SetMargins(48, 48, 48) + pdf.SetAutoPageBreak(false, 0) + pdf.AddPage() + pageW, pageH := pdf.GetPageSize() + margin := 48.0 - return buildTextPDF(lines) + // Outer frames (purple + gold) matching register.html / app.html + pdf.SetDrawColor(42, 24, 56) + pdf.SetLineWidth(2) + pdf.Rect(28, 28, pageW-56, pageH-56, "D") + pdf.SetDrawColor(196, 168, 90) + pdf.SetLineWidth(0.6) + pdf.Rect(34, 34, pageW-68, pageH-68, "D") + + // Logo + logoPath, cleanup, err := materializeLogo() + if err == nil { + pdf.ImageOptions(logoPath, margin, 48, 120, 36, false, gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}, 0, "") + if cleanup != nil { + defer cleanup() + } + } + + pdf.SetTextColor(42, 24, 56) + pdf.SetFont("Times", "B", 18) + pdf.SetXY(margin, 100) + pdf.CellFormat(pageW-margin*2, 20, "Verae Timestamp Registration Document", "", 0, "C", false, 0, "") + + pdf.SetTextColor(90, 80, 100) + pdf.SetFont("Helvetica", "", 10) + pdf.SetXY(margin, 122) + pdf.CellFormat(pageW-margin*2, 12, "Official certificate of digital content registration on the VeraeTime global ledger", "", 0, "C", false, 0, "") + + pdf.SetDrawColor(196, 168, 90) + pdf.SetLineWidth(1) + pdf.Line(margin, 142, pageW-margin, 142) + + fields := [][2]string{ + {"Document name", docName}, + } + if in.ByteLength > 0 { + fields = append(fields, [2]string{"Document size", fmt.Sprintf("%d bytes", in.ByteLength)}) + } + fields = append(fields, + [2]string{"Content SHA-256", in.SHA256}, + [2]string{"Registration scope", scope}, + [2]string{"Original timestamp (UTC)", created}, + [2]string{"Receipt ID (CID)", orDash(in.ReceiptID)}, + [2]string{"Merkle / block hash", merkle}, + [2]string{"Ledger location", "VeraeTime global ledger - api.veraetime.net"}, + [2]string{"Blockchain explorer", "https://explorer.verae.ai/"}, + [2]string{"Public lookup URL", lookup}, + ) + + y := 168.0 + for _, fv := range fields { + label, value := fv[0], fv[1] + pdf.SetFont("Helvetica", "B", 10) + pdf.SetTextColor(42, 24, 56) + pdf.SetXY(margin, y) + pdf.Cell(pageW-margin*2, 12, label) + y += 14 + pdf.SetFont("Helvetica", "", 10) + pdf.SetTextColor(40, 40, 50) + lines := pdf.SplitLines([]byte(value), pageW-margin*2-10) + for _, ln := range lines { + pdf.SetXY(margin, y) + pdf.Cell(pageW-margin*2-10, 12, string(ln)) + y += 12 + } + y += 10 + if y > pageH-220 { + break + } + } + + // QR code → explorer lookup (not mcp.verae.ai) + qrPNG, err := qrcode.Encode(lookup, qrcode.Medium, 280) + if err == nil { + qrFile, qclean, qerr := writeTempPNG(qrPNG) + if qerr == nil { + qrSize := 110.0 + qrX := pageW - margin - qrSize + qrY := pageH - margin - qrSize - 36 + pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "") + pdf.SetFont("Helvetica", "", 8) + pdf.SetTextColor(90, 80, 100) + pdf.SetXY(qrX, qrY+qrSize+4) + pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "") + if qclean != nil { + defer qclean() + } + } + } + + disclaimer := "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted." + pdf.SetFont("Helvetica", "", 8) + pdf.SetTextColor(90, 80, 100) + pdf.SetXY(margin, pageH-64) + pdf.MultiCell(pageW-margin*2-130, 10, disclaimer, "", "L", false) + + pdf.SetFont("Helvetica", "B", 9) + pdf.SetTextColor(42, 24, 56) + pdf.SetXY(margin, pageH-36) + pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai") + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, err + } + return buf.Bytes(), nil } -func sanitize(s string) string { - s = strings.ReplaceAll(s, "(", "\\(") - s = strings.ReplaceAll(s, ")", "\\)") - s = strings.ReplaceAll(s, "\\", "\\\\") +func orDash(s string) string { + if strings.TrimSpace(s) == "" { + return "—" + } return s } -// Minimal PDF 1.4 with Helvetica text lines. -func buildTextPDF(lines []string) ([]byte, error) { - var content strings.Builder - content.WriteString("BT /F1 12 Tf 50 750 Td 14 TL\n") - for i, line := range lines { - if i > 0 { - content.WriteString("T*\n") +func materializeLogo() (path string, cleanup func(), err error) { + // Prefer module assets path next to source; fall back to embed + candidates := []string{ + filepath.Join("assets", "VeraeFullLogo.png"), + "/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png", + } + for _, c := range candidates { + if st, e := os.Stat(c); e == nil && st.Size() > 0 { + return c, nil, nil } - content.WriteString("(") - content.WriteString(sanitize(line)) - content.WriteString(") Tj\n") } - content.WriteString("ET\n") - stream := content.String() - - objects := []string{ - "<< /Type /Catalog /Pages 2 0 R >>", - "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", - "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>", - fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(stream), stream), - "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + if len(embeddedLogo) == 0 { + return "", nil, fmt.Errorf("logo not found") } + return writeTempPNG(embeddedLogo) +} - var body strings.Builder - body.WriteString("%PDF-1.4\n") - offsets := make([]int, len(objects)+1) - for i, obj := range objects { - offsets[i+1] = body.Len() - fmt.Fprintf(&body, "%d 0 obj\n%s\nendobj\n", i+1, obj) +func writeTempPNG(b []byte) (path string, cleanup func(), err error) { + f, err := os.CreateTemp("", "verae-logo-*.png") + if err != nil { + return "", nil, err } - xrefStart := body.Len() - fmt.Fprintf(&body, "xref\n0 %d\n", len(objects)+1) - body.WriteString("0000000000 65535 f \n") - for i := 1; i <= len(objects); i++ { - fmt.Fprintf(&body, "%010d 00000 n \n", offsets[i]) + if _, err := f.Write(b); err != nil { + f.Close() + os.Remove(f.Name()) + return "", nil, err } - fmt.Fprintf(&body, "trailer << /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objects)+1, xrefStart) - return []byte(body.String()), nil + f.Close() + return f.Name(), func() { _ = os.Remove(f.Name()) }, nil } diff --git a/internal/pdfcert/pdfcert_test.go b/internal/pdfcert/pdfcert_test.go new file mode 100644 index 0000000..d1254d2 --- /dev/null +++ b/internal/pdfcert/pdfcert_test.go @@ -0,0 +1,54 @@ +package pdfcert + +import ( + "bytes" + "strings" + "testing" +) + +func TestBuildBrandedRegistration(t *testing.T) { + b, err := Build(Input{ + DocumentName: "Contract.pdf", + SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ReceiptID: "QmTestReceipt123", + CreatedAt: "2026-10-03T12:00:00Z", + Merkle: "113198", + Scope: "global", + ByteLength: 4096, + }) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(b, []byte("%PDF")) { + t.Fatal("not a pdf") + } + if len(b) < 5000 { + t.Fatalf("pdf too small (%d) — logo/qr missing?", len(b)) + } + s := string(b) + if strings.Contains(s, "mcp.verae.ai") { + t.Fatal("must not mention mcp.verae.ai") + } + if strings.Contains(s, "via MCP") { + t.Fatal("must not say via MCP") + } + if !strings.Contains(s, "Verae Timestamp Registration Document") { + t.Fatal("missing title") + } + if !strings.Contains(s, "only the digest was submitted") { + t.Fatal("missing digests-only disclaimer") + } + if !strings.Contains(s, "explorer.verae.ai") { + t.Fatal("expected explorer lookup") + } +} + +func TestLookupURLNoMCP(t *testing.T) { + u := LookupURL("abc", "rid") + if strings.Contains(u, "mcp.verae.ai") { + t.Fatal(u) + } + if !strings.Contains(u, "explorer.verae.ai") { + t.Fatal(u) + } +}