- promptForFilename setting (default true; env VERAE_PROMPT_FOR_FILENAME), 'verae-print config --prompt-for-filename', last Save As folder in ~/.verae/state.json - Windows helper: common file dialog (PDF filter, overwrite prompt, topmost owner) before stamping; macOS printer path: choose file name (NSSavePanel) via 'stamp -prompt-name -title'; cancel discards the job (no stamp/credit) - job.Options.OutPath: original saved at the chosen path, certificate and .verae.json next to it with the chosen base name (never overwritten) - held jobs keep the chosen path (pendingMeta.OutPath) - installer: 'Ask for a file name for each print' checkbox; removed the 'Open the user guide' finish-page checkbox; mac-key-setup asks too - auth test no longer touches the real macOS keychain (VERAE_SKIP_KEYCHAIN) - version 0.3.2
170 lines
4.6 KiB
Go
170 lines
4.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// Channel selects which stamp API to use.
|
|
type Channel string
|
|
|
|
const (
|
|
ChannelVerae Channel = "verae" // api.veraetime.net + MCP JWT
|
|
ChannelZapier Channel = "zapier" // zap.verae.ai + xVerae-
|
|
)
|
|
|
|
// ParseChannel maps CLI/env values to a Channel.
|
|
func ParseChannel(s string) (Channel, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "", "verae", "veraetime", "mcp", "default":
|
|
return ChannelVerae, nil
|
|
case "zapier", "zap", "edge":
|
|
return ChannelZapier, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown channel %q (want verae or zapier)", s)
|
|
}
|
|
}
|
|
|
|
// LoadVeraeJWT reads Bearer token from ~/.verae/auth.properties
|
|
// (access_token, auth_token, or id_token).
|
|
func LoadVeraeJWT() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path := filepath.Join(home, ".verae", "auth.properties")
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
vals := map[string]string{}
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
k, v, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
vals[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
for _, key := range []string{"access_token", "auth_token", "id_token"} {
|
|
if t := vals[key]; t != "" {
|
|
return t, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no access_token/auth_token/id_token in %s", path)
|
|
}
|
|
|
|
// LoadXVeraeToken loads a long-lived xVerae- token from, in order:
|
|
// XVERAE_TOKEN env, ~/.verae/xverae-token file, macOS Keychain service verae.xVeraeToken.
|
|
func LoadXVeraeToken() (string, error) {
|
|
if t := strings.TrimSpace(os.Getenv("XVERAE_TOKEN")); t != "" {
|
|
return t, nil
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
path := filepath.Join(home, ".verae", "xverae-token")
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
t := strings.TrimSpace(string(b))
|
|
if t != "" {
|
|
return t, nil
|
|
}
|
|
}
|
|
}
|
|
// macOS Keychain (generic password, account optional)
|
|
if runtime.GOOS != "darwin" {
|
|
return "", fmt.Errorf("no xVerae- token (set XVERAE_TOKEN or create %s)", filepath.Join(home, ".verae", "xverae-token"))
|
|
}
|
|
out, err := exec.Command("security", "find-generic-password", "-s", "verae.xVeraeToken", "-w").Output()
|
|
if err == nil {
|
|
t := strings.TrimSpace(string(out))
|
|
if t != "" {
|
|
return t, nil
|
|
}
|
|
}
|
|
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
|
|
if runtime.GOOS != "darwin" || os.Getenv("VERAE_SKIP_KEYCHAIN") == "1" {
|
|
return nil
|
|
}
|
|
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
|
|
}
|