Implements macOS v0: verae-print stamp + install-macos PDF menu items.
Saves {name}.pdf, {name}-verae-timestamp-{RECEIPTID}.pdf, and .verae.json.
Channels: verae (api.veraetime.net JWT) and zapier (zap.verae.ai xVerae- + attachPdf).
179 lines
4.5 KiB
Go
179 lines
4.5 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.1.0"
|
|
|
|
// 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
|
|
Channel auth.Channel
|
|
Client *veraeapi.Client
|
|
}
|
|
|
|
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 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)
|
|
origPath := filepath.Join(outDir, stem+".pdf")
|
|
srcBytes, err := os.ReadFile(opt.SourcePDF)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if err := os.WriteFile(origPath, srcBytes, 0o600); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
digest, err := hash.FileSHA256(origPath)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
client := *opt.Client
|
|
client.Channel = opt.Channel
|
|
rec, err := client.Stamp(digest, stem+".pdf")
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if rec.ReceiptID == "" {
|
|
return Result{}, fmt.Errorf("stamp returned empty receipt id")
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
if err := os.WriteFile(certPath, pdfBytes, 0o600); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
sidecar := map[string]any{
|
|
"helper_version": HelperVersion,
|
|
"document_name": stem + ".pdf",
|
|
"sha256": digest,
|
|
"receipt_id": rec.ReceiptID,
|
|
"created_at": rec.CreatedAt,
|
|
"merkle": rec.Merkle,
|
|
"channel": string(opt.Channel),
|
|
"base_url": rec.BaseURL,
|
|
"original_pdf": filepath.Base(origPath),
|
|
"certificate_pdf": filepath.Base(certPath),
|
|
"byte_length": len(srcBytes),
|
|
"saved_at": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
jsonPath := filepath.Join(outDir, stem+".verae.json")
|
|
jb, err := json.MarshalIndent(sidecar, "", " ")
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if err := os.WriteFile(jsonPath, jb, 0o600); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
return Result{
|
|
OriginalPDF: origPath,
|
|
CertificatePDF: certPath,
|
|
ReceiptJSON: jsonPath,
|
|
ReceiptID: rec.ReceiptID,
|
|
SHA256: digest,
|
|
Channel: opt.Channel,
|
|
}, nil
|
|
}
|