wip: registration PDF, auth setup, CLI, Windows/Linux installers

This commit is contained in:
George Lambert 2026-10-03 18:35:27 -04:00
commit 4f1d90cf64
26 changed files with 1257 additions and 42 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@
*.msi *.msi
.DS_Store .DS_Store
node_modules/ node_modules/
/build/

View file

@ -1,20 +1,53 @@
# PrintToVeraeTimeStamp # 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. Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine.
## Install ## 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 ```bash
cd /Users/marchon/research/PrintToVeraeTimeStamp cd /Users/marchon/research/PrintToVeraeTimeStamp
go build -o "$HOME/bin/verae-print" ./cmd/verae-print 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 verae-print install-printer # Print → Destination → Verae Timestamping
``` ```
Optional (buried PDF submenu): `verae-print install-macos` 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 ```bash
uvx verae-mcp-timestamping-server setup uvx verae-mcp-timestamping-server setup
@ -53,8 +86,8 @@ Default output directory: `~/Documents/Verae Timestamps/`
Channels: Channels:
- `verae` (default) — `api.veraetime.net` + JWT in `~/.verae/auth.properties` - `zapier` (default after setup) — `zap.verae.ai` + customer `xVerae-` signing key + `attachPdf:true`
- `zapier` — `zap.verae.ai` + `xVerae-` + `attachPdf:true` - `verae` — `api.veraetime.net` + JWT in `~/.verae/auth.properties` (opt-in: `-channel=verae`)
## Signed macOS installer (.pkg) ## Signed macOS installer (.pkg)
@ -64,7 +97,7 @@ Same FlightLookup Developer ID + notarization path as Verae Timestamping.app (`~
cd /Users/marchon/research/PrintToVeraeTimeStamp cd /Users/marchon/research/PrintToVeraeTimeStamp
chmod +x platform/macos/pkg/build-pkg.sh chmod +x platform/macos/pkg/build-pkg.sh
./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 # → 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. 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 ## 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 ## Contributing

View file

