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 }