Files
fn_registry/functions/infra/keyring_token_store.go
T
egutierrez c441366f89 feat(matrix-mas): 3 helpers for matrix_client_pc (issue 0147)
- mas_oidc_loopback_go_infra: OAuth2 PKCE + loopback HTTP for desktop login
- keyring_token_store_go_infra: persist OAuth tokens in SO keychain
- matrix_client_init_go_infra: init mautrix.Client from access_token + whoami

Plus go.work workspace including matrix_client_pc sub-repo for shared
import path during dev. All 3 fns tagged matrix-mas capability group.

Tests: TestMasOidcLoopback (15 cases), TestKeyringTokenStore (5 cases,
SKIP on headless), TestMatrixClientInit (6 cases) — all green/skip.

Refs: dev/issues/0147-matrix-client-pc-scaffold.md
Refs: dataforge/matrix_client_pc commit f28c2b1
2026-05-24 23:23:49 +02:00

80 lines
2.7 KiB
Go

package infra
import (
"encoding/json"
"errors"
"fmt"
"time"
keyring "github.com/zalando/go-keyring"
)
// ErrNotFound is returned by Load when no token exists for the given account.
var ErrNotFound = errors.New("token not found in keyring")
// Token holds OAuth/OIDC credentials that need to survive app restarts.
type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"` // zero = never expires
UserID string `json:"user_id"`
DeviceID string `json:"device_id,omitempty"`
HomeserverURL string `json:"homeserver_url"`
Issuer string `json:"issuer,omitempty"` // MAS/OIDC issuer URL
ClientID string `json:"client_id,omitempty"` // MAS client_id used
}
// KeyringTokenStore persists tokens in the OS keyring (Secret Service on Linux,
// Keychain on macOS, Credential Manager on Windows).
type KeyringTokenStore struct {
// Service is the keyring namespace. Keep it stable across app versions.
// Example: "fn_registry.matrix_client_pc"
Service string
}
// NewKeyringTokenStore returns a store scoped to the given service name.
func NewKeyringTokenStore(service string) *KeyringTokenStore {
return &KeyringTokenStore{Service: service}
}
// Save serialises t to JSON and writes it to the keyring under (service, account).
// Overwrites silently if an entry already exists.
// account is typically the user ID, e.g. "@user:homeserver.example.com".
func (s *KeyringTokenStore) Save(account string, t Token) error {
b, err := json.Marshal(t)
if err != nil {
return fmt.Errorf("keyring save: marshal: %w", err)
}
if err := keyring.Set(s.Service, account, string(b)); err != nil {
return fmt.Errorf("keyring save: %w", err)
}
return nil
}
// Load retrieves and deserialises the token stored under (service, account).
// Returns ErrNotFound if no entry exists. Callers should check with errors.Is.
func (s *KeyringTokenStore) Load(account string) (*Token, error) {
raw, err := keyring.Get(s.Service, account)
if err != nil {
if errors.Is(err, keyring.ErrNotFound) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("keyring load: %w", err)
}
var t Token
if err := json.Unmarshal([]byte(raw), &t); err != nil {
return nil, fmt.Errorf("keyring load: unmarshal: %w", err)
}
return &t, nil
}
// Delete removes the token for account from the keyring.
// Idempotent: if no entry exists, returns nil.
func (s *KeyringTokenStore) Delete(account string) error {
err := keyring.Delete(s.Service, account)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
return fmt.Errorf("keyring delete: %w", err)
}
return nil
}