PrintToVeraeTimeStamp/internal/pdfcert/pdfcert.go
George Lambert 48da24a661 feat: 2026 brand UI — statuslang, tray/menubar, CERT pdf, site kit
- Brand receipt status strings (statuslang) for notify/queue
- Windows tray + Start Menu/Startup install; macOS menubar helper
- CERT-01 plum/gold PDF + multihash/blake3 when present
- printit.verae.ai brand kit, queue page, customer-auth helper
- verae-print pending/tray-status/menubar commands
2026-10-13 21:11:32 -04:00

228 lines
6.6 KiB
Go

package pdfcert
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
//go:embed logo.png
var embeddedLogo []byte
// Input for the official Verae Timestamp Certificate (CERT-01).
type Input struct {
DocumentName string
SHA256 string
// Multihash is the content multihash (f1220 + sha2-256 hex) when known.
// Empty → derive from SHA256 if valid; else omit (never invent).
Multihash string
// Blake3 is the optional BLAKE3-256 hex digest when provided upstream.
// Empty → omit (never invent / never compute from file bytes here).
Blake3 string
// Blake3Multihash is the blake3 multihash (f1e20 + hex) when known.
// Empty → derive from Blake3 if valid 64-hex; else omit.
Blake3Multihash string
ReceiptID string
CreatedAt string
Merkle string
Scope string
VerifyURL string
Channel string
ByteLength int64
}
// ContentMultihash returns sha2-256 multihash hex (code 0x12, len 0x20 → f1220…)
// when sha256 is a 64-char hex digest; otherwise "" (caller omits the field).
func ContentMultihash(sha256 string) string {
return contentMultihashPrefixed("1220", sha256)
}
// ContentBlake3Multihash returns blake3 multihash hex (code 0x1e, len 0x20 → f1e20…)
// when blake3 is a 64-char hex digest; otherwise "" (caller omits the field).
func ContentBlake3Multihash(blake3 string) string {
return contentMultihashPrefixed("1e20", blake3)
}
// contentMultihashPrefixed wraps a 64-char hex digest as multibase-base16 multihash
// ("f" + code/len prefix + digest). Invalid/empty → "" (never invent).
func contentMultihashPrefixed(codeLen, hexDigest string) string {
s := strings.TrimSpace(hexDigest)
s = strings.TrimPrefix(strings.ToLower(s), "0x")
if len(s) != 64 {
return ""
}
for i := 0; i < len(s); i++ {
c := s[i]
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
return ""
}
}
return "f" + codeLen + s
}
// ResolveDigests returns CERT multihash / blake3 display values from Input.
// Empty strings mean omit that line (never invent digests).
func ResolveDigests(in Input) (mh, blake3Hex, blake3MH string) {
mh = strings.TrimSpace(in.Multihash)
if mh == "" {
mh = ContentMultihash(in.SHA256)
}
blake3Hex = strings.TrimSpace(in.Blake3)
blake3MH = strings.TrimSpace(in.Blake3Multihash)
if blake3MH == "" {
blake3MH = ContentBlake3Multihash(in.Blake3)
}
return mh, blake3Hex, blake3MH
}
// LookupURL builds a public VeraeTime verify deep link (same as explorer
// scan-bar → /time, which auto-fills receipt/sha256 and runs Lookup).
// https://explorer.verae.ai/time?receipt=…&sha256=…
func LookupURL(sha256, receiptID string) string {
sha256 = strings.TrimSpace(sha256)
sha256 = strings.TrimPrefix(strings.ToLower(sha256), "0x")
receiptID = strings.TrimSpace(receiptID)
u := "https://explorer.verae.ai/time"
sep := "?"
if receiptID != "" {
u += sep + "receipt=" + receiptID
sep = "&"
}
if sha256 != "" {
u += sep + "sha256=" + sha256
}
return u
}
// Build returns the branded CERT-01 certificate PDF (plum/gold, final-only
// evidence copy). Prefers platform/registration-pdf/build.mjs when Node is
// available; otherwise gofpdf fallback.
func Build(in Input) ([]byte, error) {
created := in.CreatedAt
if created == "" {
// Do not invent a ledger time — leave blank for the PDF field.
created = ""
}
scope := strings.ToUpper(strings.TrimSpace(in.Scope))
if scope == "" {
scope = "GLOBAL"
}
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)"
}
mh, b3, b3mh := ResolveDigests(in)
row := map[string]any{
"file": docName,
"sha256": in.SHA256,
"scope": scope,
"receipt_id": in.ReceiptID,
"created_at": created,
"merkle": merkle,
"lookup_url": lookup,
"size_bytes": in.ByteLength,
}
// Omit digest fields when empty — never invent.
if mh != "" {
row["multihash"] = mh
}
if b3 != "" {
row["blake3"] = b3
}
if b3mh != "" {
row["blake3_multihash"] = b3mh
}
if b, err := buildWithNode(row); err == nil && len(b) > 1000 {
return b, nil
}
return buildFallbackGofpdf(in, docName, created, scope, merkle, lookup)
}
func buildWithNode(row map[string]any) ([]byte, error) {
script := findBuildScript()
if script == "" {
return nil, fmt.Errorf("registration-pdf build.mjs not found")
}
tmp, err := os.MkdirTemp("", "verae-regpdf-*")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmp)
inPath := filepath.Join(tmp, "in.json")
outPath := filepath.Join(tmp, "out.pdf")
jb, err := json.Marshal(row)
if err != nil {
return nil, err
}
if err := os.WriteFile(inPath, jb, 0o600); err != nil {
return nil, err
}
cmd := exec.Command("node", script, inPath, outPath)
cmd.Dir = filepath.Dir(script)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("node build.mjs: %w (%s)", err, stderr.String())
}
return os.ReadFile(outPath)
}
func findBuildScript() string {
// Prefer tree-local build.mjs (repo / package) so CERT-01 edits win over
// a stale install under Application Support during tests and dev runs.
var candidates []string
if wd, err := os.Getwd(); err == nil {
dir := wd
for i := 0; i < 8; i++ {
candidates = append(candidates, filepath.Join(dir, "platform", "registration-pdf", "build.mjs"))
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
}
candidates = append(candidates, filepath.Join("platform", "registration-pdf", "build.mjs"))
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "registration-pdf", "build.mjs"),
filepath.Join(dir, "..", "registration-pdf", "build.mjs"),
filepath.Join(dir, "platform", "registration-pdf", "build.mjs"),
filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"),
filepath.Join(dir, "..", "..", "platform", "registration-pdf", "build.mjs"),
)
}
// Installed helper copy last (production path when no tree is present).
candidates = append(candidates,
"/Library/Application Support/Verae/PrintStamp/registration-pdf/build.mjs",
"/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs",
)
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
abs, _ := filepath.Abs(c)
return abs
}
}
return ""
}
// Ensure created stays empty when unknown — used by tests.
var _ = time.Now