Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled
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.
178 lines
5.1 KiB
Go
178 lines
5.1 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.
|
||
// 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
|
||
}
|