PrintToVeraeTimeStamp/internal/job/job.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

331 lines
9 KiB
Go

package job
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/hash"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/pdfcert"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
)
const HelperVersion = "0.3.6"
// Result paths written for one print job.
type Result struct {
OriginalPDF string
CertificatePDF string
ReceiptJSON string
ReceiptID string
SHA256 string
Multihash string // f1220… when SHA-256 known; empty otherwise
Blake3 string // optional; empty when not provided upstream
Channel auth.Channel
}
// Options for processing a captured print PDF.
type Options struct {
SourcePDF string // temp PDF from macOS PDF Services
OutDir string
DocumentName string // optional override for stem; default from source basename
// OutPath, when set, is the exact path chosen by the user in a Save As
// dialog (which already asked before replacing an existing file). The
// original is written there; certificate and sidecar go next to it using
// the chosen base name, and those are still never overwritten.
OutPath string
Channel auth.Channel
Client *veraeapi.Client
// Blake3 is an optional BLAKE3-256 hex digest from upstream. Empty → omit
// on CERT/sidecar (never invent / never hash file bytes for blake3 here).
Blake3 string
}
var unsafeName = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
// SanitizeReceiptID makes a receipt id safe for filenames.
func SanitizeReceiptID(id string) string {
id = strings.TrimSpace(id)
id = unsafeName.ReplaceAllString(id, "-")
id = strings.Trim(id, "-.")
if id == "" {
id = "unknown"
}
// Keep under common NAME_MAX budget with prefix
const max = 180
if len(id) > max {
id = id[:max]
}
return id
}
// StemFromPath returns the document stem without .pdf.
func StemFromPath(path, override string) string {
if override != "" {
s := strings.TrimSuffix(override, filepath.Ext(override))
return unsafeName.ReplaceAllString(s, "-")
}
base := filepath.Base(path)
stem := strings.TrimSuffix(base, filepath.Ext(base))
stem = unsafeName.ReplaceAllString(stem, "-")
stem = strings.Trim(stem, "-.")
if stem == "" {
stem = "document"
}
return stem
}
// DefaultOutDir is ~/Documents/Verae Timestamps.
func DefaultOutDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "Documents", "Verae Timestamps"), nil
}
// Run copies the source PDF, stamps digests-only, writes certificate + JSON.
func Run(opt Options) (Result, error) {
if opt.SourcePDF == "" {
return Result{}, fmt.Errorf("source PDF required")
}
if opt.Client == nil {
return Result{}, fmt.Errorf("client required")
}
outDir := opt.OutDir
if opt.OutPath != "" {
outDir = filepath.Dir(opt.OutPath)
}
if outDir == "" {
d, err := DefaultOutDir()
if err != nil {
return Result{}, err
}
outDir = d
}
if err := os.MkdirAll(outDir, 0o700); err != nil {
return Result{}, err
}
stem := StemFromPath(opt.SourcePDF, opt.DocumentName)
// Preserve original basename (e.g. .html) for the proven artifact + certificate label.
origBase := filepath.Base(opt.SourcePDF)
if opt.DocumentName != "" {
origBase = filepath.Base(opt.DocumentName)
if filepath.Ext(origBase) == "" {
origBase = origBase + filepath.Ext(opt.SourcePDF)
}
}
if origBase == "" || origBase == "." {
origBase = stem + ".pdf"
}
srcBytes, err := os.ReadFile(opt.SourcePDF)
if err != nil {
return Result{}, err
}
// Reserve a name that collides with nothing already there: the original,
// its .verae.json sidecar. Never overwrite; add -2, -3, ... instead.
var origPath string
if opt.OutPath != "" {
origPath = opt.OutPath
if !strings.EqualFold(filepath.Ext(origPath), ".pdf") {
origPath += ".pdf"
}
stem = userStem(filepath.Base(origPath))
if err := os.WriteFile(origPath, srcBytes, 0o600); err != nil { // replace confirmed by the user
return Result{}, err
}
} else {
origPath, stem, err = reserveOriginal(outDir, origBase, stem, srcBytes)
if err != nil {
return Result{}, err
}
}
origBase = filepath.Base(origPath)
digest, err := hash.FileSHA256(origPath)
if err != nil {
return Result{}, err
}
// CERT multihash from known SHA-256 only (never invent). Blake3 only when
// Options carries an upstream digest — job does not compute blake3.
mh := pdfcert.ContentMultihash(digest)
b3 := strings.TrimSpace(opt.Blake3)
b3mh := ""
if b3 != "" {
b3mh = pdfcert.ContentBlake3Multihash(b3)
}
client := *opt.Client
client.Channel = opt.Channel
rec, err := client.Stamp(digest, origBase)
if err != nil {
return Result{}, err
}
if rec.ReceiptID == "" {
return Result{}, fmt.Errorf("stamp returned empty receipt id")
}
safeID := SanitizeReceiptID(rec.ReceiptID)
certStem := fmt.Sprintf("%s-verae-timestamp-%s", stem, safeID)
// Always prefer the branded CERT-01 certificate (digests-only, final-only
// evidence wording, no mcp.verae.ai). Edge attachPdf may still say MCP.
pdfBytes, err := pdfcert.Build(pdfcert.Input{
DocumentName: origBase,
SHA256: digest,
Multihash: mh,
Blake3: b3,
Blake3Multihash: b3mh,
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
certPath, err := writeExclusive(outDir, certStem, ".pdf", pdfBytes)
if err != nil {
return Result{}, err
}
sidecar := map[string]any{
"helper_version": HelperVersion,
"document_name": origBase,
"sha256": digest,
"receipt_id": rec.ReceiptID,
"created_at": rec.CreatedAt,
"merkle": rec.Merkle,
"channel": string(opt.Channel),
"base_url": rec.BaseURL,
"original_file": filepath.Base(origPath),
"certificate_pdf": filepath.Base(certPath),
"byte_length": len(srcBytes),
"status": "ok",
"saved_at": time.Now().UTC().Format(time.RFC3339),
}
// Omit digest extras when empty — never invent.
if mh != "" {
sidecar["multihash"] = mh
}
if b3 != "" {
sidecar["blake3"] = b3
}
if b3mh != "" {
sidecar["blake3_multihash"] = b3mh
}
jb, err := json.MarshalIndent(sidecar, "", " ")
if err != nil {
return Result{}, err
}
jsonPath, err := writeExclusive(outDir, stem, ".verae.json", jb)
if err != nil {
return Result{}, err
}
return Result{
OriginalPDF: origPath,
CertificatePDF: certPath,
ReceiptJSON: jsonPath,
ReceiptID: rec.ReceiptID,
SHA256: digest,
Multihash: mh,
Blake3: b3,
Channel: opt.Channel,
}, nil
}
const maxSuffix = 10000
func suffixed(base string, n int) string {
if n <= 1 {
return base
}
return fmt.Sprintf("%s-%d", base, n)
}
func exists(p string) bool {
_, err := os.Lstat(p)
return err == nil
}
// createNew writes data to path only if path does not exist yet (O_EXCL).
func createNew(path string, data []byte) error {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(path)
return err
}
return f.Close()
}
// writeExclusive writes dir/<stem><ext>, or <stem>-2<ext>, -3 ... if taken.
func writeExclusive(dir, stem, ext string, data []byte) (string, error) {
for n := 1; n < maxSuffix; n++ {
p := filepath.Join(dir, suffixed(stem, n)+ext)
err := createNew(p, data)
if err == nil {
return p, nil
}
if !os.IsExist(err) {
return "", err
}
}
return "", fmt.Errorf("no free file name for %s%s in %s", stem, ext, dir)
}
// reserveOriginal saves the original under origBase (or origBase-N) such that
// neither it nor the matching <stem>.verae.json already exists. Returns the
// saved path and the stem to use for the certificate and sidecar.
func reserveOriginal(dir, origBase, stem string, data []byte) (string, string, error) {
ext := filepath.Ext(origBase)
base := strings.TrimSuffix(origBase, ext)
if base == "" {
base = stem
}
for n := 1; n < maxSuffix; n++ {
st := suffixed(stem, n)
if exists(filepath.Join(dir, st+".verae.json")) {
continue
}
p := filepath.Join(dir, suffixed(base, n)+ext)
err := createNew(p, data)
if err == nil {
return p, st, nil
}
if !os.IsExist(err) {
return "", "", err
}
}
return "", "", fmt.Errorf("no free file name for %s in %s", origBase, dir)
}
// userStem keeps a user-chosen name as typed (spaces etc.), only removing the
// extension and characters that are invalid in file names on any platform.
func userStem(base string) string {
st := strings.TrimSuffix(base, filepath.Ext(base))
st = strings.Map(func(r rune) rune {
if r < 32 || strings.ContainsRune(`<>:"/\\|?*`, r) {
return '-'
}
return r
}, st)
st = strings.TrimRight(strings.TrimSpace(st), ".")
if st == "" {
st = "document"
}
return st
}