HTTPS GlobalClient (Stamp/Verify/HashReport/Signup/Login/Session/CreateUser) with Fake RoundTripper tests; auth.properties read/write; Keeper refresh ~60s before expiry or VERAE_GLOBAL_TOKEN; authctx.Resolve for require-bearer vs host fallback.
144 lines
3.1 KiB
Go
144 lines
3.1 KiB
Go
package veraetime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Keeper mints and refreshes a host bearer for VeraeTime when callers omit one.
|
|
type Keeper struct {
|
|
client *Client
|
|
authPath string
|
|
globalToken string
|
|
useEnv bool
|
|
|
|
mu sync.Mutex
|
|
token string
|
|
expiresAt time.Time
|
|
username string
|
|
password string
|
|
loaded bool
|
|
}
|
|
|
|
// KeeperOption configures Keeper.
|
|
type KeeperOption func(*Keeper)
|
|
|
|
// WithGlobalToken sets a static host bearer (overrides file until empty).
|
|
func WithGlobalToken(tok string) KeeperOption {
|
|
return func(k *Keeper) {
|
|
k.globalToken = strings.TrimSpace(tok)
|
|
}
|
|
}
|
|
|
|
// WithGlobalTokenEnv loads VERAE_GLOBAL_TOKEN when Token is called.
|
|
func WithGlobalTokenEnv(enabled bool) KeeperOption {
|
|
return func(k *Keeper) {
|
|
k.useEnv = enabled
|
|
}
|
|
}
|
|
|
|
// NewKeeper builds a host-token keeper bound to client and auth.properties path.
|
|
func NewKeeper(client *Client, authPath string, opts ...KeeperOption) *Keeper {
|
|
k := &Keeper{
|
|
client: client,
|
|
authPath: authPath,
|
|
useEnv: true,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(k)
|
|
}
|
|
return k
|
|
}
|
|
|
|
// Token returns a usable host bearer: VERAE_GLOBAL_TOKEN, stored access_token,
|
|
// or a fresh login persisted to auth.properties. Never logs the token.
|
|
func (k *Keeper) Token(ctx context.Context) (string, error) {
|
|
if k == nil {
|
|
return "", fmt.Errorf("veraetime: nil keeper")
|
|
}
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
|
|
if k.useEnv {
|
|
if env := strings.TrimSpace(os.Getenv("VERAE_GLOBAL_TOKEN")); env != "" {
|
|
return env, nil
|
|
}
|
|
}
|
|
if tok := strings.TrimSpace(k.globalToken); tok != "" {
|
|
return tok, nil
|
|
}
|
|
|
|
if err := k.loadLocked(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if k.token != "" && !k.needsRefreshLocked() {
|
|
return k.token, nil
|
|
}
|
|
if k.username == "" || k.password == "" {
|
|
if k.token != "" {
|
|
return k.token, nil
|
|
}
|
|
return "", fmt.Errorf("veraetime: no host token (set VERAE_GLOBAL_TOKEN or auth.properties)")
|
|
}
|
|
if k.client == nil {
|
|
return "", fmt.Errorf("veraetime: nil client")
|
|
}
|
|
if k.authPath != "" && k.client.authPath == "" {
|
|
k.client.authPath = k.authPath
|
|
}
|
|
res, err := k.client.Login(ctx, k.username, k.password)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
props, err := ReadProperties(k.authPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tok := storedAccessToken(props)
|
|
if tok == "" {
|
|
return "", fmt.Errorf("veraetime: login did not persist access_token")
|
|
}
|
|
k.token = tok
|
|
if exp, ok := ParseExpiry(res.ExpiresAt); ok {
|
|
k.expiresAt = exp
|
|
}
|
|
return k.token, nil
|
|
}
|
|
|
|
func (k *Keeper) loadLocked() error {
|
|
if k.loaded {
|
|
return nil
|
|
}
|
|
k.loaded = true
|
|
if k.authPath == "" {
|
|
return nil
|
|
}
|
|
props, err := ReadProperties(k.authPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if k.token == "" {
|
|
k.token = storedAccessToken(props)
|
|
}
|
|
k.username = props["username"]
|
|
k.password = props["password"]
|
|
if exp, ok := ParseExpiry(props["expires_at"]); ok {
|
|
k.expiresAt = exp
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (k *Keeper) needsRefreshLocked() bool {
|
|
if k.username == "" || k.password == "" {
|
|
return false
|
|
}
|
|
if k.token == "" || k.expiresAt.IsZero() {
|
|
return true
|
|
}
|
|
return !time.Now().UTC().Before(k.expiresAt.Add(-TokenRefreshSkew))
|
|
}
|