feat(macos): PDF Services stamp helper with dual-channel Verae auth
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).
This commit is contained in:
parent
e91275a67c
commit
46f2bcd43e
2 changed files with 1259 additions and 24 deletions
179
internal/job/job.go
Normal file
179
internal/job/job.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
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
|
||||
}
|
||||
96
internal/job/job_test.go
Normal file
96
internal/job/job_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
|
||||
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
|
||||
)
|
||||
|
||||
func TestSanitizeAndStem(t *testing.T) {
|
||||
if SanitizeReceiptID("bafy/xyz!") != "bafy-xyz" {
|
||||
t.Fatal(SanitizeReceiptID("bafy/xyz!"))
|
||||
}
|
||||
if StemFromPath("/tmp/My Contract (1).pdf", "") != "My-Contract-1" {
|
||||
t.Fatal(StemFromPath("/tmp/My Contract (1).pdf", ""))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunZapierWritesNamedFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "src.pdf")
|
||||
if err := os.WriteFile(src, []byte("%PDF-1.4 hello"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := filepath.Join(dir, "out")
|
||||
pdf := []byte("%PDF-1.4 certificate-bytes")
|
||||
b64 := base64.StdEncoding.EncodeToString(pdf)
|
||||
|
||||
client := &veraeapi.Client{
|
||||
Channel: auth.ChannelZapier,
|
||||
ZapierBase: "https://zap.test",
|
||||
XVeraeToken: "xVerae-x",
|
||||
HTTPDo: func(req *http.Request) (*http.Response, error) {
|
||||
body := `{"receipt_id":"bafyReceipt99","pdfBase64":"` + b64 + `"}`
|
||||
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
|
||||
},
|
||||
}
|
||||
|
||||
res, err := Run(Options{
|
||||
SourcePDF: src,
|
||||
OutDir: out,
|
||||
DocumentName: "Contract.pdf",
|
||||
Channel: auth.ChannelZapier,
|
||||
Client: client,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantCert := filepath.Join(out, "Contract-verae-timestamp-bafyReceipt99.pdf")
|
||||
if res.CertificatePDF != wantCert {
|
||||
t.Fatalf("cert path %s want %s", res.CertificatePDF, wantCert)
|
||||
}
|
||||
if _, err := os.Stat(res.OriginalPDF); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ := os.ReadFile(res.CertificatePDF)
|
||||
if string(got) != string(pdf) {
|
||||
t.Fatalf("cert bytes mismatch")
|
||||
}
|
||||
if _, err := os.Stat(res.ReceiptJSON); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVeraeRendersLocalCert(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "memo.pdf")
|
||||
_ = os.WriteFile(src, []byte("%PDF-1.4 memo"), 0o600)
|
||||
out := filepath.Join(dir, "out")
|
||||
client := &veraeapi.Client{
|
||||
Channel: auth.ChannelVerae,
|
||||
VeraeBase: "https://api.test",
|
||||
VeraeToken: "jwt",
|
||||
HTTPDo: func(req *http.Request) (*http.Response, error) {
|
||||
body := `{"certificate":"cidLocal1","timestamp":"2026-10-03T12:00:00Z","existing":true}`
|
||||
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
|
||||
},
|
||||
}
|
||||
res, err := Run(Options{SourcePDF: src, OutDir: out, Channel: auth.ChannelVerae, Client: client})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(res.CertificatePDF, "memo-verae-timestamp-cidLocal1.pdf") {
|
||||
t.Fatal(res.CertificatePDF)
|
||||
}
|
||||
b, _ := os.ReadFile(res.CertificatePDF)
|
||||
if !strings.HasPrefix(string(b), "%PDF") {
|
||||
t.Fatal("not a pdf")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue