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:
George Lambert 2026-10-03 03:56:27 -04:00
commit 46f2bcd43e
14 changed files with 1259 additions and 24 deletions

View file

@ -1,27 +1,65 @@
# PrintToVeraeTimeStamp # PrintToVeraeTimeStamp
A virtual printer for macOS, Linux, and Windows. Instead of (or in addition to) printing, it captures the print job as PDF, computes its SHA-256, timestamps that digest with the Verae Timestamping service, and lets you save or print the document together with its Verae timestamp certificate. macOS Print → PDF helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`.
Only the digest is sent to Verae. The document never leaves your machine. Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine.
## Install
```bash
cd /Users/marchon/research/PrintToVeraeTimeStamp
go build -o "$HOME/bin/verae-print" ./cmd/verae-print
verae-print install-macos
```
Ensure `$HOME/bin` is on your `PATH`. Refresh Verae Time login when the JWT expires:
```bash
uvx verae-mcp-timestamping-server setup
# or session check:
uvx verae-mcp-timestamping-server session
```
For the Zapier channel, store an `xVerae-` token from [customer.verae.ai](https://customer.verae.ai/portal/) / [zap.verae.ai](https://zap.verae.ai/portal/) Security tab:
```bash
mkdir -p ~/.verae
chmod 700 ~/.verae
printenv XVERAE_TOKEN > ~/.verae/xverae-token # or paste once
chmod 600 ~/.verae/xverae-token
```
## Usage
In any app: **File → Print → PDF → Timestamp with Verae** (or **Timestamp with Verae (Zapier)**).
CLI:
```bash
verae-print stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] file.pdf
```
Default output directory: `~/Documents/Verae Timestamps/`
| File | Content |
|------|---------|
| `{name}.pdf` | Exact printed bytes (proven artifact) |
| `{name}-verae-timestamp-{RECEIPTID}.pdf` | Registration / certificate PDF |
| `{name}.verae.json` | Receipt sidecar |
Channels:
- `verae` (default) — `api.veraetime.net` + JWT in `~/.verae/auth.properties`
- `zapier` — `zap.verae.ai` + `xVerae-` + `attachPdf:true`
## Status ## Status
Design stage. See: macOS v0 (PDF Services) is implemented. CUPS virtual printer, Linux, and Windows remain per the [spec](docs/superpowers/specs/2026-10-03-print-to-verae-timestamp.md) and [plan](docs/superpowers/plans/2026-10-03-print-to-verae-timestamp.md).
- [Spec](docs/superpowers/specs/2026-10-03-print-to-verae-timestamp.md) ## Contributing
- [Implementation plan](docs/superpowers/plans/2026-10-03-print-to-verae-timestamp.md)
## How it works (short) Open questions and design changes in this repository. Pull requests are accepted for review against the spec.
```
Any app → Print → "Verae Timestamp" printer
→ job captured as PDF (CUPS on macOS/Linux, IPP/PDF port on Windows)
→ per-user helper: SHA-256 (whole document + per page)
→ Verae Timestamp API (digest only) → receipt JSON / certificate
→ Save: original PDF + receipt (+ optional certificate page / embedded receipt)
→ Optional: forward to a real printer with certificate page
```
## License ## License
TBD. UNLICENSED © Verae / George Lambert

221
cmd/verae-print/main.go Normal file
View file

@ -0,0 +1,221 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "stamp":
os.Exit(runStamp(os.Args[2:]))
case "install-macos":
os.Exit(runInstallMacOS(os.Args[2:]))
case "version", "-version", "--version":
fmt.Println(job.HelperVersion)
case "help", "-h", "--help":
usage()
default:
// PDF Services may invoke the binary with the PDF path as argv[1]
if strings.HasSuffix(strings.ToLower(os.Args[1]), ".pdf") && fileExists(os.Args[1]) {
os.Exit(runStamp([]string{"-channel", envOr("VERAE_PRINT_CHANNEL", "verae"), os.Args[1]}))
}
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
usage()
os.Exit(2)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `verae-print %s — stamp a printed PDF with Verae (digests only)
Usage:
verae-print stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] <file.pdf>
verae-print install-macos # install PDF Services menu items
verae-print version
Channels:
verae (default) api.veraetime.net + ~/.verae/auth.properties JWT
zapier zap.verae.ai + xVerae- token (XVERAE_TOKEN / ~/.verae/xverae-token / Keychain)
Outputs in --out-dir (default ~/Documents/Verae Timestamps):
{name}.pdf
{name}-verae-timestamp-{RECEIPTID}.pdf
{name}.verae.json
`, job.HelperVersion)
}
func runStamp(args []string) int {
fs := flag.NewFlagSet("stamp", flag.ExitOnError)
channel := fs.String("channel", envOr("VERAE_PRINT_CHANNEL", "verae"), "verae | zapier")
outDir := fs.String("out-dir", "", "output directory")
name := fs.String("name", "", "document stem override")
openFolder := fs.Bool("open", false, "open output folder on success (macOS)")
_ = fs.Parse(args)
if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "stamp: PDF path required")
return 2
}
pdfPath := fs.Arg(0)
ch, err := auth.ParseChannel(*channel)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
client := &veraeapi.Client{Channel: ch}
switch ch {
case auth.ChannelZapier:
tok, err := auth.LoadXVeraeToken()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
client.XVeraeToken = tok
default:
tok, err := auth.LoadVeraeJWT()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
client.VeraeToken = tok
}
res, err := job.Run(job.Options{
SourcePDF: pdfPath,
OutDir: *outDir,
DocumentName: *name,
Channel: ch,
Client: client,
})
if err != nil {
fmt.Fprintf(os.Stderr, "stamp failed: %v\n", err)
return 1
}
fmt.Printf("Saved:\n %s\n %s\n %s\nReceipt: %s\nSHA-256: %s\nChannel: %s\n",
res.OriginalPDF, res.CertificatePDF, res.ReceiptJSON, res.ReceiptID, res.SHA256, res.Channel)
if *openFolder {
_ = exec.Command("open", filepath.Dir(res.OriginalPDF)).Run()
}
// macOS notification (best-effort)
notify(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
return 0
}
func runInstallMacOS(args []string) int {
fs := flag.NewFlagSet("install-macos", flag.ExitOnError)
bin := fs.String("bin", "", "path to verae-print binary (default: this executable)")
_ = fs.Parse(args)
exe := *bin
if exe == "" {
var err error
exe, err = os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
exe, _ = filepath.EvalSymlinks(exe)
}
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
destDir := filepath.Join(home, "Library", "PDF Services")
if err := os.MkdirAll(destDir, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
items := []struct {
name string
channel string
}{
{"Timestamp with Verae", "verae"},
{"Timestamp with Verae (Zapier)", "zapier"},
}
for _, it := range items {
appDir := filepath.Join(destDir, it.name+".app", "Contents", "MacOS")
if err := os.MkdirAll(appDir, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
plist := filepath.Join(destDir, it.name+".app", "Contents", "Info.plist")
plistBody := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>runner</string>
<key>CFBundleIdentifier</key><string>ai.verae.print.%s</string>
<key>CFBundleName</key><string>%s</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>%s</string>
</dict></plist>
`, strings.ReplaceAll(it.channel, " ", ""), it.name, job.HelperVersion)
if err := os.WriteFile(plist, []byte(plistBody), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
runner := filepath.Join(appDir, "runner")
script := fmt.Sprintf(`#!/bin/bash
set -euo pipefail
LOG="${HOME}/Library/Logs/verae-print.log"
mkdir -p "$(dirname "$LOG")"
exec >>"$LOG" 2>&1
echo "$(date -u +%%Y-%%m-%%dT%%H:%%MZ) channel=%s args: $*"
PDF=""
for a in "$@"; do
case "$a" in
*.pdf|*.PDF) PDF="$a" ;;
esac
done
if [[ -z "${PDF}" && $# -ge 1 ]]; then PDF="$1"; fi
if [[ -z "${PDF}" || ! -f "${PDF}" ]]; then
osascript -e 'display notification "No PDF received from Print dialog" with title "Verae Timestamp"' || true
exit 1
fi
%q stamp -channel %s -open "$PDF"
`, it.channel, exe, it.channel)
if err := os.WriteFile(runner, []byte(script), 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println("Installed:", filepath.Join(destDir, it.name+".app"))
}
fmt.Println("Print → PDF → Timestamp with Verae (or Zapier)")
fmt.Println("Binary:", exe)
return 0
}
func notify(title, subtitle string) {
script := fmt.Sprintf(`display notification %q with title "Verae Timestamp" subtitle %q`, subtitle, title)
_ = exec.Command("osascript", "-e", script).Run()
}
func envOr(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func fileExists(p string) bool {
st, err := os.Stat(p)
return err == nil && !st.IsDir()
}

View file

@ -53,7 +53,10 @@
- [ ] Commit `feat(pdf): certificate page and embedded receipt`. - [ ] Commit `feat(pdf): certificate page and embedded receipt`.
### Task 5: macOS v0 — PDF Services ### Task 5: macOS v0 — PDF Services
- [ ] App bundle in `~/Library/PDF Services/` calling the core; Save/Print/Both dialog. - [x] CLI `verae-print stamp` writes `{name}.pdf`, `{name}-verae-timestamp-{RECEIPTID}.pdf`, `{name}.verae.json`.
- [x] Dual channel: `verae` (api.veraetime.net JWT) and `zapier` (zap.verae.ai xVerae- + attachPdf).
- [x] `verae-print install-macos` installs PDF Services apps (Verae + Zapier).
- [ ] Optional Save/Print/Both dialog (v0 auto-saves to `~/Documents/Verae Timestamps`).
- [ ] Commit `feat(macos): PDF Services entry`. - [ ] Commit `feat(macos): PDF Services entry`.
### Task 6: Spool protocol + helper ### Task 6: Spool protocol + helper

View file

@ -26,7 +26,9 @@ Provide a "Verae Timestamp" printer on macOS, Linux, and Windows. Printing to it
- **Embedding without breaking the hash.** If the receipt is embedded, it is appended as a PDF *incremental update* (attachment + annotation/certificate page). The original byte range is unchanged, so `sha256(first N bytes)` still matches. The receipt records `N`. - **Embedding without breaking the hash.** If the receipt is embedded, it is appended as a PDF *incremental update* (attachment + annotation/certificate page). The original byte range is unchanged, so `sha256(first N bytes)` still matches. The receipt records `N`.
- **Per-page hashes.** Each page is extracted as a single-page PDF (deterministic extraction via the same library/version) and hashed; the list is included in a canonical manifest whose SHA-256 is also stamped. This allows proving individual pages. - **Per-page hashes.** Each page is extracted as a single-page PDF (deterministic extraction via the same library/version) and hashed; the list is included in a canonical manifest whose SHA-256 is also stamped. This allows proving individual pages.
## 4. Verae Timestamp API (as used today by Datacube-Fuse) ## 4. Verae Timestamp API (dual channel)
### 4.1 Verae Time (default — non-Zapier)
| Call | Purpose | | Call | Purpose |
|---|---| |---|---|
@ -35,8 +37,18 @@ Provide a "Verae Timestamp" printer on macOS, Linux, and Windows. Printing to it
| `GET {base}/api/hash/{digest}` | Look up existing stamp | | `GET {base}/api/hash/{digest}` | Look up existing stamp |
| `POST {base}/api/verify` | Verify | | `POST {base}/api/verify` | Verify |
- Default base: `https://api.veraetime.net`; `Authorization: Bearer <token>` from `~/.verae/auth.json` (keys `access_token` / `auth_token` / `id_token`). - Default base: `https://api.veraetime.net`
- **Open question:** endpoint/format for a downloadable certificate PDF. Until confirmed, the helper renders its own certificate page from the receipt JSON (digest, time, certificate id, verify URL, QR code). - Auth: `Authorization: Bearer <token>` from `~/.verae/auth.properties` (keys `access_token` / `auth_token` / `id_token`)
- Certificate PDF: render locally from receipt when the API does not return a PDF body
### 4.2 Zapier / edge channel
| Call | Purpose |
|---|---|
| `POST https://zap.verae.ai/v1/timestamp` body `{sha256, attachPdf:true, wait:true, documentName}` | Stamp + optional registration PDF |
- Auth: `x-api-key: xVerae-…` (or Bearer) from Keychain / `~/.verae/xverae-token` / env `XVERAE_TOKEN`
- Certificate PDF: decode `pdfBase64` from the JSON response (or raw PDF when `Accept: application/pdf`)
## 5. Architecture ## 5. Architecture
@ -60,7 +72,10 @@ App ──▶ Print │ macOS/Linux: CUPS queue "Verae Timestamp" │
### 5.1 macOS ### 5.1 macOS
- **v0 (fast path):** PDF Services entry — "Timestamp with Verae" in the Print dialog's PDF menu. Installed to `~/Library/PDF Services/` as an app that receives the PDF path. No driver needed. - **v0 (fast path):** PDF Services entry — "Timestamp with Verae" in the Print dialog's PDF menu. Installed to `~/Library/PDF Services/` as an app/script that receives the PDF path. No CUPS driver needed.
- Default channel: **Verae Time** (`api.veraetime.net` + MCP JWT from `~/.verae/auth.properties`).
- Optional second menu item **Timestamp with Verae (Zapier)** or `--channel=zapier`: `zap.verae.ai` + `xVerae-` token + `attachPdf: true`.
- Saves to `~/Documents/Verae Timestamps/` by default (override with `--out-dir` / preference).
- **v1:** CUPS queue with custom backend `/usr/libexec/cups/backend/verae` + PPD (or IPP Everywhere). The backend is sandboxed and cannot show UI, so it writes to `/Library/Application Support/Verae/spool/<user>/` and exits; the menu-bar helper (LaunchAgent) picks it up. - **v1:** CUPS queue with custom backend `/usr/libexec/cups/backend/verae` + PPD (or IPP Everywhere). The backend is sandboxed and cannot show UI, so it writes to `/Library/Application Support/Verae/spool/<user>/` and exits; the menu-bar helper (LaunchAgent) picks it up.
- Signed + notarized `.pkg`. - Signed + notarized `.pkg`.
@ -85,16 +100,21 @@ A single local **IPP Everywhere printer service** (e.g. built on PAPPL or a Go I
## 6. Outputs ## 6. Outputs
For a job named `Contract.pdf`: For a print job whose document name is `Contract` (extension stripped for the stem):
| File | Content | | File | Content |
|---|---| |---|---|
| `Contract.pdf` | Exact captured bytes (the proven artifact) | | `Contract.pdf` | Exact captured bytes (the proven artifact) |
| `Contract.verae.json` | Receipt: doc sha256, page sha256 list, manifest sha256, Verae response(s), time, base URL, helper version, original byte length | | `Contract-verae-timestamp-{RECEIPTID}.pdf` | Verae registration / certificate PDF only |
| `Contract.stamped.pdf` (optional) | Original bytes + incremental update containing certificate page and `Contract.verae.json` as attachment | | `Contract.verae.json` | Receipt sidecar: doc sha256, receipt id, channel, API host, time, helper version, original byte length |
| `Contract.certificate.pdf` (optional) | Certificate page alone (from API if available, else rendered) |
Certificate page: document name, SHA-256, Verae timestamp time (UTC + local), certificate/receipt id, verify URL + QR code, page-hash list (first N, rest in attachment). `RECEIPTID` is the Verae receipt / certificate CID, sanitized for filenames (safe charset). Long CIDs are kept in full when under filesystem limits.
**macOS v0 naming is fixed to the table above** (approved 2026-10-03). Later optional embed/combined outputs may still use `.stamped.pdf` but are out of scope for v0.
Certificate PDF source by channel:
- **Zapier channel:** `pdfBase64` from `POST https://zap.verae.ai/v1/timestamp` with `attachPdf: true`.
- **Verae Time / default:** stamp via `api.veraetime.net`; use API/MCP registration PDF when available, otherwise render a local certificate page from the receipt JSON (document name, SHA-256, time UTC + local, receipt id, verify URL).
## 7. Settings ## 7. Settings

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/VeraeLLC/PrintToVeraeTimeStamp
go 1.26.1

94
internal/auth/auth.go Normal file
View file

@ -0,0 +1,94 @@
package auth
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// Channel selects which stamp API to use.
type Channel string
const (
ChannelVerae Channel = "verae" // api.veraetime.net + MCP JWT
ChannelZapier Channel = "zapier" // zap.verae.ai + xVerae-
)
// ParseChannel maps CLI/env values to a Channel.
func ParseChannel(s string) (Channel, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "verae", "veraetime", "mcp", "default":
return ChannelVerae, nil
case "zapier", "zap", "edge":
return ChannelZapier, nil
default:
return "", fmt.Errorf("unknown channel %q (want verae or zapier)", s)
}
}
// LoadVeraeJWT reads Bearer token from ~/.verae/auth.properties
// (access_token, auth_token, or id_token).
func LoadVeraeJWT() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
path := filepath.Join(home, ".verae", "auth.properties")
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
vals := map[string]string{}
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
vals[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
if err := sc.Err(); err != nil {
return "", err
}
for _, key := range []string{"access_token", "auth_token", "id_token"} {
if t := vals[key]; t != "" {
return t, nil
}
}
return "", fmt.Errorf("no access_token/auth_token/id_token in %s", path)
}
// LoadXVeraeToken loads a long-lived xVerae- token from, in order:
// XVERAE_TOKEN env, ~/.verae/xverae-token file, macOS Keychain service verae.xVeraeToken.
func LoadXVeraeToken() (string, error) {
if t := strings.TrimSpace(os.Getenv("XVERAE_TOKEN")); t != "" {
return t, nil
}
home, err := os.UserHomeDir()
if err == nil {
path := filepath.Join(home, ".verae", "xverae-token")
if b, err := os.ReadFile(path); err == nil {
t := strings.TrimSpace(string(b))
if t != "" {
return t, nil
}
}
}
// macOS Keychain (generic password, account optional)
out, err := exec.Command("security", "find-generic-password", "-s", "verae.xVeraeToken", "-w").Output()
if err == nil {
t := strings.TrimSpace(string(out))
if t != "" {
return t, nil
}
}
return "", fmt.Errorf("no xVerae- token (set XVERAE_TOKEN, ~/.verae/xverae-token, or Keychain verae.xVeraeToken)")
}

View file

@ -0,0 +1,36 @@
package auth
import (
"os"
"path/filepath"
"testing"
)
func TestParseChannel(t *testing.T) {
c, err := ParseChannel("zapier")
if err != nil || c != ChannelZapier {
t.Fatalf("%v %v", c, err)
}
c, err = ParseChannel("")
if err != nil || c != ChannelVerae {
t.Fatalf("%v %v", c, err)
}
}
func TestLoadVeraeJWT(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
dir := filepath.Join(home, ".verae")
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "auth.properties")
body := "# test\naccess_token=tok-abc\nid_token=other\n"
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
got, err := LoadVeraeJWT()
if err != nil || got != "tok-abc" {
t.Fatalf("got %q err %v", got, err)
}
}

39
internal/hash/hash.go Normal file
View file

@ -0,0 +1,39 @@
package hash
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"strings"
)
// FileSHA256 returns the lowercase hex SHA-256 of the file at path.
func FileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// NormalizeDigest lowercases and strips an optional sha256: prefix.
func NormalizeDigest(d string) (string, error) {
s := strings.TrimSpace(strings.ToLower(d))
s = strings.TrimPrefix(s, "sha256:")
if len(s) != 64 {
return "", fmt.Errorf("digest must be 64 hex chars, got %d", len(s))
}
for _, c := range s {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
return "", fmt.Errorf("digest is not hex")
}
}
return s, nil
}

View file

@ -0,0 +1,37 @@
package hash
import (
"os"
"path/filepath"
"testing"
)
func TestFileSHA256(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "a.pdf")
if err := os.WriteFile(p, []byte("%PDF-1.4 test"), 0o600); err != nil {
t.Fatal(err)
}
got, err := FileSHA256(p)
if err != nil {
t.Fatal(err)
}
if len(got) != 64 {
t.Fatalf("len=%d", len(got))
}
again, err := FileSHA256(p)
if err != nil || again != got {
t.Fatalf("not deterministic: %v %s vs %s", err, got, again)
}
}
func TestNormalizeDigest(t *testing.T) {
const hex64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
got, err := NormalizeDigest("SHA256:" + hex64)
if err != nil || got != hex64 {
t.Fatalf("got %q err %v", got, err)
}
if _, err := NormalizeDigest("short"); err == nil {
t.Fatal("expected error")
}
}

179
internal/job/job.go Normal file
View 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
View 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")
}
}

103
internal/pdfcert/pdfcert.go Normal file
View file

@ -0,0 +1,103 @@
package pdfcert
import (
"fmt"
"strings"
"time"
)
// Input for a simple one-page Verae registration certificate PDF.
type Input struct {
DocumentName string
SHA256 string
ReceiptID string
CreatedAt string
Merkle string
VerifyURL string
Channel string
}
// Build returns a minimal PDF (no external deps) describing the stamp.
func Build(in Input) ([]byte, error) {
created := in.CreatedAt
if created == "" {
created = time.Now().UTC().Format(time.RFC3339)
}
local := ""
if t, err := time.Parse(time.RFC3339, created); err == nil {
local = t.Local().Format(time.RFC1123)
}
verify := in.VerifyURL
if verify == "" && in.SHA256 != "" {
verify = fmt.Sprintf("https://mcp.verae.ai/register.html?sha256=%s&receipt=%s", in.SHA256, in.ReceiptID)
}
lines := []string{
"Verae Timestamp Registration",
"",
"Document: " + sanitize(in.DocumentName),
"SHA-256: " + in.SHA256,
"Receipt ID: " + in.ReceiptID,
"UTC: " + created,
}
if local != "" {
lines = append(lines, "Local: "+local)
}
if in.Merkle != "" {
lines = append(lines, "Merkle/block: "+in.Merkle)
}
if in.Channel != "" {
lines = append(lines, "Channel: "+in.Channel)
}
lines = append(lines, "Verify: "+verify)
lines = append(lines, "", "Digests only — document bytes were not uploaded.")
return buildTextPDF(lines)
}
func sanitize(s string) string {
s = strings.ReplaceAll(s, "(", "\\(")
s = strings.ReplaceAll(s, ")", "\\)")
s = strings.ReplaceAll(s, "\\", "\\\\")
return s
}
// Minimal PDF 1.4 with Helvetica text lines.
func buildTextPDF(lines []string) ([]byte, error) {
var content strings.Builder
content.WriteString("BT /F1 12 Tf 50 750 Td 14 TL\n")
for i, line := range lines {
if i > 0 {
content.WriteString("T*\n")
}
content.WriteString("(")
content.WriteString(sanitize(line))
content.WriteString(") Tj\n")
}
content.WriteString("ET\n")
stream := content.String()
objects := []string{
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(stream), stream),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
}
var body strings.Builder
body.WriteString("%PDF-1.4\n")
offsets := make([]int, len(objects)+1)
for i, obj := range objects {
offsets[i+1] = body.Len()
fmt.Fprintf(&body, "%d 0 obj\n%s\nendobj\n", i+1, obj)
}
xrefStart := body.Len()
fmt.Fprintf(&body, "xref\n0 %d\n", len(objects)+1)
body.WriteString("0000000000 65535 f \n")
for i := 1; i <= len(objects); i++ {
fmt.Fprintf(&body, "%010d 00000 n \n", offsets[i])
}
fmt.Fprintf(&body, "trailer << /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objects)+1, xrefStart)
return []byte(body.String()), nil
}

