wip: registration PDF, auth setup, CLI, Windows/Linux installers
This commit is contained in:
parent
c075478783
commit
4f1d90cf64
5 changed files with 1257 additions and 42 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
106
internal/auth/setup_darwin.go
Normal file
106
internal/auth/setup_darwin.go
Normal 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()
|
||||
}
|
||||
79
internal/auth/setup_other.go
Normal file
79
internal/auth/setup_other.go
Normal 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()
|
||||
}
|
||||
28
internal/auth/token_save_test.go
Normal file
28
internal/auth/token_save_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue