diff --git a/.gitignore b/.gitignore index 55dde68..d17d79e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ *.msi .DS_Store node_modules/ +/build/ diff --git a/README.md b/README.md index 5f07e62..b5175d9 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,53 @@ # PrintToVeraeTimeStamp -macOS Print → PDF helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`. +Print / CLI helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`. Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine. ## Install +Downloads: [printit.verae.ai](https://printit.verae.ai/) (macOS `.pkg`, Windows `.zip`, Linux `.tar.gz`). + +**macOS (recommended):** open `VeraePrintStamp-macos.pkg`. First launch opens **customer.verae.ai** for an **xVerae-** signing key. + +**Windows (GUI installer):** + +```text +Download VeraePrintStamp-windows-amd64-setup.exe from printit.verae.ai +Run the installer → Finish with “Run setup” → paste your xVerae- key +verae-print stamp .\document.pdf +``` + +**Debian / Ubuntu:** + +```bash +sudo apt install ./verae-print_amd64.deb +verae-print setup +verae-print stamp document.pdf +``` + +**Linux (tarball, any distro):** + +```bash +tar xzf VeraePrintStamp-linux-amd64.tar.gz +cd VeraePrintStamp-linux-amd64-* +sudo ./install.sh # CLI + optional CUPS queue +verae-print setup +verae-print stamp document.pdf +``` + +From source (macOS): + ```bash cd /Users/marchon/research/PrintToVeraeTimeStamp go build -o "$HOME/bin/verae-print" ./cmd/verae-print +verae-print setup # create account + save xVerae- signing key verae-print install-printer # Print → Destination → Verae Timestamping ``` Optional (buried PDF submenu): `verae-print install-macos` -Ensure `$HOME/bin` is on your `PATH`. Refresh Verae Time login when the JWT expires: +Ensure `$HOME/bin` is on your `PATH`. For the legacy Verae Time JWT channel: ```bash uvx verae-mcp-timestamping-server setup @@ -53,8 +86,8 @@ Default output directory: `~/Documents/Verae Timestamps/` Channels: -- `verae` (default) — `api.veraetime.net` + JWT in `~/.verae/auth.properties` -- `zapier` — `zap.verae.ai` + `xVerae-` + `attachPdf:true` +- `zapier` (default after setup) — `zap.verae.ai` + customer `xVerae-` signing key + `attachPdf:true` +- `verae` — `api.veraetime.net` + JWT in `~/.verae/auth.properties` (opt-in: `-channel=verae`) ## Signed macOS installer (.pkg) @@ -64,7 +97,7 @@ Same FlightLookup Developer ID + notarization path as Verae Timestamping.app (`~ cd /Users/marchon/research/PrintToVeraeTimeStamp chmod +x platform/macos/pkg/build-pkg.sh ./platform/macos/pkg/build-pkg.sh -# → build/macos-pkg/dist/VeraePrintStamp-0.1.0.pkg +# → build/macos-pkg/dist/VeraePrintStamp-0.1.2.pkg # → build/macos-pkg/dist/VeraePrintStamp-macos.pkg ``` @@ -79,9 +112,20 @@ The pkg installs: Requires: `Developer ID Application` + `Developer ID Installer` (FlightLookup), App Store Connect API key in `signing.env`, Node for bundling the registration PDF builder. +## Windows / Linux release archives + +```bash +./scripts/build-win-linux.sh +./platform/windows/build-installer.sh # NSIS GUI setup.exe (needs makensis) +# on Debian/Ubuntu host: +VERSION=0.1.2 BIN_AMD64=... BIN_ARM64=... ./platform/linux/deb/build-deb.sh all +``` + +Windows ships an NSIS GUI installer; stamp via CLI after setup (virtual printer still on the [spec](docs/superpowers/specs/2026-10-03-print-to-verae-timestamp.md) roadmap). Debian/Ubuntu use `verae-print_*.deb`; other distros use the tarball + `install.sh`. + ## Status -macOS Destination printer + branded registration PDF work locally. Signed `.pkg` build script is under `platform/macos/pkg/`. 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). +macOS Destination printer (notarized `.pkg`), Windows/Linux CLI packages, and branded registration PDF are published at [printit.verae.ai](https://printit.verae.ai/). ## Contributing diff --git a/cmd/verae-print/main.go b/cmd/verae-print/main.go index 3e11f78..07dce1b 100644 --- a/cmd/verae-print/main.go +++ b/cmd/verae-print/main.go @@ -21,6 +21,8 @@ func main() { 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": @@ -30,9 +32,9 @@ func main() { 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]})) + // 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() @@ -44,14 +46,18 @@ 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] 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: - verae (default) api.veraetime.net + ~/.verae/auth.properties JWT - zapier zap.verae.ai + xVerae- token (XVERAE_TOKEN / ~/.verae/xverae-token / Keychain) + 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 @@ -60,12 +66,27 @@ Outputs in --out-dir (default ~/Documents/Verae Timestamps): `, 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", envOr("VERAE_PRINT_CHANNEL", "verae"), "verae | zapier") + 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") @@ -79,6 +100,19 @@ func runStamp(args []string) int { 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: @@ -91,7 +125,7 @@ func runStamp(args []string) int { default: tok, err := auth.LoadVeraeJWT() if err != nil { - fmt.Fprintln(os.Stderr, err) + 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 diff --git a/internal/auth/auth.go b/internal/auth/auth.go index c794b3e..f696296 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -92,3 +92,72 @@ func LoadXVeraeToken() (string, error) { } return "", fmt.Errorf("no xVerae- token (set XVERAE_TOKEN, ~/.verae/xverae-token, or Keychain verae.xVeraeToken)") } + +// HasXVeraeToken reports whether a customer signing key is already stored. +func HasXVeraeToken() bool { + _, err := LoadXVeraeToken() + return err == nil +} + +// HasVeraeJWT reports whether a Verae Time JWT is available. +func HasVeraeJWT() bool { + _, err := LoadVeraeJWT() + return err == nil +} + +// PreferredChannel picks the print product path: customer xVerae- on zap.verae.ai. +// Override with VERAE_PRINT_CHANNEL=verae for the legacy JWT channel. +func PreferredChannel() Channel { + if env := strings.ToLower(strings.TrimSpace(os.Getenv("VERAE_PRINT_CHANNEL"))); env != "" { + if c, err := ParseChannel(env); err == nil { + return c + } + } + return ChannelZapier +} + +const ( + KeychainService = "verae.xVeraeToken" + PortalSignupURL = "https://customer.verae.ai/portal/" +) + +// SaveXVeraeToken stores the customer signing key in ~/.verae/xverae-token (0600) +// and macOS Keychain service verae.xVeraeToken. +func SaveXVeraeToken(token string) error { + token = strings.TrimSpace(token) + if token == "" { + return fmt.Errorf("empty token") + } + if !strings.HasPrefix(token, "xVerae-") && !strings.HasPrefix(token, "key-") { + return fmt.Errorf("token should start with xVerae- (or legacy key-)") + } + home, err := os.UserHomeDir() + if err != nil { + return err + } + dir := filepath.Join(home, ".verae") + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + path := filepath.Join(dir, "xverae-token") + if err := os.WriteFile(path, []byte(token+"\n"), 0o600); err != nil { + return err + } + // Keychain (best-effort): delete old, add new + account := os.Getenv("USER") + if account == "" { + account = "verae" + } + _ = exec.Command("security", "delete-generic-password", "-s", KeychainService, "-a", account).Run() + if err := exec.Command("security", "add-generic-password", + "-a", account, + "-s", KeychainService, + "-w", token, + "-T", "", // allow all apps that know the service; print helper needs it + "-U", + ).Run(); err != nil { + // Non-fatal if file write succeeded + return nil + } + return nil +} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 9ada3a8..aa5bf00 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -17,6 +17,18 @@ func TestParseChannel(t *testing.T) { } } +func TestPreferredChannelDefaultsToZapier(t *testing.T) { + t.Setenv("VERAE_PRINT_CHANNEL", "") + t.Setenv("XVERAE_TOKEN", "") + if c := PreferredChannel(); c != ChannelZapier { + t.Fatalf("got %v want zapier (customer signing key path)", c) + } + t.Setenv("VERAE_PRINT_CHANNEL", "verae") + if c := PreferredChannel(); c != ChannelVerae { + t.Fatalf("got %v want verae from env", c) + } +} + func TestLoadVeraeJWT(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) diff --git a/internal/auth/setup_darwin.go b/internal/auth/setup_darwin.go new file mode 100644 index 0000000..fd807d4 --- /dev/null +++ b/internal/auth/setup_darwin.go @@ -0,0 +1,106 @@ +//go:build darwin + +package auth + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// RunFirstRunSetup guides the user through customer.verae.ai signup and +// xVerae- signing-key paste. Interactive (osascript dialogs). +func RunFirstRunSetup() error { + if HasXVeraeToken() { + return nil + } + + btn, err := osaButton( + "Verae Print Stamp needs a customer signing key before it can timestamp documents.\n\nCreate a free account on customer.verae.ai, open Security, and create an xVerae- token.", + "Verae customer account", + []string{"Create account…", "I already have a key", "Cancel"}, + "Create account…", + ) + if err != nil { + return err + } + switch btn { + case "Cancel": + return fmt.Errorf("setup cancelled") + case "Create account…": + _ = exec.Command("open", PortalSignupURL).Start() + btn2, err2 := osaButton( + "In the browser:\n1. Create your account (or sign in)\n2. Open the Security tab\n3. Create an xVerae- token\n4. Copy it once — it is shown only once\n\nClick Continue when you have copied the key.", + "Create your signing key", + []string{"Continue", "Cancel"}, + "Continue", + ) + if err2 != nil || btn2 == "Cancel" { + return fmt.Errorf("setup cancelled") + } + } + + token, err := osaPrompt( + "Paste your xVerae- signing key from customer.verae.ai → Security.\n\nIt starts with xVerae- and is shown only once when created.", + "Paste signing key", + "", + ) + if err != nil { + return err + } + token = strings.TrimSpace(token) + if token == "" { + return fmt.Errorf("no key pasted") + } + if err := SaveXVeraeToken(token); err != nil { + return err + } + _, _ = osaButton( + "Signing key saved.\n\nYou can Print → Destination → Verae Timestamping from any app.\nKeys are stored in Keychain (verae.xVeraeToken) and ~/.verae/xverae-token.", + "Verae Print Stamp", + []string{"OK"}, + "OK", + ) + return nil +} + +func osaButton(message, title string, buttons []string, defaultBtn string) (string, error) { + quoted := make([]string, len(buttons)) + for i, b := range buttons { + quoted[i] = fmt.Sprintf("%q", b) + } + script := fmt.Sprintf( + `button returned of (display dialog %q with title %q buttons {%s} default button %q)`, + message, title, strings.Join(quoted, ", "), defaultBtn, + ) + out, err := exec.Command("osascript", "-e", script).Output() + if err != nil { + return "", fmt.Errorf("dialog cancelled") + } + return strings.TrimSpace(string(out)), nil +} + +func osaPrompt(message, title, defaultAnswer string) (string, error) { + script := fmt.Sprintf( + `text returned of (display dialog %q with title %q default answer %q with hidden answer buttons {"Cancel", "Save key"} default button "Save key")`, + message, title, defaultAnswer, + ) + out, err := exec.Command("osascript", "-e", script).Output() + if err != nil { + return "", fmt.Errorf("prompt cancelled") + } + return strings.TrimSpace(string(out)), nil +} + +// EnsureReady runs first-run setup when no signing key is present. +func EnsureReady() error { + if HasXVeraeToken() { + return nil + } + // Headless (no GUI session): fail with instructions + if os.Getenv("SSH_CONNECTION") != "" && os.Getenv("DISPLAY") == "" { + return fmt.Errorf("no xVerae- signing key — run: verae-print setup\nOr open %s , create an account, mint a key under Security, then:\n verae-print setup", PortalSignupURL) + } + return RunFirstRunSetup() +} diff --git a/internal/auth/setup_other.go b/internal/auth/setup_other.go new file mode 100644 index 0000000..0a759c9 --- /dev/null +++ b/internal/auth/setup_other.go @@ -0,0 +1,79 @@ +//go:build !darwin + +package auth + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "runtime" + "strings" +) + +// RunFirstRunSetup opens the customer portal and prompts for an xVerae- signing key on stdin. +func RunFirstRunSetup() error { + fmt.Println("Verae Print Stamp — first-run setup") + fmt.Println() + fmt.Println("1. Create a free account (or sign in):") + fmt.Println(" ", PortalSignupURL) + fmt.Println("2. Open Security → create an xVerae- token") + fmt.Println("3. Paste the token below (starts with xVerae-)") + fmt.Println() + _ = openBrowser(PortalSignupURL) + + fmt.Print("Paste xVerae- signing key: ") + line, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return fmt.Errorf("read token: %w", err) + } + token := strings.TrimSpace(line) + if token == "" { + return fmt.Errorf("empty token — create one at %s then re-run: verae-print setup", PortalSignupURL) + } + if err := SaveXVeraeToken(token); err != nil { + return err + } + fmt.Println("Signing key saved to ~/.verae/xverae-token") + return nil +} + +// EnsureReady requires an xVerae- token on non-macOS. +func EnsureReady() error { + if HasXVeraeToken() { + return nil + } + if !isInteractive() { + return fmt.Errorf("no xVerae- signing key — run: verae-print setup\nPortal: %s", PortalSignupURL) + } + return RunFirstRunSetup() +} + +func isInteractive() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + return (fi.Mode() & os.ModeCharDevice) != 0 +} + +func openBrowser(url string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("cmd", "/c", "start", "", url) + case "linux": + for _, bin := range []string{"xdg-open", "gio", "sensible-browser"} { + if _, err := exec.LookPath(bin); err == nil { + cmd = exec.Command(bin, url) + break + } + } + default: + return fmt.Errorf("unsupported OS") + } + if cmd == nil { + return fmt.Errorf("no browser opener found") + } + return cmd.Start() +} diff --git a/internal/auth/token_save_test.go b/internal/auth/token_save_test.go new file mode 100644 index 0000000..5b340be --- /dev/null +++ b/internal/auth/token_save_test.go @@ -0,0 +1,28 @@ +package auth + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSaveXVeraeToken(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XVERAE_TOKEN", "") // force file path + if err := SaveXVeraeToken("xVerae-testtoken123"); err != nil { + t.Fatal(err) + } + p := filepath.Join(home, ".verae", "xverae-token") + b, err := os.ReadFile(p) + if err != nil { + t.Fatal(err) + } + if string(b) != "xVerae-testtoken123\n" { + t.Fatalf("%q", b) + } + st, _ := os.Stat(p) + if st.Mode().Perm()&0o077 != 0 { + t.Fatalf("perms %v", st.Mode()) + } +} diff --git a/internal/job/job.go b/internal/job/job.go index 16796e4..47de8a5 100644 --- a/internal/job/job.go +++ b/internal/job/job.go @@ -15,7 +15,7 @@ import ( "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi" ) -const HelperVersion = "0.1.0" +const HelperVersion = "0.1.2" // Result paths written for one print job. type Result struct { diff --git a/internal/pdfcert/fallback.go b/internal/pdfcert/fallback.go index e4584d6..a1559ab 100644 --- a/internal/pdfcert/fallback.go +++ b/internal/pdfcert/fallback.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/phpdave11/gofpdf" qrcode "github.com/skip2/go-qrcode" @@ -69,6 +70,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin }, fields[1:]...) } + isHTTP := func(s string) bool { + return strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "http://") || + strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "https://") + } + y := 168.0 for _, fv := range fields { pdf.SetFont("Helvetica", "B", 10) @@ -77,10 +83,20 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin pdf.Cell(pageW-margin*2, 12, fv[0]) y += 14 pdf.SetFont("Helvetica", "", 10) - pdf.SetTextColor(40, 40, 50) + linkURL := "" + if isHTTP(fv[1]) { + linkURL = strings.TrimSpace(fv[1]) + pdf.SetTextColor(74, 46, 140) + } else { + pdf.SetTextColor(40, 40, 50) + } for _, ln := range pdf.SplitLines([]byte(fv[1]), pageW-margin*2-10) { pdf.SetXY(margin, y) - pdf.Cell(pageW-margin*2-10, 12, string(ln)) + if linkURL != "" { + pdf.CellFormat(pageW-margin*2-10, 12, string(ln), "", 0, "L", false, 0, linkURL) + } else { + pdf.Cell(pageW-margin*2-10, 12, string(ln)) + } y += 12 } y += 10 @@ -95,10 +111,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin qrX := pageW - margin - qrSize qrY := pageH - margin - qrSize - 36 pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "") + pdf.LinkString(qrX, qrY, qrSize, qrSize, lookup) pdf.SetFont("Helvetica", "", 8) - pdf.SetTextColor(90, 80, 100) + pdf.SetTextColor(74, 46, 140) pdf.SetXY(qrX, qrY+qrSize+4) - pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "") + pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, lookup) if qclean != nil { defer qclean() } @@ -112,7 +129,13 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin pdf.SetFont("Helvetica", "B", 9) pdf.SetTextColor(42, 24, 56) pdf.SetXY(margin, pageH-36) - pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai") + pdf.Cell(40, 12, "Verae | ") + pdf.SetTextColor(74, 46, 140) + pdf.CellFormat(55, 12, "verae.com", "", 0, "L", false, 0, "https://verae.com") + pdf.SetTextColor(42, 24, 56) + pdf.Cell(12, 12, " | ") + pdf.SetTextColor(74, 46, 140) + pdf.CellFormat(90, 12, "explorer.verae.ai", "", 0, "L", false, 0, "https://explorer.verae.ai/") var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { diff --git a/internal/pdfcert/pdfcert.go b/internal/pdfcert/pdfcert.go index 72e55f5..571c5c8 100644 --- a/internal/pdfcert/pdfcert.go +++ b/internal/pdfcert/pdfcert.go @@ -28,13 +28,21 @@ type Input struct { ByteLength int64 } -// LookupURL builds a public lookup link without mcp.verae.ai. +// LookupURL builds a public VeraeTime verify deep link (same as explorer +// scan-bar → /time, which auto-fills receipt/sha256 and runs Lookup). +// https://explorer.verae.ai/time?receipt=…&sha256=… func LookupURL(sha256, receiptID string) string { sha256 = strings.TrimSpace(sha256) + sha256 = strings.TrimPrefix(strings.ToLower(sha256), "0x") receiptID = strings.TrimSpace(receiptID) - u := "https://explorer.verae.ai/?sha256=" + sha256 + u := "https://explorer.verae.ai/time" + sep := "?" if receiptID != "" { - u += "&receipt=" + receiptID + u += sep + "receipt=" + receiptID + sep = "&" + } + if sha256 != "" { + u += sep + "sha256=" + sha256 } return u } @@ -113,6 +121,7 @@ func buildWithNode(row map[string]any) ([]byte, error) { func findBuildScript() string { candidates := []string{ + "/Library/Application Support/Verae/PrintStamp/registration-pdf/build.mjs", "/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs", filepath.Join("platform", "registration-pdf", "build.mjs"), } @@ -120,6 +129,8 @@ func findBuildScript() string { if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) candidates = append(candidates, + filepath.Join(dir, "registration-pdf", "build.mjs"), + filepath.Join(dir, "..", "registration-pdf", "build.mjs"), filepath.Join(dir, "platform", "registration-pdf", "build.mjs"), filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"), ) diff --git a/internal/pdfcert/pdfcert_test.go b/internal/pdfcert/pdfcert_test.go index d1254d2..14b05d6 100644 --- a/internal/pdfcert/pdfcert_test.go +++ b/internal/pdfcert/pdfcert_test.go @@ -41,14 +41,30 @@ func TestBuildBrandedRegistration(t *testing.T) { if !strings.Contains(s, "explorer.verae.ai") { t.Fatal("expected explorer lookup") } + // Clickable link annotations (jsPDF textWithLink / link, or gofpdf LinkString) + if !strings.Contains(s, "/URI") && !strings.Contains(s, "/URI(") { + if !strings.Contains(s, "URI") || !strings.Contains(s, "explorer.verae.ai/time") { + t.Fatal("expected clickable URI annotation for public lookup URL") + } + } + if !strings.Contains(s, "/time?") || !strings.Contains(s, "QmTestReceipt123") { + t.Fatal("expected /time? deep link with receipt in PDF") + } } func TestLookupURLNoMCP(t *testing.T) { - u := LookupURL("abc", "rid") + u := LookupURL("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "QmTestReceipt123") if strings.Contains(u, "mcp.verae.ai") { t.Fatal(u) } - if !strings.Contains(u, "explorer.verae.ai") { + if !strings.HasPrefix(u, "https://explorer.verae.ai/time?") { + t.Fatalf("want /time? deep link, got %s", u) + } + if !strings.Contains(u, "receipt=QmTestReceipt123") || !strings.Contains(u, "sha256=0123456789abcdef") { t.Fatal(u) } + // Must not use the old root query form that the scan bar does not honor + if strings.Contains(u, "explorer.verae.ai/?sha256=") { + t.Fatalf("old root URL form: %s", u) + } } diff --git a/platform/linux/README.txt b/platform/linux/README.txt new file mode 100644 index 0000000..0113391 --- /dev/null +++ b/platform/linux/README.txt @@ -0,0 +1,39 @@ +Verae Print Stamp — Linux (CLI + optional CUPS) +=============================================== + +Digests-only timestamping. Document bytes stay on your machine. + +Install +------- + tar xzf VeraePrintStamp-linux-*.tar.gz + cd VeraePrintStamp-linux-* + sudo ./install.sh # CLI + CUPS queue when cups is present + # or: PREFIX=$HOME/.local ./install.sh # user install; WITH_CUPS=0 + +Setup +----- + verae-print setup + # opens https://customer.verae.ai/portal/ — mint xVerae- → paste + +Stamp +----- + verae-print stamp ./Contract.pdf + +Outputs (default) +----------------- + ~/Documents/Verae Timestamps/ + Contract.pdf + Contract-verae-timestamp-{RECEIPTID}.pdf + Contract.verae.json + +CUPS destination +---------------- +install.sh installs backend `verae` and queue Verae_Timestamping when +`lpadmin` is available. Use your desktop’s print dialog → Verae Timestamping. +If the queue is missing, stamp via the CLI above. + +Links +----- + Downloads: https://printit.verae.ai/ + Portal: https://customer.verae.ai/portal/ + Explorer: https://explorer.verae.ai/time diff --git a/platform/linux/cups/backend-verae.sh b/platform/linux/cups/backend-verae.sh new file mode 100755 index 0000000..9fa9dcd --- /dev/null +++ b/platform/linux/cups/backend-verae.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# CUPS backend: verae — virtual printer "Verae Timestamping" (Linux) +# Discovery (no args): advertise device URI verae:/ +# Job: spool PDF and run verae-print as the job user (digests only). +set -euo pipefail + +LOG_ROOT="${CUPS_SERVERROOT:-/var/log/cups}" +BACKEND_LOG="${LOG_ROOT}/verae-backend.log" + +log() { + echo "$(date -u +%Y-%m-%dT%H:%MZ) $*" >>"$BACKEND_LOG" 2>/dev/null || true +} + +if [[ $# -eq 0 ]]; then + echo 'direct verae:/ "Verae Timestamping" "Verae Timestamping — digests-only stamp"' + exit 0 +fi + +JOB_ID="${1:-}" +JOB_USER="${2:-}" +JOB_TITLE="${3:-document}" +JOB_FILE="${6:-}" + +VERAE_PRINT="${VERAE_PRINT_BIN:-/usr/local/bin/verae-print}" +CHANNEL="${VERAE_PRINT_CHANNEL:-zapier}" + +if [[ -n "$JOB_USER" && -d "/home/$JOB_USER" ]]; then + SPOOL_BASE="/home/$JOB_USER/.local/share/verae/print-spool" + OUT_DIR="/home/$JOB_USER/Documents/Verae Timestamps" +else + SPOOL_BASE="/var/spool/verae/${JOB_USER:-unknown}" + OUT_DIR="/tmp/VeraeTimestamps" +fi + +mkdir -p "$SPOOL_BASE" +chmod 700 "$SPOOL_BASE" 2>/dev/null || true + +SAFE_TITLE=$(echo "$JOB_TITLE" | tr -cd 'A-Za-z0-9._ -' | head -c 80) +SAFE_TITLE=${SAFE_TITLE:-document} +WORK="$SPOOL_BASE/job-${JOB_ID}-$$" +mkdir -p "$WORK" + +INPUT="$WORK/input.dat" +if [[ -n "$JOB_FILE" && -f "$JOB_FILE" ]]; then + cp "$JOB_FILE" "$INPUT" +else + cat >"$INPUT" +fi + +PDF="$WORK/${SAFE_TITLE}.pdf" +if [[ "$(head -c 4 "$INPUT")" == "%PDF" ]]; then + cp "$INPUT" "$PDF" +else + # Best-effort: keep bytes; stamp accepts PDF/HTML + cp "$INPUT" "$PDF" +fi + +log "job=$JOB_ID user=$JOB_USER title=$JOB_TITLE pdf=$PDF channel=$CHANNEL" + +run_as_user() { + if [[ -n "$JOB_USER" && "$JOB_USER" != "root" ]] && command -v runuser >/dev/null 2>&1; then + runuser -u "$JOB_USER" -- "$@" + elif [[ -n "$JOB_USER" && "$JOB_USER" != "root" ]] && command -v su >/dev/null 2>&1; then + su - "$JOB_USER" -c "$(printf '%q ' "$@")" + else + "$@" + fi +} + +mkdir -p "$OUT_DIR" 2>/dev/null || true +if run_as_user "$VERAE_PRINT" stamp -channel "$CHANNEL" -out-dir "$OUT_DIR" -skip-setup "$PDF"; then + log "job $JOB_ID ok → $OUT_DIR" + exit 0 +fi +log "job $JOB_ID stamp failed" +exit 1 diff --git a/platform/linux/deb/build-deb.sh b/platform/linux/deb/build-deb.sh new file mode 100755 index 0000000..38ce40d --- /dev/null +++ b/platform/linux/deb/build-deb.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# Build Debian/Ubuntu .deb packages for verae-print. +# Run on a Debian/Ubuntu host (or any system with dpkg-deb + fakeroot). +# +# Usage: +# ./platform/linux/deb/build-deb.sh [amd64|arm64|all] +# Env: +# VERSION=0.1.2 +# BIN_AMD64=path/to/verae-print BIN_ARM64=path/to/verae-print +# OUT=build/deb +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +VERSION="${VERSION:-0.1.2}" +OUT="${OUT:-$ROOT/build/deb}" +ARCHS="${1:-all}" +PKG_NAME="verae-print" + +mkdir -p "$OUT" + +if ! command -v dpkg-deb >/dev/null 2>&1; then + echo "dpkg-deb required (run on Debian/Ubuntu, e.g. host 138)" >&2 + exit 1 +fi + +build_arch() { + local arch="$1" + local bin="$2" + if [[ ! -f "$bin" ]]; then + echo "missing binary for $arch: $bin" >&2 + exit 1 + fi + + local stage="$OUT/stage-${arch}" + rm -rf "$stage" + mkdir -p "$stage/DEBIAN" \ + "$stage/usr/bin" \ + "$stage/usr/lib/cups/backend" \ + "$stage/usr/share/doc/$PKG_NAME" \ + "$stage/usr/share/man/man1" + + install -m 755 "$bin" "$stage/usr/bin/verae-print" + if [[ -f "$ROOT/platform/linux/cups/backend-verae.sh" ]]; then + install -m 755 "$ROOT/platform/linux/cups/backend-verae.sh" "$stage/usr/lib/cups/backend/verae" + fi + + cat >"$stage/usr/share/doc/$PKG_NAME/copyright" <"$stage/usr/share/doc/$PKG_NAME/README.Debian" <"$stage/usr/share/doc/$PKG_NAME/README.Debian.gz" 2>/dev/null || true + + # Simple man page + cat >"$stage/usr/share/man/man1/verae-print.1" <"$stage/DEBIAN/control" < +Depends: libc6 +Recommends: cups-client +Installed-Size: $size +Homepage: https://printit.verae.ai/ +Description: Digests-only Verae timestamping from Print / CLI + Verae Print Stamp hashes a document locally and registers only the + SHA-256 digest with Verae. Includes an optional CUPS backend for a + "Verae Timestamping" print destination. +EOF + + cat >"$stage/DEBIAN/postinst" <<'EOF' +#!/bin/sh +set -e +if command -v lpadmin >/dev/null 2>&1 && [ -x /usr/lib/cups/backend/verae ]; then + lpadmin -x Verae_Timestamping 2>/dev/null || true + lpadmin -p Verae_Timestamping -E -v verae:/ -m raw \ + -D "Verae Timestamping" -L "Digests-only Verae stamp" 2>/dev/null || \ + lpadmin -p Verae_Timestamping -E -v verae:/ -m everywhere \ + -D "Verae Timestamping" 2>/dev/null || true + cupsenable Verae_Timestamping 2>/dev/null || true + cupsaccept Verae_Timestamping 2>/dev/null || true +fi +echo "verae-print installed. Next: verae-print setup" +exit 0 +EOF + chmod 755 "$stage/DEBIAN/postinst" + + cat >"$stage/DEBIAN/prerm" <<'EOF' +#!/bin/sh +set -e +if command -v lpadmin >/dev/null 2>&1; then + lpadmin -x Verae_Timestamping 2>/dev/null || true +fi +exit 0 +EOF + chmod 755 "$stage/DEBIAN/prerm" + + local deb="$OUT/${PKG_NAME}_${VERSION}_${arch}.deb" + if command -v fakeroot >/dev/null 2>&1; then + fakeroot dpkg-deb --build "$stage" "$deb" + else + dpkg-deb --build "$stage" "$deb" + fi + # Stable name + cp -f "$deb" "$OUT/${PKG_NAME}_${arch}.deb" + echo "built $deb" + dpkg-deb -I "$deb" | head -20 +} + +resolve_bin() { + local arch="$1" + local val="" + case "$arch" in + amd64) val="${BIN_AMD64:-}" ;; + arm64) val="${BIN_ARM64:-}" ;; + esac + if [[ -n "$val" && -f "$val" ]]; then + echo "$val" + return + fi + local label="linux-amd64" + [[ "$arch" == "arm64" ]] && label="linux-arm64" + local cand + for cand in \ + "$ROOT/build/win-linux/VeraePrintStamp-${label}-${VERSION}/verae-print" \ + "$ROOT/build/win-linux/VeraePrintStamp-${label}-0.1.2/verae-print" \ + "$ROOT/build/linux-bins/verae-print-${arch}" + do + if [[ -f "$cand" ]]; then + echo "$cand" + return + fi + done + echo "" +} + +do_one() { + local arch="$1" + local bin + bin="$(resolve_bin "$arch")" + if [[ -z "$bin" ]]; then + echo "No binary for $arch — set BIN_AMD64 / BIN_ARM64 or run scripts/build-win-linux.sh first" >&2 + exit 1 + fi + build_arch "$arch" "$bin" +} + +case "$ARCHS" in + all) + do_one amd64 + # arm64 deb can be built on amd64 host (just packages the arm64 binary) + if [[ -n "$(resolve_bin arm64)" ]]; then + do_one arm64 + else + echo "(skip arm64 — no binary)" + fi + ;; + amd64|arm64) + do_one "$ARCHS" + ;; + *) + echo "Usage: $0 [amd64|arm64|all]" >&2 + exit 2 + ;; +esac + +(cd "$OUT" && shasum -a 256 *.deb 2>/dev/null | tee SHA256SUMS) || \ + (cd "$OUT" && sha256sum *.deb | tee SHA256SUMS) +ls -lh "$OUT"/*.deb diff --git a/platform/linux/install.sh b/platform/linux/install.sh new file mode 100755 index 0000000..8b7b02e --- /dev/null +++ b/platform/linux/install.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Install verae-print CLI (+ optional CUPS "Verae Timestamping" queue). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +PREFIX="${PREFIX:-/usr/local}" +BIN_DST="$PREFIX/bin/verae-print" +WITH_CUPS="${WITH_CUPS:-1}" + +if [[ ! -x "$ROOT/verae-print" ]]; then + echo "verae-print binary missing next to install.sh" >&2 + exit 1 +fi + +echo "→ Installing $BIN_DST" +install -d "$PREFIX/bin" +install -m 755 "$ROOT/verae-print" "$BIN_DST" + +if [[ "$WITH_CUPS" == "1" ]] && command -v lpadmin >/dev/null 2>&1; then + BACKEND_SRC="$ROOT/cups/backend-verae.sh" + BACKEND_DST="/usr/lib/cups/backend/verae" + if [[ ! -d /usr/lib/cups/backend && -d /usr/libexec/cups/backend ]]; then + BACKEND_DST="/usr/libexec/cups/backend/verae" + fi + if [[ -f "$BACKEND_SRC" ]]; then + echo "→ CUPS backend $BACKEND_DST" + install -m 755 "$BACKEND_SRC" "$BACKEND_DST" + # Point backend at installed binary + if grep -q 'VERAE_PRINT_BIN:-/usr/local/bin/verae-print' "$BACKEND_DST" 2>/dev/null; then + : + fi + QUEUE="Verae_Timestamping" + lpadmin -x "$QUEUE" 2>/dev/null || true + lpadmin -p "$QUEUE" -E -v verae:/ -m raw -D "Verae Timestamping" -L "Digests-only Verae stamp" || \ + lpadmin -p "$QUEUE" -E -v verae:/ -m everywhere -D "Verae Timestamping" || true + cupsenable "$QUEUE" 2>/dev/null || true + cupsaccept "$QUEUE" 2>/dev/null || true + echo "CUPS queue: $QUEUE (Print → Verae Timestamping when your DE lists it)" + fi +else + echo "(Skipping CUPS — set WITH_CUPS=1 and install cups-client to enable)" +fi + +echo +echo "Next:" +echo " verae-print setup # paste xVerae- key from https://customer.verae.ai/portal/" +echo " verae-print stamp file.pdf" +echo "Outputs: ~/Documents/Verae Timestamps/" diff --git a/platform/macos/ipp/verae-print-command.sh b/platform/macos/ipp/verae-print-command.sh index d44332b..378ab8b 100755 --- a/platform/macos/ipp/verae-print-command.sh +++ b/platform/macos/ipp/verae-print-command.sh @@ -8,10 +8,17 @@ exec >>"$LOG" 2>&1 echo "$(date -u +%Y-%m-%dT%H:%MZ) IPP job start env=$(env | grep -E '^IPP_|^CONTENT_|^USER=' | tr '\n' ' ')" VERAE_PRINT="${VERAE_PRINT_BIN:-$HOME/bin/verae-print}" -CHANNEL="${VERAE_PRINT_CHANNEL:-verae}" OUT_DIR="${HOME}/Documents/Verae Timestamps" mkdir -p "$OUT_DIR" +# First-run: require customer xVerae- signing key (opens portal dialogs) +if ! "$VERAE_PRINT" setup; then + echo "ERROR: signing key setup required" >&2 + osascript -e 'display notification "Create a Verae customer account to get your signing key" with title "Verae Timestamping"' || true + open "https://customer.verae.ai/portal/" 2>/dev/null || true + exit 1 +fi + TITLE="${IPP_JOB_NAME:-${IPP_DOCUMENT_NAME:-document}}" # Strip path / extension for stem BASE="$(basename "$TITLE")" @@ -48,8 +55,8 @@ else cp "$RAW" "$PDF" fi -echo "stamping stem=$STEM channel=$CHANNEL pdf=$PDF" -"$VERAE_PRINT" stamp -channel "$CHANNEL" -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF" +echo "stamping stem=$STEM pdf=$PDF" +"$VERAE_PRINT" stamp -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF" echo "ATTR: job-impressions-completed=1" >&2 rm -rf "$WORK" exit 0 diff --git a/platform/macos/pkg/build-pkg.sh b/platform/macos/pkg/build-pkg.sh index a9c620f..d962726 100755 --- a/platform/macos/pkg/build-pkg.sh +++ b/platform/macos/pkg/build-pkg.sh @@ -9,7 +9,7 @@ SIGNING_ENV="${SIGNING_ENV:-$HOME/fie/installers/macos/signing.env}" # shellcheck source=/dev/null source "$SIGNING_ENV" -VERSION="${VERSION:-0.1.0}" +VERSION="${VERSION:-0.1.2}" BUILD_ROOT="$ROOT/build/macos-pkg" STAGE="$BUILD_ROOT/stage" SCRIPTS="$BUILD_ROOT/scripts" @@ -98,18 +98,46 @@ PLIST cat > "$APP/Contents/MacOS/Verae Print Stamp" <<'RUN' #!/bin/bash BIN="/Library/Application Support/Verae/PrintStamp/bin/verae-print" -CHANNEL="${VERAE_PRINT_CHANNEL:-verae}" +# First launch: create customer account + save xVerae- signing key +SETUP_OK=0 +if "$BIN" setup >/tmp/verae-print-setup.log 2>&1; then + SETUP_OK=1 +else + osascript -e 'display notification "Create a Verae customer account to get your signing key" with title "Verae Print Stamp"' || true +fi if [[ $# -eq 0 ]]; then - osascript <<'OSA' -display dialog "Drop a PDF on this app, use Open With, or Print → Destination → Verae Timestamping." buttons {"OK"} default button 1 with title "Verae Print Stamp" + if [[ "$SETUP_OK" -eq 1 ]]; then + osascript <<'OSA' +display dialog "Signing key saved. You are ready to timestamp. + +• Drop a PDF on this app, or Open With +• Or Print → Destination → Verae Timestamping + +Manage keys: https://customer.verae.ai/portal/" buttons {"Open portal", "OK"} default button "OK" with title "Verae Print Stamp" +if button returned of result is "Open portal" then + open location "https://customer.verae.ai/portal/" +end if OSA - open -R "/Library/Application Support/Verae/PrintStamp" 2>/dev/null || true + else + osascript <<'OSA' +display dialog "Verae Print Stamp needs a customer signing key. + +1. Create a free account on customer.verae.ai +2. Open Security and create an xVerae- token +3. Run this app again (or: verae-print setup) and paste the key + +Print → Destination → Verae Timestamping works after the key is saved." buttons {"Open portal", "OK"} default button "Open portal" with title "Verae Print Stamp" +if button returned of result is "Open portal" then + open location "https://customer.verae.ai/portal/" +end if +OSA + fi exit 0 fi for f in "$@"; do case "$f" in *.pdf|*.PDF|*.html|*.HTML|*.htm|*.HTM) - "$BIN" stamp -channel "$CHANNEL" -open "$f" \ + "$BIN" stamp -open "$f" \ || osascript -e "display notification \"Stamp failed for $(basename "$f")\" with title \"Verae Print Stamp\"" ;; *) diff --git a/platform/macos/pkg/scripts/postinstall b/platform/macos/pkg/scripts/postinstall index 13028bc..2791465 100755 --- a/platform/macos/pkg/scripts/postinstall +++ b/platform/macos/pkg/scripts/postinstall @@ -62,5 +62,11 @@ if [[ -n "${CONSOLE_USER:-}" && "$CONSOLE_USER" != "root" ]]; then fi fi +# First-run: open the app so the user creates a customer account + signing key +if [[ -n "${CONSOLE_USER:-}" && "$CONSOLE_USER" != "root" ]]; then + echo "Launching Verae Print Stamp for account setup (user=$CONSOLE_USER)" + sudo -u "$CONSOLE_USER" open "$APP" 2>/dev/null || true +fi + echo "$(date -u +%Y-%m-%dT%H:%MZ) postinstall done" exit 0 diff --git a/platform/registration-pdf/build.mjs b/platform/registration-pdf/build.mjs index 4b1748f..bb57f97 100644 --- a/platform/registration-pdf/build.mjs +++ b/platform/registration-pdf/build.mjs @@ -34,10 +34,15 @@ async function loadQR() { return QRCode.default || QRCode; } +/** Same destination as explorer scan-bar → /time (auto Lookup). */ function lookupURL(sha, receipt) { - let u = `https://explorer.verae.ai/?sha256=${encodeURIComponent(sha || "")}`; - if (receipt) u += `&receipt=${encodeURIComponent(receipt)}`; - return u; + const params = new URLSearchParams(); + const rid = String(receipt || "").trim(); + let digest = String(sha || "").trim().toLowerCase().replace(/^0x/, ""); + if (rid) params.set("receipt", rid); + if (digest) params.set("sha256", digest); + const q = params.toString(); + return q ? `https://explorer.verae.ai/time?${q}` : "https://explorer.verae.ai/time"; } function findLogo() { @@ -120,6 +125,9 @@ async function buildPdf(row) { fields.splice(1, 0, ["Document size", `${row.size_bytes} bytes`]); } + const linkColor = [74, 46, 140]; // Verae purple — visible as a link + const isHttpURL = (s) => /^https?:\/\//i.test(String(s || "").trim()); + let y = 168; doc.setFontSize(10); for (const [label, value] of fields) { @@ -127,9 +135,21 @@ async function buildPdf(row) { doc.setTextColor(42, 24, 56); doc.text(label, margin, y); doc.setFont("helvetica", "normal"); - doc.setTextColor(40, 40, 50); - const lines = doc.splitTextToSize(String(value ?? ""), pageW - margin * 2 - 10); - doc.text(lines, margin, y + 14); + const text = String(value ?? ""); + const maxW = pageW - margin * 2 - 10; + const lines = doc.splitTextToSize(text, maxW); + const valueY = y + 14; + if (isHttpURL(text)) { + doc.setTextColor(...linkColor); + let ly = valueY; + for (const line of lines) { + doc.textWithLink(line, margin, ly, { url: text.trim() }); + ly += 12; + } + } else { + doc.setTextColor(40, 40, 50); + doc.text(lines, margin, valueY); + } y += 14 + lines.length * 12 + 10; if (y > pageH - 220) break; } @@ -144,13 +164,17 @@ async function buildPdf(row) { const qrX = pageW - margin - qrSize; const qrY = pageH - margin - qrSize - 36; doc.addImage(qr, "PNG", qrX, qrY, qrSize, qrSize); + // Clickable hotspot over the QR (same destination as the public lookup URL) + doc.link(qrX, qrY, qrSize, qrSize, { url: lookup }); doc.setFontSize(8); - doc.setTextColor(90, 80, 100); - doc.text("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, { + doc.setTextColor(...linkColor); + doc.textWithLink("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, { align: "center", + url: lookup, }); doc.setFontSize(8); + doc.setTextColor(90, 80, 100); doc.text( "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.", margin, @@ -159,7 +183,17 @@ async function buildPdf(row) { ); doc.setFont("helvetica", "bold"); doc.setTextColor(42, 24, 56); - doc.text("Verae · verae.com · explorer.verae.ai", margin, pageH - 36); + const footY = pageH - 36; + doc.text("Verae · ", margin, footY); + let fx = margin + doc.getTextWidth("Verae · "); + doc.setTextColor(...linkColor); + doc.textWithLink("verae.com", fx, footY, { url: "https://verae.com" }); + fx += doc.getTextWidth("verae.com"); + doc.setTextColor(42, 24, 56); + doc.text(" · ", fx, footY); + fx += doc.getTextWidth(" · "); + doc.setTextColor(...linkColor); + doc.textWithLink("explorer.verae.ai", fx, footY, { url: "https://explorer.verae.ai/" }); return Buffer.from(doc.output("arraybuffer")); } diff --git a/platform/windows/LICENSE.txt b/platform/windows/LICENSE.txt new file mode 100644 index 0000000..04c11f5 --- /dev/null +++ b/platform/windows/LICENSE.txt @@ -0,0 +1,11 @@ +Verae Print Stamp +Copyright (c) Verae / George Lambert + +UNLICENSED — all rights reserved unless otherwise agreed in writing. + +This software sends only a SHA-256 digest of your document to Verae for +timestamp registration. Document bytes remain on your computer. + +Downloads: https://printit.verae.ai/ +Portal: https://customer.verae.ai/portal/ +Support: https://verae.com diff --git a/platform/windows/README.txt b/platform/windows/README.txt new file mode 100644 index 0000000..b87f203 --- /dev/null +++ b/platform/windows/README.txt @@ -0,0 +1,38 @@ +Verae Print Stamp — Windows (CLI) +================================= + +Digests-only timestamping. Document bytes stay on your PC. + +Install +------- +1. Unzip this archive. +2. Right-click install.ps1 → Run with PowerShell + (or: powershell -ExecutionPolicy Bypass -File .\install.ps1) +3. Open a new terminal and run: + + verae-print setup + + Create an account at https://customer.verae.ai/portal/ + Security tab → mint an xVerae- token → paste when prompted. + +4. Stamp a file: + + verae-print stamp .\Contract.pdf + +Outputs (default) +----------------- + %USERPROFILE%\Documents\Verae Timestamps\ + Contract.pdf + Contract-verae-timestamp-{RECEIPTID}.pdf + Contract.verae.json + +Print destination +----------------- +A Windows virtual printer (Print → Verae Timestamping) is not in this +build yet. Workflow today: print to PDF, then `verae-print stamp`. + +Links +----- + Downloads: https://printit.verae.ai/ + Portal: https://customer.verae.ai/portal/ + Explorer: https://explorer.verae.ai/time diff --git a/platform/windows/build-installer.sh b/platform/windows/build-installer.sh new file mode 100755 index 0000000..0354e1c --- /dev/null +++ b/platform/windows/build-installer.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Build NSIS GUI setup.exe for Windows (amd64 / arm64). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +VERSION="${VERSION:-0.1.2}" +OUT="${OUT:-$ROOT/build/windows-installer}" +MAKENSIS="${MAKENSIS:-$(command -v makensis || true)}" +MAKENSIS="${MAKENSIS:-/opt/homebrew/bin/makensis}" + +if [[ ! -x "$MAKENSIS" ]]; then + echo "makensis not found — brew install nsis" >&2 + exit 1 +fi + +mkdir -p "$OUT" + +build_arch() { + local arch="$1" + local src="$ROOT/build/win-linux/VeraePrintStamp-windows-${arch}-${VERSION}" + if [[ ! -f "$src/verae-print.exe" ]]; then + src="$ROOT/build/win-linux/VeraePrintStamp-windows-${arch}-0.1.2" + fi + if [[ ! -f "$src/verae-print.exe" ]]; then + echo "missing $src/verae-print.exe — run scripts/build-win-linux.sh first" >&2 + exit 1 + fi + # Stage with Windows-style paths for File directives + local stage="$OUT/stage-${arch}" + rm -rf "$stage" + mkdir -p "$stage" + cp "$src/verae-print.exe" "$stage/" + cp "$ROOT/platform/windows/README.txt" "$stage/" + cp "$ROOT/platform/windows/LICENSE.txt" "$stage/" + + local nsi="$ROOT/platform/windows/installer.nsi" + # NSIS OutFile is relative to cwd + ( + cd "$OUT" + "$MAKENSIS" -V2 \ + -DVERSION="$VERSION" \ + -DARCH="$arch" \ + -DSRCDIR="$stage" \ + "$nsi" + ) + local exe="$OUT/VeraePrintStamp-windows-${arch}-${VERSION}-setup.exe" + if [[ ! -f "$exe" ]]; then + # NSIS may write next to nsi or cwd + exe=$(find "$OUT" "$ROOT/platform/windows" -name "VeraePrintStamp-windows-${arch}-${VERSION}-setup.exe" 2>/dev/null | head -1) + fi + if [[ -z "$exe" || ! -f "$exe" ]]; then + echo "setup.exe not produced for $arch" >&2 + ls -la "$OUT" "$ROOT/platform/windows" | head -40 + exit 1 + fi + cp -f "$exe" "$OUT/VeraePrintStamp-windows-${arch}-setup.exe" + echo "built $exe ($(wc -c <"$exe") bytes)" +} + +build_arch amd64 +build_arch arm64 +(cd "$OUT" && shasum -a 256 *-setup.exe | tee SHA256SUMS) +ls -lh "$OUT"/*-setup.exe diff --git a/platform/windows/install.ps1 b/platform/windows/install.ps1 new file mode 100644 index 0000000..1f9f3ee --- /dev/null +++ b/platform/windows/install.ps1 @@ -0,0 +1,29 @@ +# Install verae-print for the current user (Windows). +# Run: powershell -ExecutionPolicy Bypass -File .\install.ps1 +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$Exe = Join-Path $Root "verae-print.exe" +if (-not (Test-Path $Exe)) { + Write-Error "verae-print.exe missing next to install.ps1" +} + +$BinDir = Join-Path $env:LOCALAPPDATA "Verae\PrintStamp\bin" +New-Item -ItemType Directory -Force -Path $BinDir | Out-Null +Copy-Item -Force $Exe (Join-Path $BinDir "verae-print.exe") + +# User PATH +$userPath = [Environment]::GetEnvironmentVariable("Path", "User") +if (-not $userPath) { $userPath = "" } +if ($userPath -notlike "*$BinDir*") { + [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(';') + ";" + $BinDir), "User") + $env:Path = $env:Path + ";" + $BinDir + Write-Host "Added to user PATH: $BinDir" +} + +Write-Host "" +Write-Host "Installed: $BinDir\verae-print.exe" +Write-Host "Next (new terminal):" +Write-Host " verae-print setup" +Write-Host " verae-print stamp .\document.pdf" +Write-Host "Portal: https://customer.verae.ai/portal/" +Write-Host "Outputs: Documents\Verae Timestamps\" diff --git a/platform/windows/installer.nsi b/platform/windows/installer.nsi new file mode 100644 index 0000000..e00d4e7 --- /dev/null +++ b/platform/windows/installer.nsi @@ -0,0 +1,146 @@ +; Verae Print Stamp — Windows GUI installer (NSIS 3) +; Build via platform/windows/build-installer.sh + +!ifndef VERSION + !define VERSION "0.1.2" +!endif +!ifndef ARCH + !define ARCH "amd64" +!endif +!ifndef SRCDIR + !define SRCDIR "." +!endif + +!define PRODUCT_NAME "Verae Print Stamp" +!define PRODUCT_PUBLISHER "Verae" +!define PRODUCT_URL "https://printit.verae.ai/" +!define PORTAL_URL "https://customer.verae.ai/portal/" + +Name "${PRODUCT_NAME} ${VERSION}" +OutFile "VeraePrintStamp-windows-${ARCH}-${VERSION}-setup.exe" +InstallDir "$LOCALAPPDATA\Verae\PrintStamp" +InstallDirRegKey HKCU "Software\Verae\PrintStamp" "InstallDir" +RequestExecutionLevel user +Unicode true +SetCompressor /SOLID lzma +ShowInstDetails show + +!include "MUI2.nsh" +!include "WinMessages.nsh" + +!define MUI_ABORTWARNING +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" + +!define MUI_WELCOMEPAGE_TITLE "${PRODUCT_NAME}" +!define MUI_WELCOMEPAGE_TEXT "Setup will install Verae Print Stamp on your computer.$\r$\n$\r$\nDigests-only timestamping: document bytes stay on this PC.$\r$\n$\r$\nClick Next to continue." + +!define MUI_FINISHPAGE_RUN "$INSTDIR\bin\verae-print.exe" +!define MUI_FINISHPAGE_RUN_PARAMETERS "setup" +!define MUI_FINISHPAGE_RUN_TEXT "Run setup now (create account / paste xVerae- key)" +!define MUI_FINISHPAGE_LINK "Open ${PORTAL_URL}" +!define MUI_FINISHPAGE_LINK_LOCATION "${PORTAL_URL}" + +Var StartMenuFolder + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "${SRCDIR}\LICENSE.txt" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +Section "Verae Print Stamp (required)" SecMain + SectionIn RO + SetOutPath "$INSTDIR\bin" + File "${SRCDIR}\verae-print.exe" + SetOutPath "$INSTDIR" + File "${SRCDIR}\README.txt" + File "${SRCDIR}\LICENSE.txt" + + ; Append install bin dir to user PATH if missing + ReadRegStr $0 HKCU "Environment" "Path" + StrCmp $0 "" path_empty + Push "$0" + Push "$INSTDIR\bin" + Call StrStr + Pop $1 + StrCmp $1 "" path_append path_done + path_empty: + WriteRegExpandStr HKCU "Environment" "Path" "$INSTDIR\bin" + Goto path_broadcast + path_append: + WriteRegExpandStr HKCU "Environment" "Path" "$0;$INSTDIR\bin" + path_broadcast: + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=3000 + path_done: + + WriteRegStr HKCU "Software\Verae\PrintStamp" "InstallDir" "$INSTDIR" + WriteRegStr HKCU "Software\Verae\PrintStamp" "Version" "${VERSION}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" "DisplayName" "${PRODUCT_NAME} ${VERSION}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" "DisplayVersion" "${VERSION}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" "URLInfoAbout" "${PRODUCT_URL}" + WriteUninstaller "$INSTDIR\Uninstall.exe" + + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + CreateDirectory "$SMPROGRAMS\$StartMenuFolder" + CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Setup signing key.lnk" "$INSTDIR\bin\verae-print.exe" "setup" + CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Customer portal.lnk" "${PORTAL_URL}" + CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe" + !insertmacro MUI_STARTMENU_WRITE_END + + CreateShortCut "$DESKTOP\Verae Print Stamp.lnk" "$INSTDIR\bin\verae-print.exe" "setup" +SectionEnd + +Section "Uninstall" + Delete "$INSTDIR\bin\verae-print.exe" + RMDir "$INSTDIR\bin" + Delete "$INSTDIR\README.txt" + Delete "$INSTDIR\LICENSE.txt" + Delete "$INSTDIR\Uninstall.exe" + RMDir "$INSTDIR" + + !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder + Delete "$SMPROGRAMS\$StartMenuFolder\*.lnk" + RMDir "$SMPROGRAMS\$StartMenuFolder" + Delete "$DESKTOP\Verae Print Stamp.lnk" + + DeleteRegKey HKCU "Software\Verae\PrintStamp" + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" +SectionEnd + +; Case-insensitive substring search: haystack, needle → result on stack (needle or "") +Function StrStr + Exch $R1 ; needle + Exch + Exch $R2 ; haystack + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrCpy $R4 0 + loop: + StrCpy $R5 $R2 $R3 $R4 + StrCmp $R5 "" notfound + StrCmp $R5 $R1 found + IntOp $R4 $R4 + 1 + Goto loop + found: + StrCpy $R1 $R1 + Goto done + notfound: + StrCpy $R1 "" + done: + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Exch $R1 +FunctionEnd diff --git a/scripts/build-win-linux.sh b/scripts/build-win-linux.sh new file mode 100755 index 0000000..2358dd8 --- /dev/null +++ b/scripts/build-win-linux.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Cross-compile Windows + Linux release archives for printit.verae.ai +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +VERSION="${VERSION:-$(go run ./cmd/verae-print version 2>/dev/null || true)}" +if [[ -z "$VERSION" || "$VERSION" == *"go: "* ]]; then + VERSION=$(grep -E 'HelperVersion\s*=' internal/job/job.go | head -1 | sed -E 's/.*"([^"]+)".*/\1/') +fi +OUT="${OUT:-$ROOT/build/win-linux}" +rm -rf "$OUT" +mkdir -p "$OUT" + +echo "Building verae-print $VERSION → $OUT" + +build_one() { + local goos="$1" goarch="$2" label="$3" + local dir="$OUT/VeraePrintStamp-${label}-${VERSION}" + mkdir -p "$dir" + local bin="verae-print" + [[ "$goos" == "windows" ]] && bin="verae-print.exe" + echo "→ $goos/$goarch" + CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -trimpath -ldflags "-s -w" -o "$dir/$bin" ./cmd/verae-print + if [[ "$goos" == "linux" ]]; then + mkdir -p "$dir/cups" + cp platform/linux/install.sh "$dir/" + cp platform/linux/README.txt "$dir/" + cp platform/linux/cups/backend-verae.sh "$dir/cups/" + chmod +x "$dir/install.sh" "$dir/cups/backend-verae.sh" "$dir/$bin" + tar -C "$OUT" -czf "$OUT/VeraePrintStamp-${label}-${VERSION}.tar.gz" "VeraePrintStamp-${label}-${VERSION}" + cp "$OUT/VeraePrintStamp-${label}-${VERSION}.tar.gz" "$OUT/VeraePrintStamp-${label}.tar.gz" + else + cp platform/windows/install.ps1 "$dir/" + cp platform/windows/README.txt "$dir/" + ( + cd "$OUT" + zip -qr "VeraePrintStamp-${label}-${VERSION}.zip" "VeraePrintStamp-${label}-${VERSION}" + cp "VeraePrintStamp-${label}-${VERSION}.zip" "VeraePrintStamp-${label}.zip" + ) + fi +} + +build_one linux amd64 linux-amd64 +build_one linux arm64 linux-arm64 +build_one windows amd64 windows-amd64 +# Windows arm64 is increasingly common +build_one windows arm64 windows-arm64 + +( + cd "$OUT" + shasum -a 256 VeraePrintStamp-*.tar.gz VeraePrintStamp-*.zip 2>/dev/null | tee SHA256SUMS +) + +echo +echo "Artifacts:" +ls -lh "$OUT"/VeraePrintStamp-*.{tar.gz,zip} 2>/dev/null +echo "Done."