296
internal/veraeapi/client.go Normal file
View file

@ -0,0 +1,296 @@
package veraeapi
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
)
const (
defaultVeraeBase = "https://api.veraetime.net"
defaultZapierBase = "https://zap.verae.ai"
defaultPoll = 250 * time.Millisecond
defaultMaxPolls = 80
)
// Receipt is the stamp result used by the print helper.
type Receipt struct {
ReceiptID string
SHA256 string
CreatedAt string
Merkle string
Channel auth.Channel
BaseURL string
PDF []byte // registration / certificate PDF when available
PDFFilename string
RawResponse json.RawMessage
}
// Client stamps digests on Verae Time or the Zapier edge.
type Client struct {
Channel auth.Channel
VeraeBase string
ZapierBase string
VeraeToken string // JWT
XVeraeToken string
HTTPDo func(*http.Request) (*http.Response, error)
PollInterval time.Duration
MaxPolls int
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
if c.HTTPDo != nil {
return c.HTTPDo(req)
}
return http.DefaultClient.Do(req)
}
func (c *Client) veraeBase() string {
if c.VeraeBase != "" {
return strings.TrimRight(c.VeraeBase, "/")
}
return defaultVeraeBase
}
func (c *Client) zapierBase() string {
if c.ZapierBase != "" {
return strings.TrimRight(c.ZapierBase, "/")
}
return defaultZapierBase
}
// Stamp submits digest and waits for a receipt. documentName is for PDF metadata.
func (c *Client) Stamp(digest, documentName string) (Receipt, error) {
switch c.Channel {
case auth.ChannelZapier:
return c.stampZapier(digest, documentName)
default:
return c.stampVerae(digest)
}
}
func (c *Client) stampVerae(digest string) (Receipt, error) {
base := c.veraeBase()
body, _ := json.Marshal(map[string]any{
"data": digest,
"digest": digest,
"hashAlg": "SHA256",
"restamp": false,
})
req, err := http.NewRequest(http.MethodPost, base+"/api/timestamp", bytes.NewReader(body))
if err != nil {
return Receipt{}, err
}
req.Header.Set("Content-Type", "application/json")
if c.VeraeToken != "" {
req.Header.Set("Authorization", "Bearer "+c.VeraeToken)
}
res, err := c.do(req)
if err != nil {
return Receipt{}, err
}
defer res.Body.Close()
raw, err := io.ReadAll(res.Body)
if err != nil {
return Receipt{}, err
}
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
if res.StatusCode == http.StatusOK {
cid := str(parsed["certificate"])
if cid == "" {
cid = str(parsed["receipt_id"])
}
if cid != "" || truthy(parsed["existing"]) {
if cid == "" {
cid = str(parsed["certificate"])
}
return Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: str(parsed["timestamp"]),
Merkle: str(parsed["blockHash"]),
Channel: auth.ChannelVerae,
BaseURL: base,
RawResponse: raw,
}, nil
}
}
if res.StatusCode >= 300 {
return Receipt{}, fmt.Errorf("verae stamp HTTP %d: %s", res.StatusCode, truncate(raw, 400))
}
jobID := str(parsed["jobId"])
if jobID == "" {
return Receipt{}, fmt.Errorf("verae stamp: missing jobId in %s", truncate(raw, 400))
}
return c.pollVerae(base, digest, jobID, raw)
}
func (c *Client) pollVerae(base, digest, jobID string, firstRaw []byte) (Receipt, error) {
interval := c.PollInterval
if interval <= 0 {
interval = defaultPoll
}
max := c.MaxPolls
if max <= 0 {
max = defaultMaxPolls
}
statusURL := base + "/api/status/" + jobID
var last []byte = firstRaw
for i := 0; i < max; i++ {
time.Sleep(interval)
req, err := http.NewRequest(http.MethodGet, statusURL, nil)
if err != nil {
return Receipt{}, err
}
if c.VeraeToken != "" {
req.Header.Set("Authorization", "Bearer "+c.VeraeToken)
}
res, err := c.do(req)
if err != nil {
return Receipt{}, err
}
raw, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return Receipt{}, err
}
last = raw
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
st := strings.ToLower(str(parsed["status"]))
if st == "failed" || st == "error" {
return Receipt{}, fmt.Errorf("verae job %s failed: %s", jobID, truncate(raw, 400))
}
cid := str(parsed["receipt_id"])
if cid == "" {
cid = str(parsed["certificate"])
}
if cid == "" {
if r, ok := parsed["result"].(map[string]any); ok {
cid = str(r["receipt_id"])
if cid == "" {
cid = str(r["certificate"])
}
}
}
if cid != "" || st == "completed" || st == "done" {
if cid == "" {
cid = jobID
}
return Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: firstNonEmpty(str(parsed["timestamp"]), str(parsed["created_at"]), time.Now().UTC().Format(time.RFC3339)),
Merkle: firstNonEmpty(str(parsed["blockHash"]), str(parsed["merkle"])),
Channel: auth.ChannelVerae,
BaseURL: base,
RawResponse: last,
}, nil
}
}
return Receipt{}, fmt.Errorf("verae job %s timed out; last=%s", jobID, truncate(last, 400))
}
func (c *Client) stampZapier(digest, documentName string) (Receipt, error) {
base := c.zapierBase()
body, _ := json.Marshal(map[string]any{
"sha256": digest,
"attachPdf": true,
"wait": true,
"documentName": documentName,
})
req, err := http.NewRequest(http.MethodPost, base+"/v1/timestamp", bytes.NewReader(body))
if err != nil {
return Receipt{}, err
}
req.Header.Set("Content-Type", "application/json")
if c.XVeraeToken != "" {
req.Header.Set("x-api-key", c.XVeraeToken)
}
req.Header.Set("x-billing-channel", "api")
res, err := c.do(req)
if err != nil {
return Receipt{}, err
}
defer res.Body.Close()
raw, err := io.ReadAll(res.Body)
if err != nil {
return Receipt{}, err
}
if res.StatusCode >= 300 {
return Receipt{}, fmt.Errorf("zapier stamp HTTP %d: %s", res.StatusCode, truncate(raw, 400))
}
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
cid := firstNonEmpty(str(parsed["receipt_id"]), str(parsed["certificate"]))
var pdf []byte
if b64 := str(parsed["pdfBase64"]); b64 != "" {
pdf, err = decodeBase64(b64)
if err != nil {
return Receipt{}, fmt.Errorf("pdfBase64: %w", err)
}
}
return Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: firstNonEmpty(str(parsed["created_at"]), str(parsed["timestamp"]), time.Now().UTC().Format(time.RFC3339)),
Merkle: firstNonEmpty(str(parsed["merkle"]), str(parsed["blockHash"])),
Channel: auth.ChannelZapier,
BaseURL: base,
PDF: pdf,
PDFFilename: str(parsed["pdfFilename"]),
RawResponse: raw,
}, nil
}
func str(v any) string {
switch t := v.(type) {
case string:
return t
case fmt.Stringer:
return t.String()
default:
return ""
}
}
func truthy(v any) bool {
switch t := v.(type) {
case bool:
return t
case string:
return t != "" && t != "false" && t != "0"
default:
return false
}
}
func firstNonEmpty(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}
func truncate(b []byte, n int) string {
if len(b) <= n {
return string(b)
}
return string(b[:n]) + "…"
}
func decodeBase64(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}

View file

@ -0,0 +1,70 @@
package veraeapi
import (
"encoding/base64"
"io"
"net/http"
"strings"
"testing"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
)
const digest64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
func TestStampVeraeImmediateCertificate(t *testing.T) {
c := &Client{
Channel: auth.ChannelVerae,
VeraeBase: "https://api.example.test",
VeraeToken: "tok",
HTTPDo: func(req *http.Request) (*http.Response, error) {
if req.URL.Path != "/api/timestamp" {
t.Fatalf("path %s", req.URL.Path)
}
body := `{"certificate":"bafyTestReceipt","timestamp":"2026-10-03T00:00:00Z","existing":true}`
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(body)),
Header: make(http.Header),
}, nil
},
}
r, err := c.Stamp(digest64, "Doc.pdf")
if err != nil {
t.Fatal(err)
}
if r.ReceiptID != "bafyTestReceipt" {
t.Fatalf("receipt %q", r.ReceiptID)
}
}
func TestStampZapierAttachPdf(t *testing.T) {
pdf := []byte("%PDF-1.4 cert")
b64 := base64.StdEncoding.EncodeToString(pdf)
c := &Client{
Channel: auth.ChannelZapier,
ZapierBase: "https://zap.example.test",
XVeraeToken: "xVerae-test",
HTTPDo: func(req *http.Request) (*http.Response, error) {
if req.URL.Path != "/v1/timestamp" {
t.Fatalf("path %s", req.URL.Path)
}
if req.Header.Get("x-api-key") != "xVerae-test" {
t.Fatal("missing x-api-key")
}
body := `{"receipt_id":"cid123","pdfBase64":"` + b64 + `","pdfFilename":"reg.pdf"}`
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(body)),
Header: make(http.Header),
}, nil
},
}
r, err := c.Stamp(digest64, "Contract.pdf")
if err != nil {
t.Fatal(err)
}
if r.ReceiptID != "cid123" || string(r.PDF) != string(pdf) {
t.Fatalf("%+v", r)
}
}