Documentation
¶
Overview ¶
Package cliconfig manages the user's CLI credentials in ~/.instant-config.
SECURITY MODEL (post-2026-05-20, T16 P1-1 fix)
The user's API key is stored in the OS keychain via the secretstore package — macOS Keychain, Linux Secret Service / libsecret, or Windows Credential Manager. When no keychain is available (headless Linux, CI, sandboxed environments) we fall back to writing the key into ~/.instant-config mode 0600 and print a one-time stderr warning so the user knows their bearer token is on disk.
~/.instant-config itself always stores the non-secret display fields (email, plan tier, team name, API base URL, last-saved timestamp) so `instant whoami` can still answer offline without prompting the keychain.
Index ¶
Constants ¶
const FallbackAPIKeyField = "api_key_fallback"
FallbackAPIKeyField is the JSON field name we use when the keychain is unavailable and we have to write the bearer token to ~/.instant-config. Tests assert that this field is ABSENT when the keychain backend is in use (T16 P1-1 regression).
Variables ¶
var ErrNotLoggedIn = errors.New("not logged in — run `instant login` to authenticate")
ErrNotLoggedIn is returned when an action requires authentication but no API key is found.
Functions ¶
Types ¶
type Config ¶
type Config struct {
// APIKey is the bearer token sent with every authenticated API request.
// In-memory only; persistence routes through secretstore.
// Empty = anonymous mode.
APIKey string `json:"-"`
// LegacyAPIKey captures any value found in the old `api_key` field on
// disk so we can migrate it to the keychain on Save().
LegacyAPIKey string `json:"api_key,omitempty"`
// FallbackAPIKey is written ONLY when the keychain is unavailable and
// we must store the secret on disk. Tests assert this is empty when
// the keychain is in use.
FallbackAPIKey string `json:"api_key_fallback,omitempty"`
// Email is the account email, stored for display in `instant whoami`.
Email string `json:"email,omitempty"`
// Tier is the current plan: "anonymous", "hobby", "pro", "team".
Tier string `json:"tier,omitempty"`
// TeamName is the team name, if the user belongs to a team.
TeamName string `json:"team_name,omitempty"`
// APIBaseURL overrides the default https://api.instanode.dev endpoint.
// Populated when INSTANT_API_URL env var was set at login time.
APIBaseURL string `json:"api_base_url,omitempty"`
// SavedAt is when the config was last written (for staleness checks).
SavedAt time.Time `json:"saved_at,omitempty"`
// contains filtered or unexported fields
}
Config holds the authenticated user's non-secret display data plus an in-memory copy of the API key loaded from the secretstore (or, on the file-fallback path, from disk).
The on-disk JSON shape includes only:
- non-secret fields (email, tier, team_name, api_base_url, saved_at)
- api_key_fallback (set ONLY when the keychain is unavailable)
The legacy `api_key` JSON field is no longer written. We still READ it on Load() so existing installs are migrated transparently into the keychain on the next Save().
func Load ¶
Load reads ~/.instant-config from disk and, where the keychain is available, also reads the API key out of the secretstore.
Migration: if the legacy `api_key` field is present on disk (a config written before this fix) we load it into APIKey and clear it on the next Save() — keychain takes over.
func (*Config) EffectiveTier ¶
EffectiveTier returns the tier string, defaulting to "anonymous".
func (*Config) IsAuthenticated ¶
IsAuthenticated reports whether the config holds valid credentials.
func (*Config) Save ¶
Save writes the non-secret fields to disk and routes the API key into the secretstore. If the secretstore Set fails (no keychain), the key falls back to ~/.instant-config and a one-time stderr warning is emitted.
The on-disk file is always written mode 0600 via an atomic temp+rename.
func (*Config) SecretBackendName ¶
SecretBackendName surfaces which secret backend is in use (for `whoami` to truthfully report "Key stored in: macOS Keychain" vs "file").