package auth import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "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) 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 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 }