PrintToVeraeTimeStamp/internal/job/job.go
Cursor Agent f96ade41bd
Ship the Mac menu bar as an app bundle and use one real stamp path.
The tray and AirDrop helper now live in Verae NetPrint.app so notifications can start, /api/stamp uses the same client as Print-Job and refuses mock receipts, and the default printer name includes the hostname with an mDNS rename when that name is taken.

Co-authored-by: George Lambert <marchon@gmail.com>
2026-10-05 02:09:48 +00:00

320 lines
8.8 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.4"
// Result paths written for one print job.
type Result struct {
OriginalPDF string
CertificatePDF string
ReceiptJSON string
ReceiptID string
SHA256 string
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
}
var unsafeName = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
// RefuseMockReceipt rejects a test double. Print-Job and /api/stamp both
// call this. A receipt id with the mock-cert prefix is not a Verae receipt,
// and it is refused outside test builds that opt into a real id.
func RefuseMockReceipt(id, merkle string) error {
lid := strings.ToLower(strings.TrimSpace(id))
if strings.HasPrefix(lid, "mock-cert") || strings.HasPrefix(lid, "mock-") {
return fmt.Errorf("mock receipt %s is not used outside test builds", strings.TrimSpace(id))
}
if strings.TrimSpace(merkle) == "42" && strings.Contains(lid, "mock") {
return fmt.Errorf("mock receipt %s is not used outside test builds", strings.TrimSpace(id))
}
return nil
}
// 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
}
client := *opt.Client
client.Channel = opt.Channel
rec, err := client.Stamp(digest, origBase)
if err != nil {
return Result{}, err
}
if err := RefuseMockReceipt(rec.ReceiptID, rec.Merkle); 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 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: origBase,
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
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),
}
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,
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
}