PrintToVeraeTimeStamp/internal/auth/auth.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" {
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
}