Documentation
¶
Overview ¶
Package credentials stores provider secrets keyed by Ref{Provider, Account}. Values live in the OS keychain; the refs themselves live in a separate index, because keychains cannot enumerate.
It is deliberately a leaf: no dependency on app, flow, or any connector, so a connector package can import it without a cycle — the same rule that put the connector vocabulary in cmd/desktop/internal/app/sources/connector.
Two rules govern everything here. **Config holds refs, never tokens**: flows/ is dotfiles-managed, so a token embedded in a node's config is a token in a git repo. And **the keychain is the truth, the index is a cache**: a ref present in the index whose keychain entry is gone reads as absent and is pruned, because the alternative is a "Connected" badge over a credential that no longer exists.
Index ¶
Constants ¶
const (
// EnvKeyringService gives development launches an isolated OS keychain namespace.
EnvKeyringService = "HIVE_DESKTOP_DEVELOPMENT_KEYRING_SERVICE"
)
Variables ¶
var ErrNotFound = errors.New("credentials: not found")
ErrNotFound is returned by Get for a ref with no stored value. It is a distinct error rather than a zero Secret so a caller cannot mistake "no credential" for "an empty credential" — the first is an ordinary state that means "not connected", the second is corruption.
Functions ¶
func EnvOverrideName ¶
EnvOverrideName is the environment variable that overrides every stored credential for one provider: HIVE_GITHUB_TOKEN, HIVE_GRAFANA_TOKEN.
It is provider-wide rather than per-account because its purpose is headless operation — CI, the server build, the e2e harness — where there is one identity and no keychain to read. A machine with several accounts configured is not the case it serves.
func HasEnvOverride ¶
HasEnvOverride reports whether a provider's environment override is set.
It is what lets a caller tell "connected with no stored credential" from "not connected": the override is provider-wide and names no account, so a listing of stored refs is empty for it while every fetch still succeeds. Reporting that as disconnected would contradict the working feed beside it.
Types ¶
type KeychainStore ¶
type KeychainStore struct {
// contains filtered or unexported fields
}
KeychainStore keeps values in the OS keychain and refs in a JSON index beside the app's other state.
The index exists because keychains do not enumerate: zalando/go-keyring has no List, and neither does the API underneath it. Only refs go in the file — never values — so a leaked or backed-up index discloses which accounts are configured and nothing more.
func NewKeychainStore ¶
func NewKeychainStore(indexPath string) *KeychainStore
NewKeychainStore builds a store whose ref index lives at indexPath. The path is a parameter rather than derived from cmd/desktop/internal/app/settings so this package stays a leaf and a test can point it at a temp dir.
func NewKeychainStoreWithService ¶
func NewKeychainStoreWithService(indexPath, service string) *KeychainStore
NewKeychainStoreWithService builds a store in a specific OS keychain namespace.
func (*KeychainStore) Delete ¶
func (s *KeychainStore) Delete(ref Ref) error
Delete removes a credential and its ref. A ref absent from either side is not an error: deleting what is already gone is the caller's intent either way.
func (*KeychainStore) Get ¶
func (s *KeychainStore) Get(ref Ref) (string, error)
Get reads one credential. A ref listed in the index whose keychain entry is gone — revoked in Keychain Access, restored from a backup without it — is pruned and reported as absent: the index is a cache and the keychain is the truth, and the alternative is a "Connected" badge over nothing.
func (*KeychainStore) List ¶
func (s *KeychainStore) List() ([]Ref, error)
List returns every recorded ref, sorted.
It reads the index alone and does not verify each ref against the keychain: on macOS every keychain read can prompt, and a status screen that enumerates connectors must not turn into one prompt per provider. Callers that need certainty about a single credential use Get, which does check.
func (*KeychainStore) Set ¶
func (s *KeychainStore) Set(ref Ref, value string) error
Set stores a credential and records its ref. The keychain is written first: an index entry with no keychain value is the divergence Get already self-heals, whereas a keychain value with no index entry is invisible to List and leaks silently.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore keeps credentials in memory. It backs mock modes and tests: a keychain read can prompt, and a suite that prompts is a suite that hangs in CI.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
func (*MemoryStore) Delete ¶
func (s *MemoryStore) Delete(ref Ref) error
func (*MemoryStore) List ¶
func (s *MemoryStore) List() ([]Ref, error)
type Ref ¶
type Ref struct {
// Provider names the connector family: "github", "grafana".
Provider string
// Account distinguishes several credentials for one provider — a login, a
// hostname, an environment.
Account string
}
Ref identifies one credential. A Value Object: immutable, compared by value, self-validating, with no identity of its own. A credential is referred to by this and never by a bare string, so "which account" cannot be lost in a signature that takes two strings in the wrong order.
Refs compare exactly and are not case-normalised. Providers come from connector descriptor types, which are constants.
func ListProvider ¶
ListProvider returns every stored ref for one provider, sorted. It is what an Integrations screen enumerates and what a connector offers when a node picks which account to use.
type Resolver ¶
Resolver returns a credential's current value, or "" when none is stored.
Empty-and-no-error is deliberate: with providers configured per connector rather than gating the app, "not connected" is an ordinary state a caller reports, not an error it propagates. A consumer that needs a token maps "" to its own "not authenticated" error at the point it would have used one.
A Resolver reads the store on every call. That is what makes connecting, rotating, or disconnecting a credential take effect on the next use — nothing captures the value, so there is no cache to invalidate.
type Store ¶
type Store interface {
Get(ref Ref) (string, error)
Set(ref Ref, value string) error
Delete(ref Ref) error
List() ([]Ref, error)
}
Store persists credentials. Get returns ErrNotFound when a ref has no value; List enumerates the refs that have one.
Values are plain strings. A redacting wrapper type earns its place when secrets are loaded from config and flow through structs that get marshalled and logged — but the rule above means config carries refs and never tokens, so no such path exists here. A value's whole life is keychain → connector factory → provider client, and none of those marshal it.
A consumer-defined interface lives with its consumer, but this one is declared here on purpose: it has two implementations in this package (keychain and memory) and the narrow views its callers want — "read one credential" — are declared at those call sites instead.