Files
tea/modules/credstore/store.go
Bo-Yi WuandClaude Fable 5 f6d939a8df refactor(credstore): embed credential store and drop sdk-go dependency
- Embed the minimal credstore subset used by tea (SecureStore,
  EncryptedFileStore, KeyringStore, FileStore) as modules/credstore so
  external SDK renames can no longer break the build
- Keep the on-disk format fully compatible: AES-256-GCM values with the
  v1: prefix, credentials.json / credentials.json.enc paths, and the
  Token JSON field names are unchanged, verified by a ciphertext fixture
  generated with sdk-go v1.1.0
- Store the keyring master key under a tea-owned account name
- Reuse the existing kernel-level filelock module instead of the
  upstream lockfile protocol, removing a stale-lock race
- Cover roundtrip, keyring-unavailable fallback, and fixture decryption
  with tests using a mocked keyring
- Remove github.com/go-signet/sdk-go and promote
  github.com/zalando/go-keyring to a direct dependency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:59:07 +08:00

58 lines
2.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package credstore
import (
"errors"
"time"
)
// ErrNotFound indicates that no data was found for the given client ID.
var ErrNotFound = errors.New("not found")
// ErrEmptyClientID is returned when an empty client ID is passed to Save.
var ErrEmptyClientID = errors.New("client ID cannot be empty")
// Store defines the interface for loading, saving, and deleting data by client ID.
type Store[T any] interface {
Load(clientID string) (T, error)
Save(clientID string, data T) error
Delete(clientID string) error
String() string
}
// Token represents saved tokens for a specific client.
type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
IDToken string `json:"id_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
ClientID string `json:"client_id"`
}
// IsExpired reports whether the token has expired.
// Returns false if ExpiresAt is zero (token has no expiry).
func (t *Token) IsExpired() bool {
return !t.ExpiresAt.IsZero() && time.Now().After(t.ExpiresAt)
}
// IsValid reports whether the token has a non-empty access token and is not expired.
func (t *Token) IsValid() bool {
return t.AccessToken != "" && !t.IsExpired()
}
// NewStringKeyringStore creates a KeyringStore for plain string values.
func NewStringKeyringStore(serviceName string) *KeyringStore[string] {
return NewKeyringStore[string](serviceName, StringCodec{})
}
// DefaultTokenSecureStore creates a SecureStore for Token values with sensible defaults.
// Tokens are AES-256-GCM-encrypted to filePath+".enc" with the master key in
// the OS keyring; see DefaultSecureStore for details.
func DefaultTokenSecureStore(serviceName, filePath string) *SecureStore[Token] {
return DefaultSecureStore[Token](serviceName, filePath, JSONCodec[Token]{})
}