PrintToVeraeTimeStamp/internal/auth/auth.go
George Lambert 6f8796c25e
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
Open Zap Billing when PrintIt stamps fail for credits
On HTTP 402/429 (and credit/billing error bodies), hold the print job and open
https://zap.verae.ai/portal/?tab=billing so the user can reload prepaid balance.
Throttle browser opens to once per 10 minutes. Auth failures still use the
sign-in flow unchanged.
2026-10-20 01:07:39 -04:00

178 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
// Empty / default → zapier (single billing edge). Direct VeraeTime is opt-in only.
func ParseChannel(s string) (Channel, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "zapier", "zap", "edge", "default", "billed":
return ChannelZapier, nil
case "verae", "veraetime", "mcp", "direct":
return ChannelVerae, nil
default:
return "", fmt.Errorf("unknown channel %q (want zapier or verae)", 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
// (single commercial billing edge). Direct api.veraetime.net only when
// VERAE_PRINT_CHANNEL=verae and VERAE_ALLOW_DIRECT_VERAE=1 (engineering escape hatch).
func PreferredChannel() Channel {
if env := strings.ToLower(strings.TrimSpace(os.Getenv("VERAE_PRINT_CHANNEL"))); env != "" {
if c, err := ParseChannel(env); err == nil {
if c == ChannelVerae && os.Getenv("VERAE_ALLOW_DIRECT_VERAE") != "1" {
return ChannelZapier
}
return c
}
}
return ChannelZapier
}
const (
KeychainService = "verae.xVeraeToken"
PortalSignupURL = "https://customer.verae.ai/portal/"
// ZapBillingURL is the prepaid reload / Add funds tab on the Zap portal
// (PrintIt’s commercial meter). Open this when stamps fail for credits.
ZapBillingURL = "https://zap.verae.ai/portal/?tab=billing"
)
// 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
}