verae-mcp-timestamping-serv.../internal/veraetime/keeper.go
George Lambert 348ccd0bf2 feat: VeraeTime client, auth.properties, and host-token keeper
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.
2026-09-29 11:57:40 -04:00

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))
}