294 lines
8.4 KiB
Go
294 lines
8.4 KiB
Go
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 "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)
|
|
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 version
|
|
|
|
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")
|
|
_ = 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
|
|
}
|
|
|
|
// 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 {
|
|
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 runInstallPrinter() int {
|
|
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 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()
|
|
}
|