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 // EmbedBrandFonts requests PDF embed of a licensed brand serif when a // .ttf/.otf is present under BrandFontsDir(). Default false: core // Times/Helvetica only. Also via VERAE_EMBED_BRAND_FONTS=1|true|yes. // gofpdf path attempts AddUTF8FontFromBytes; failure is a logged no-op. EmbedBrandFonts bool } // EmbedBrandFontsEnabled reports whether brand-font PDF embed is requested. // Default false. True only when Input.EmbedBrandFonts is set or env // VERAE_EMBED_BRAND_FONTS is 1/true/yes. When enabled, gofpdf may register a // .ttf/.otf from BrandFontsDir(); failure is a logged no-op (core fonts). func EmbedBrandFontsEnabled(in Input) bool { if in.EmbedBrandFonts { return true } v := strings.TrimSpace(os.Getenv("VERAE_EMBED_BRAND_FONTS")) switch strings.ToLower(v) { case "1", "true", "yes", "on": return true default: return false } } // BrandFontsDir is the directory scanned for .ttf/.otf when EmbedBrandFonts is on. // Override with VERAE_BRAND_FONTS_DIR; else site/printit.verae.ai/fonts from the // module tree. Empty when neither is available (embed no-ops with a log). func BrandFontsDir() string { if d := strings.TrimSpace(os.Getenv("VERAE_BRAND_FONTS_DIR")); d != "" { if st, err := os.Stat(d); err == nil && st.IsDir() { return d } } var candidates []string if wd, err := os.Getwd(); err == nil { dir := wd for i := 0; i < 8; i++ { candidates = append(candidates, filepath.Join(dir, "site", "printit.verae.ai", "fonts")) if st, se := os.Stat(filepath.Join(dir, "go.mod")); se == nil && !st.IsDir() { break } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } } candidates = append(candidates, filepath.Join("site", "printit.verae.ai", "fonts")) for _, c := range candidates { if st, err := os.Stat(c); err == nil && st.IsDir() { abs, _ := filepath.Abs(c) return abs } } return "" } // findBrandFaceFile returns the preferred .ttf/.otf under dir (Fraunces-named // first, then any ttf, then otf). Empty when none present. func findBrandFaceFile(dir string) string { if dir == "" { return "" } ents, err := os.ReadDir(dir) if err != nil { return "" } var frauncesTTF, anyTTF, frauncesOTF, anyOTF string for _, e := range ents { if e.IsDir() { continue } name := e.Name() low := strings.ToLower(name) path := filepath.Join(dir, name) switch { case strings.HasSuffix(low, ".ttf"): if strings.Contains(low, "fraunces") && frauncesTTF == "" { frauncesTTF = path } else if anyTTF == "" { anyTTF = path } case strings.HasSuffix(low, ".otf"): if strings.Contains(low, "fraunces") && frauncesOTF == "" { frauncesOTF = path } else if anyOTF == "" { anyOTF = path } } } for _, p := range []string{frauncesTTF, anyTTF, frauncesOTF, anyOTF} { if p != "" { return p } } return "" } // 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. // // Typography: default PDF standard fonts (Times title, Helvetica body). // EmbedBrandFonts defaults false (env VERAE_EMBED_BRAND_FONTS off). When on, // gofpdf fallback may register a licensed .ttf/.otf from BrandFontsDir(); // failure is a logged no-op. Node path stays core faces (OPEN-ISSUES #7). 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). // No machine-specific absolute research paths — packaging resolves via go.mod walk above. candidates = append(candidates, "/Library/Application Support/Verae/PrintStamp/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