@ -21,6 +21,8 @@ func main() {
switch os.Args[1] { switch os.Args[1] {
case "stamp": case "stamp":
os.Exit(runStamp(os.Args[2:])) os.Exit(runStamp(os.Args[2:]))
case "setup", "login":
os.Exit(runSetup())
case "install-macos": case "install-macos":
os.Exit(runInstallMacOS(os.Args[2:])) os.Exit(runInstallMacOS(os.Args[2:]))
case "install-printer": case "install-printer":
@ -30,9 +32,9 @@ func main() {
case "help", "-h", "--help": case "help", "-h", "--help":
usage() usage()
default: default:
// PDF Services may invoke the binary with the PDF path as argv[1] // PDF Services / drop-target may invoke the binary with the file as argv[1]
if strings.HasSuffix(strings.ToLower(os.Args[1]), ".pdf") && fileExists(os.Args[1]) { if fileExists(os.Args[1]) {
os.Exit(runStamp([]string{"-channel", envOr("VERAE_PRINT_CHANNEL", "verae"), 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]) fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
usage() usage()
@ -44,14 +46,18 @@ func usage() {
fmt.Fprintf(os.Stderr, `verae-print %s — stamp a printed PDF with Verae (digests only) fmt.Fprintf(os.Stderr, `verae-print %s — stamp a printed PDF with Verae (digests only)
Usage: 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 stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] <file.pdf>
verae-print install-printer # Destination list: Verae Timestamping (recommended) verae-print install-printer # Destination list: Verae Timestamping (recommended)
verae-print install-macos # also install PDF Services menu items (legacy) verae-print install-macos # also install PDF Services menu items (legacy)
verae-print version 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: Channels:
verae (default) api.veraetime.net + ~/.verae/auth.properties JWT zapier (default) zap.verae.ai + customer xVerae- signing key
zapier zap.verae.ai + xVerae- token (XVERAE_TOKEN / ~/.verae/xverae-token / Keychain) verae api.veraetime.net + ~/.verae/auth.properties JWT
Outputs in --out-dir (default ~/Documents/Verae Timestamps): Outputs in --out-dir (default ~/Documents/Verae Timestamps):
{name}.pdf {name}.pdf
@ -60,12 +66,27 @@ Outputs in --out-dir (default ~/Documents/Verae Timestamps):
`, job.HelperVersion) `, 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 { func runStamp(args []string) int {
fs := flag.NewFlagSet("stamp", flag.ExitOnError) 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") outDir := fs.String("out-dir", "", "output directory")
name := fs.String("name", "", "document stem override") name := fs.String("name", "", "document stem override")
openFolder := fs.Bool("open", false, "open output folder on success (macOS)") 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) _ = fs.Parse(args)
if fs.NArg() < 1 { if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "stamp: PDF path required") fmt.Fprintln(os.Stderr, "stamp: PDF path required")
@ -79,6 +100,19 @@ func runStamp(args []string) int {
return 2 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} client := &veraeapi.Client{Channel: ch}
switch ch { switch ch {
case auth.ChannelZapier: case auth.ChannelZapier:
@ -91,7 +125,7 @@ func runStamp(args []string) int {
default: default:
tok, err := auth.LoadVeraeJWT() tok, err := auth.LoadVeraeJWT()
if err != nil { 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 return 1
} }
client.VeraeToken = tok client.VeraeToken = tok

View file

@ -92,3 +92,72 @@ func LoadXVeraeToken() (string, error) {
} }
return "", fmt.Errorf("no xVerae- token (set XVERAE_TOKEN, ~/.verae/xverae-token, or Keychain verae.xVeraeToken)") 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
}

View file

@ -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) { func TestLoadVeraeJWT(t *testing.T) {
home := t.TempDir() home := t.TempDir()
t.Setenv("HOME", home) t.Setenv("HOME", home)

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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())
}
}

View file

@ -15,7 +15,7 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi" "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
) )
const HelperVersion = "0.1.0" const HelperVersion = "0.1.2"
// Result paths written for one print job. // Result paths written for one print job.
type Result struct { type Result struct {

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/phpdave11/gofpdf" "github.com/phpdave11/gofpdf"
qrcode "github.com/skip2/go-qrcode" qrcode "github.com/skip2/go-qrcode"
@ -69,6 +70,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
}, fields[1:]...) }, 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 y := 168.0
for _, fv := range fields { for _, fv := range fields {
pdf.SetFont("Helvetica", "B", 10) 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]) pdf.Cell(pageW-margin*2, 12, fv[0])
y += 14 y += 14
pdf.SetFont("Helvetica", "", 10) 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) { for _, ln := range pdf.SplitLines([]byte(fv[1]), pageW-margin*2-10) {
pdf.SetXY(margin, y) 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 += 12
} }
y += 10 y += 10
@ -95,10 +111,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
qrX := pageW - margin - qrSize qrX := pageW - margin - qrSize
qrY := pageH - margin - qrSize - 36 qrY := pageH - margin - qrSize - 36
pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "") 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.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100) pdf.SetTextColor(74, 46, 140)
pdf.SetXY(qrX, qrY+qrSize+4) 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 { if qclean != nil {
defer qclean() defer qclean()
} }
@ -112,7 +129,13 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
pdf.SetFont("Helvetica", "B", 9) pdf.SetFont("Helvetica", "B", 9)
pdf.SetTextColor(42, 24, 56) pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, pageH-36) 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 var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil { if err := pdf.Output(&buf); err != nil {

View file

@ -28,13 +28,21 @@ type Input struct {
ByteLength int64 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 { func LookupURL(sha256, receiptID string) string {
sha256 = strings.TrimSpace(sha256) sha256 = strings.TrimSpace(sha256)
sha256 = strings.TrimPrefix(strings.ToLower(sha256), "0x")
receiptID = strings.TrimSpace(receiptID) receiptID = strings.TrimSpace(receiptID)
u := "https://explorer.verae.ai/?sha256=" + sha256 u := "https://explorer.verae.ai/time"
sep := "?"
if receiptID != "" { if receiptID != "" {
u += "&receipt=" + receiptID u += sep + "receipt=" + receiptID
sep = "&"
}
if sha256 != "" {
u += sep + "sha256=" + sha256
} }
return u return u
} }
@ -113,6 +121,7 @@ func buildWithNode(row map[string]any) ([]byte, error) {
func findBuildScript() string { func findBuildScript() string {
candidates := []string{ candidates := []string{
"/Library/Application Support/Verae/PrintStamp/registration-pdf/build.mjs",
"/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs", "/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs",
filepath.Join("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 { if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe) dir := filepath.Dir(exe)
candidates = append(candidates, 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"),
filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"), filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"),
) )

View file

@ -41,14 +41,30 @@ func TestBuildBrandedRegistration(t *testing.T) {
if !strings.Contains(s, "explorer.verae.ai") { if !strings.Contains(s, "explorer.verae.ai") {
t.Fatal("expected explorer lookup") 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) { func TestLookupURLNoMCP(t *testing.T) {
u := LookupURL("abc", "rid") u := LookupURL("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "QmTestReceipt123")
if strings.Contains(u, "mcp.verae.ai") { if strings.Contains(u, "mcp.verae.ai") {
t.Fatal(u) 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) 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)
}
} }

39
platform/linux/README.txt Normal file
View file

@ -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

View file

@ -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

210
platform/linux/deb/build-deb.sh Executable file
View file

@ -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" <<EOF
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: PrintToVeraeTimeStamp
Source: https://printit.verae.ai/
Files: *
Copyright: Verae / George Lambert
License: UNLICENSED
EOF
cat >"$stage/usr/share/doc/$PKG_NAME/README.Debian" <<EOF
verae-print for Debian/Ubuntu
=============================
After install:
verae-print setup
verae-print stamp document.pdf
Optional CUPS queue (if cups-client is installed):
sudo lpadmin -p Verae_Timestamping -E -v verae:/ -m raw -D "Verae Timestamping" || true
sudo cupsenable Verae_Timestamping; sudo cupsaccept Verae_Timestamping
Portal: https://customer.verae.ai/portal/
Downloads: https://printit.verae.ai/
EOF
gzip -9n -c "$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" <<EOF
.TH VERAE-PRINT 1 "$VERSION" "Verae" "User Commands"
.SH NAME
verae-print \\- stamp a PDF digest with Verae Time
.SH SYNOPSIS
.B verae-print
setup
.br
.B verae-print
stamp [options] file.pdf
.SH DESCRIPTION
Digests-only timestamping. Document bytes stay local.
.SH SEE ALSO
https://printit.verae.ai/
EOF
gzip -9n -f "$stage/usr/share/man/man1/verae-print.1"
local size
size=$(du -sk "$stage" | awk '{print $1}')
cat >"$stage/DEBIAN/control" <<EOF
Package: $PKG_NAME
Version: $VERSION
Section: utils
Priority: optional
Architecture: $arch
Maintainer: Verae <support@verae.ai>
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

48
platform/linux/install.sh Executable file
View file

@ -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/"

View file

@ -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' ' ')" 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}" VERAE_PRINT="${VERAE_PRINT_BIN:-$HOME/bin/verae-print}"
CHANNEL="${VERAE_PRINT_CHANNEL:-verae}"
OUT_DIR="${HOME}/Documents/Verae Timestamps" OUT_DIR="${HOME}/Documents/Verae Timestamps"
mkdir -p "$OUT_DIR" 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}}" TITLE="${IPP_JOB_NAME:-${IPP_DOCUMENT_NAME:-document}}"
# Strip path / extension for stem # Strip path / extension for stem
BASE="$(basename "$TITLE")" BASE="$(basename "$TITLE")"
@ -48,8 +55,8 @@ else
cp "$RAW" "$PDF" cp "$RAW" "$PDF"
fi fi
echo "stamping stem=$STEM channel=$CHANNEL pdf=$PDF" echo "stamping stem=$STEM pdf=$PDF"
"$VERAE_PRINT" stamp -channel "$CHANNEL" -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF" "$VERAE_PRINT" stamp -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF"
echo "ATTR: job-impressions-completed=1" >&2 echo "ATTR: job-impressions-completed=1" >&2
rm -rf "$WORK" rm -rf "$WORK"
exit 0 exit 0

View file

@ -9,7 +9,7 @@ SIGNING_ENV="${SIGNING_ENV:-$HOME/fie/installers/macos/signing.env}"
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$SIGNING_ENV" source "$SIGNING_ENV"
VERSION="${VERSION:-0.1.0}" VERSION="${VERSION:-0.1.2}"
BUILD_ROOT="$ROOT/build/macos-pkg" BUILD_ROOT="$ROOT/build/macos-pkg"
STAGE="$BUILD_ROOT/stage" STAGE="$BUILD_ROOT/stage"
SCRIPTS="$BUILD_ROOT/scripts" SCRIPTS="$BUILD_ROOT/scripts"
@ -98,18 +98,46 @@ PLIST
cat > "$APP/Contents/MacOS/Verae Print Stamp" <<'RUN' cat > "$APP/Contents/MacOS/Verae Print Stamp" <<'RUN'
#!/bin/bash #!/bin/bash
BIN="/Library/Application Support/Verae/PrintStamp/bin/verae-print" 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 if [[ $# -eq 0 ]]; then
osascript <<'OSA' if [[ "$SETUP_OK" -eq 1 ]]; then
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" 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 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 exit 0
fi fi
for f in "$@"; do for f in "$@"; do
case "$f" in case "$f" in
*.pdf|*.PDF|*.html|*.HTML|*.htm|*.HTM) *.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\"" || osascript -e "display notification \"Stamp failed for $(basename "$f")\" with title \"Verae Print Stamp\""
;; ;;
*) *)

View file

@ -62,5 +62,11 @@ if [[ -n "${CONSOLE_USER:-}" && "$CONSOLE_USER" != "root" ]]; then
fi fi
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" echo "$(date -u +%Y-%m-%dT%H:%MZ) postinstall done"
exit 0 exit 0

View file

@ -34,10 +34,15 @@ async function loadQR() {
return QRCode.default || QRCode; return QRCode.default || QRCode;
} }
/** Same destination as explorer scan-bar → /time (auto Lookup). */
function lookupURL(sha, receipt) { function lookupURL(sha, receipt) {
let u = `https://explorer.verae.ai/?sha256=${encodeURIComponent(sha || "")}`; const params = new URLSearchParams();
if (receipt) u += `&receipt=${encodeURIComponent(receipt)}`; const rid = String(receipt || "").trim();
return u; 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() { function findLogo() {
@ -120,6 +125,9 @@ async function buildPdf(row) {
fields.splice(1, 0, ["Document size", `${row.size_bytes} bytes`]); 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; let y = 168;
doc.setFontSize(10); doc.setFontSize(10);
for (const [label, value] of fields) { for (const [label, value] of fields) {
@ -127,9 +135,21 @@ async function buildPdf(row) {
doc.setTextColor(42, 24, 56); doc.setTextColor(42, 24, 56);
doc.text(label, margin, y); doc.text(label, margin, y);
doc.setFont("helvetica", "normal"); doc.setFont("helvetica", "normal");
doc.setTextColor(40, 40, 50); const text = String(value ?? "");
const lines = doc.splitTextToSize(String(value ?? ""), pageW - margin * 2 - 10); const maxW = pageW - margin * 2 - 10;
doc.text(lines, margin, y + 14); 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; y += 14 + lines.length * 12 + 10;
if (y > pageH - 220) break; if (y > pageH - 220) break;
} }
@ -144,13 +164,17 @@ async function buildPdf(row) {
const qrX = pageW - margin - qrSize; const qrX = pageW - margin - qrSize;
const qrY = pageH - margin - qrSize - 36; const qrY = pageH - margin - qrSize - 36;
doc.addImage(qr, "PNG", qrX, qrY, qrSize, qrSize); 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.setFontSize(8);
doc.setTextColor(90, 80, 100); doc.setTextColor(...linkColor);
doc.text("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, { doc.textWithLink("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, {
align: "center", align: "center",
url: lookup,
}); });
doc.setFontSize(8); doc.setFontSize(8);
doc.setTextColor(90, 80, 100);
doc.text( 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.", "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.",
margin, margin,
@ -159,7 +183,17 @@ async function buildPdf(row) {
); );
doc.setFont("helvetica", "bold"); doc.setFont("helvetica", "bold");
doc.setTextColor(42, 24, 56); 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")); return Buffer.from(doc.output("arraybuffer"));
} }

Some files were not shown because too many files have changed in this diff Show more