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