PrintToVeraeTimeStamp/internal/auth/auth.go
Grok Bot (for George) 13ebc351f5 0.3.2: Save As filename prompt per print (Windows + macOS)
- 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
2026-10-03 20:00:56 -04:00

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
}