PrintToVeraeTimeStamp/cmd/verae-print/main.go

320 lines
9.5 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
func main() {
// verae-printhelper.exe (GUI subsystem, no console) defaults to "serve".
if len(os.Args) < 2 && isHelperExe() {
os.Args = append(os.Args, "serve")
}
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "stamp":
os.Exit(runStamp(os.Args[2:]))
case "serve":
os.Exit(runServe(os.Args[2:]))
case "ipp-selftest":
os.Exit(runIPPSelftest(os.Args[2:]))
case "setup", "login":
os.Exit(runSetup())
case "install-macos":
os.Exit(runInstallMacOS(os.Args[2:]))
case "install-printer":
os.Exit(runInstallPrinter())
case "version", "-version", "--version":
fmt.Println(job.HelperVersion)
if len(os.Args) > 2 {
fmt.Println("logs:", vlog.Dir())
}
case "help", "-h", "--help":
usage()
default:
// PDF Services / drop-target may invoke the binary with the file as argv[1]
if fileExists(os.Args[1]) {
os.Exit(runStamp([]string{"-channel", string(auth.PreferredChannel()), 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 setup # first run: create customer account + save xVerae- signing key
verae-print stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] <file.pdf>
verae-print install-printer # Destination list: Verae Timestamping (recommended)
verae-print install-macos # also install PDF Services menu items (legacy)
verae-print serve [--addr 127.0.0.1:8631] [--no-ipp] [--no-watch] [--debug]
# Windows print helper: IPP loopback printer + fallback watcher
verae-print ipp-selftest [--addr 127.0.0.1:8631] # probe the helper like Windows does
verae-print version
Debug: set VERAE_DEBUG=1 (or --debug) for full IPP request dumps. Logs: see "verae-print version -v".
First install opens https://customer.verae.ai/portal/ so you can create an account
and mint an xVerae- signing key (Security tab). The key is stored in Keychain.
Channels:
zapier (default) zap.verae.ai + customer xVerae- signing key
verae api.veraetime.net + ~/.verae/auth.properties JWT
Outputs in --out-dir (default ~/Documents/Verae Timestamps):
{name}.pdf
{name}-verae-timestamp-{RECEIPTID}.pdf
{name}.verae.json
`, job.HelperVersion)
}
func runSetup() int {
if auth.HasXVeraeToken() {
fmt.Println("Signing key already saved (xVerae-).")
fmt.Println("Portal:", auth.PortalSignupURL)
return 0
}
if err := auth.RunFirstRunSetup(); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println("Signing key saved. Print → Destination → Verae Timestamping is ready.")
return 0
}
func runStamp(args []string) int {
fs := flag.NewFlagSet("stamp", flag.ExitOnError)
channel := fs.String("channel", string(auth.PreferredChannel()), "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)")
skipSetup := fs.Bool("skip-setup", false, "do not open first-run account dialog")
jobFlag := fs.String("job-id", "", "debug job id (for log correlation)")
_ = fs.Parse(args)
if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "stamp: PDF path required")
return 2
}
pdfPath := fs.Arg(0)
jobID := *jobFlag
if jobID == "" {
jobID = vlog.NewJobID("cli")
}
vlog.Init("cli", false)
lj := vlog.Job(jobID)
lj.Printf("stamp start pdf=%q channel=%s out=%q name=%q", pdfPath, *channel, *outDir, *name)
ch, err := auth.ParseChannel(*channel)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 2
}
// First-run: customer account + xVerae- signing key (preferred path)
if ch == auth.ChannelZapier && !auth.HasXVeraeToken() {
if *skipSetup {
fmt.Fprintf(os.Stderr, "no xVerae- signing key — run: verae-print setup\nPortal: %s\n", auth.PortalSignupURL)
return 1
}
if err := auth.EnsureReady(); err != nil {
fmt.Fprintln(os.Stderr, err)
notify("Create a Verae customer account to get your signing key", auth.PortalSignupURL)
return 1
}
}
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.Fprintf(os.Stderr, "%v\nTip: run verae-print setup to create a customer account and xVerae- key instead.\n", 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 {
lj.Errorf("stamp failed: %v", err)
fmt.Fprintf(os.Stderr, "stamp failed: %v\n", err)
return 1
}
lj.Printf("stamp ok receipt=%s sha256=%s cert=%q", res.ReceiptID, res.SHA256, res.CertificatePDF)
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 {
openFolderPath(filepath.Dir(res.OriginalPDF))
}
// macOS notification (best-effort)
notify(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
return 0
}
func runInstallPrinter() int {
if runtime.GOOS == "windows" {
return runInstallPrinterWindows()
}
script := filepath.Join(repoRootHint(), "platform", "macos", "ipp", "install-ipp-printer.sh")
if _, err := os.Stat(script); err != nil {
// fall back next to executable
exe, _ := os.Executable()
exe, _ = filepath.EvalSymlinks(exe)
script = filepath.Join(filepath.Dir(exe), "..", "platform", "macos", "ipp", "install-ipp-printer.sh")
}
if abs, err := filepath.Abs(script); err == nil {
script = abs
}
cmd := exec.Command("/bin/bash", script)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "install-printer failed: %v\n(run platform/macos/ipp/install-ipp-printer.sh)\n", err)
return 1
}
return 0
}
func repoRootHint() string {
// Prefer known research path; else cwd
candidates := []string{
"/Users/marchon/research/PrintToVeraeTimeStamp",
".",
}
for _, c := range candidates {
if st, err := os.Stat(filepath.Join(c, "platform", "macos", "ipp", "install-ipp-printer.sh")); err == nil && !st.IsDir() {
return c
}
}
return "."
}
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 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()
}