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
9 changed files with 1257 additions and 42 deletions

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)")
}
// 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) {
home := t.TempDir()
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"
)
const HelperVersion = "0.1.0"
const HelperVersion = "0.1.2"
// Result paths written for one print job.
type Result struct {

View file

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

View file

@ -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"),
)

View file

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