Documentation
¶
Overview ¶
Package secret resolves, stores and fingerprints PayCLI's credentials (§5.1, §5.2, §4.4).
Three rules govern everything here:
- Storage is file-first. credentials.json at 0600 is the default target; the OS keychain is used only when the profile opted in or PAY_KEYRING asks for it (§5.2).
- A resolved credential never reaches stdout, a log, an audit record, a cache file or an error message. Only its 16-hex fingerprint does (§5.3).
- Nothing in this package reads the process environment or the clock directly: both arrive as arguments, so the ten-step chain of §5.1 is a table test (§3.1).
Index ¶
- Constants
- func APIKeyEnvNames(profile string) []string
- func CheckPerm(path string) error
- func EnvSuffix(profile string) string
- func Fingerprint(credential string) string
- func FingerprintFor(mode Mode, credential string) string
- func JWTEnvNames(profile string) []string
- func KeyringAccount(profile string) string
- func PreviewSecretEnvNames(profile string) []string
- type Credential
- type Credentials
- type Env
- type FileStore
- func (s *FileStore) ClearPreviewSecret(profile string) (bool, error)
- func (s *FileStore) Delete(profile string) (bool, error)
- func (s *FileStore) FixPerms() error
- func (s *FileStore) Get(profile string) (*Record, bool, error)
- func (s *FileStore) Load() (*Credentials, error)
- func (s *FileStore) Profiles() ([]string, error)
- func (s *FileStore) Put(profile string, rec *Record, now time.Time) error
- func (s *FileStore) Rename(oldName, newName string) (bool, error)
- func (s *FileStore) SetPreviewSecret(profile, value string, now time.Time) error
- type Flags
- type Helper
- type HelperRunner
- type Input
- type JWT
- type Keyring
- type KeyringMode
- type Mode
- type PreviewInput
- type PreviewSecret
- type Record
- type SaveResult
- type Store
- func (s *Store) KeyringMode() KeyringMode
- func (s *Store) KeyringOnly(ctx context.Context, profile string) (string, bool, error)
- func (s *Store) Logout(ctx context.Context, profile string) (bool, error)
- func (s *Store) Lookup(ctx context.Context, profile string) (*Record, string, error)
- func (s *Store) Path() string
- func (s *Store) SaveAPIKey(ctx context.Context, profile, key string, useKeyring bool, now time.Time) (SaveResult, error)
- func (s *Store) SaveJWT(ctx context.Context, profile string, token JWT, identifier, field string, ...) (SaveResult, error)
Constants ¶
const ( SourceStdin = "flag:--api-key-stdin" SourceKeyFile = "flag:--api-key-file" SourceFlag = "flag:--api-key" SourceProfileEnv = "env:api_key_env" SourceCredFile = "file:credentials.json" SourceKeyringName = "keyring" SourceHelper = "credential_helper" SourceStoredJWT = "file:credentials.json (jwt)" SourceNone = "none" )
Source strings recorded by each step of §5.1, for `pay config explain` and `pay auth status`. They name a location, never a value.
const ( EnvAPIKey = "PAY_API_KEY" EnvJWT = "PAY_JWT" )
Environment variable names of §4.6.
const ( SourcePreviewEnvVar = "env:preview_secret_env" SourcePreviewCredFile = "file:credentials.json (preview_secret)" )
Preview-secret source strings. They name a location, never a value.
const ( TargetFile = "file" TargetKeyring = "keyring" )
Storage targets reported by SaveResult.
const Anon = "anon"
Anon is the fingerprint of the anonymous identity (§4.4, §8.1). It is a literal, not a hash: there is no credential to hash, and a fixed sentinel keeps anonymous discovery in its own cache scope.
const CredentialsVersion = 1
CredentialsVersion is the schema version of the file.
const Domain = redact.FingerprintDomain
Domain is §4.4's fingerprint domain separator. It exists so that a fingerprint cannot be compared against a bare sha256 of the key computed elsewhere, and so two PayCLI-era hash schemes can never collide. It is redact.FingerprintDomain: there is exactly one formula (§4.4).
const EnvPreviewSecret = "PAY_PREVIEW_SECRET"
EnvPreviewSecret is F9's environment variable for the site's draft-preview secret. Like PAY_API_KEY it has a per-profile form, PAY_PREVIEW_SECRET_<P>.
const ExpirySkew = 60 * time.Second
ExpirySkew is §5.0's "within 60 s counts as expired" window. A token this close to its expiry is treated as absent, so PayCLI re-logs-in *before* sending — which is a fresh request and therefore safe for writes too.
const FilePerm fs.FileMode = 0o600
FilePerm is credentials.json's mandatory mode (§4.4).
const FingerprintLen = redact.FingerprintLen
FingerprintLen is the number of hex characters every producer emits (§4.4). Sixteen everywhere — credentials.json, manifest.meta.key_fingerprint, manifest.identity.key_fingerprint, `pay auth status`, `pay cache ls`, `pay doctor` and §8.1's scope input — so a profile can always be matched to a cache scope by string equality.
const HelperTimeout = 10 * time.Second
HelperTimeout bounds the credential helper (§5.1 step 9). Ten seconds is enough for `op read`, a Vault round trip or an AWS Secrets Manager call, and short enough that a hung helper does not hang an agent.
const (
KeyringService = "pay-cli"
)
Keychain identifiers (§5.1 step 8).
const KeyringTimeout = 2 * time.Second
KeyringTimeout bounds every keychain call. A dbus or wincred prompt that never returns must not hang a CLI an agent is driving (§5.1 step 8).
Variables ¶
This section is empty.
Functions ¶
func APIKeyEnvNames ¶
APIKeyEnvNames lists the API-key variables in §5.1 order: the profile-specific one first, then the general one.
func CheckPerm ¶
CheckPerm enforces §5.1's "refuses to read if the file mode is group/world readable" for credentials.json and for --api-key-file.
The check is skipped on Windows, where Unix permission bits are synthesised by the Go runtime and would report a false positive on every file.
func EnvSuffix ¶
EnvSuffix renders a profile name as the UPPER_SNAKE suffix of §4.6's PAY_API_KEY_<PROFILE> and PAY_JWT_<PROFILE> variables. Every character that cannot appear in an environment variable name becomes an underscore, and a leading digit is prefixed with one, so any profile name a user can type maps to a legal variable.
func Fingerprint ¶
Fingerprint returns the first 16 hex characters of sha256("paycli-key-v1\x00" + credential) (§4.4).
The raw credential is not derivable from the result: SHA-256 is preimage resistant and the output is truncated to 64 bits of a 256-bit digest. An empty credential yields Anon rather than the hash of the empty string, so a caller that forgot to check cannot publish a constant that looks like a real key fingerprint.
func FingerprintFor ¶
FingerprintFor is Fingerprint keyed by auth mode: the API key in api-key mode, the JWT in jwt mode, the literal "anon" in anonymous mode (§4.4).
func JWTEnvNames ¶
JWTEnvNames is APIKeyEnvNames for PAY_JWT.
func KeyringAccount ¶
KeyringAccount is the keychain account name for a profile.
func PreviewSecretEnvNames ¶ added in v0.3.0
PreviewSecretEnvNames lists the variables in resolution order: the profile-specific one first, then the general one (the §5.1 steps 4-5 order).
Types ¶
type Credential ¶
type Credential struct {
Mode Mode
Value string
Source string
Fingerprint string
// Exp, LoginIdentifier and LoginField are set in jwt mode (§5.0).
Exp time.Time
LoginIdentifier string
LoginField string
// Warnings are non-fatal observations: an --api-key typed on a TTY, a
// keychain that could not be read while a usable fallback existed.
Warnings []string
}
Credential is the outcome of the chain. Value is the secret itself and must never be logged, printed, cached or put in an error; Fingerprint is the only identity that may leave the process (§5.3).
func Resolve ¶
func Resolve(ctx context.Context, in Input) (*Credential, error)
Resolve walks §5.1's ten steps, first hit wins.
Two shortcuts precede the walk, both from §5.1's closing paragraph: --auth-mode anonymous short-circuits the whole chain and sends no credential even when one is available; an explicit api-key or jwt mode restricts the chain to that mode's sources, so a stale token cannot satisfy --auth-mode api-key.
Step 11 is the important one: when nothing resolves and the mode is auto or anonymous, this returns an anonymous credential and no error. Refusing to run without a credential would make PayCLI useless against a project with public read access.
func (*Credential) Anonymous ¶
func (c *Credential) Anonymous() bool
Anonymous reports whether the command will run without an Authorization header (§5.0).
type Credentials ¶
type Credentials struct {
Version int `json:"version"`
Profiles map[string]*Record `json:"profiles"`
}
Credentials is the whole file.
type Env ¶
Env is the environment snapshot the chain reads. config.Env satisfies it, and so does any test map: the interface exists so internal/secret does not depend on internal/config, and so §4.5's "set but empty counts as unset" rule is the caller's single implementation.
type FileStore ¶
type FileStore struct {
Path string
}
FileStore is §5.2's default storage target: credentials.json, 0600, written atomically.
func NewFileStore ¶
NewFileStore binds a store to a path (config.Paths.CredentialsFile()).
func (*FileStore) ClearPreviewSecret ¶ added in v0.3.0
ClearPreviewSecret removes a profile's stored preview secret and reports whether there was one. A record left with no credential at all is removed entirely, so `pay auth list` does not show an empty entry.
func (*FileStore) Delete ¶
Delete removes a profile's record. It reports whether anything was removed so `pay auth logout` can say so.
func (*FileStore) FixPerms ¶
FixPerms implements `pay auth fix-perms`: chmod 0600 the credentials file and 0700 its directory.
func (*FileStore) Load ¶
func (s *FileStore) Load() (*Credentials, error)
Load reads the file. A missing file is not an error — it is the state of every fresh install.
Before reading a single byte it enforces §5.1 step 7: a group- or world-readable credentials file is refused, not quietly used.
func (*FileStore) Put ¶
Put stores (or replaces) a profile's record, stamping the fingerprint and updated_at. now is a parameter because §3.1 forbids time.Now outside app.go.
func (*FileStore) SetPreviewSecret ¶ added in v0.3.0
SetPreviewSecret stores a profile's preview secret in credentials.json, creating the record when the profile has no API credential yet. The API credential, if any, is left untouched.
type Flags ¶
type Flags struct {
APIKey string
APIKeyStdin bool
APIKeyFile string
// Stdin is where --api-key-stdin reads from. A nil reader means standard
// input is not available, which makes the flag an error rather than a hang.
Stdin io.Reader
// IsTTY drives the §5.1 step-3 warning.
IsTTY bool
}
Flags are the credential-bearing root flags of §9.1.
type Helper ¶
type Helper struct {
Command string
Timeout time.Duration
Run HelperRunner
}
Helper is the `credential_helper` exec hook: a shell command whose stdout is the credential (§5.1 step 9). This is how 1Password, Vault and AWS Secrets Manager are supported with zero extra Go dependencies.
func (*Helper) Configured ¶
Configured reports whether there is anything to run.
func (*Helper) Resolve ¶
Resolve runs the helper and returns its trimmed stdout.
A zero exit with empty stdout is "no credential here", not a failure: the chain continues to the next step. A non-zero exit is auth_helper_failed (exit 2), because a helper that was configured and broke is a real problem the caller must see.
type HelperRunner ¶
HelperRunner executes the helper. It is a field so tests never fork a process.
type Input ¶
type Input struct {
Profile string
BaseURL string
Env Env
Flags Flags
Mode Mode
// APIKeyEnv is the profile's api_key_env (§5.1 step 6).
APIKeyEnv string
// Helper is the profile's credential_helper (§5.1 step 9).
Helper *Helper
Store *Store
Now time.Time
}
Input is everything the chain may consider. Everything that would otherwise be ambient — the environment, the clock, stdin — is a field, so §5.1 is a table test.
type JWT ¶
JWT is the credential record of §5.2: the minted token and its expiry, and nothing else. The password that produced it is never stored, anywhere, in any form.
func NewJWT ¶
NewJWT builds a record from Payload's login response, whose `exp` is Unix seconds. A zero or absent exp falls back to the token's own claim.
func ParseJWT ¶
ParseJWT reads the `exp` claim out of a JWT without verifying the signature — PayCLI is not the issuer and cannot verify it; the server remains the only authority. The claim is used purely to decide whether it is worth sending.
A token whose payload cannot be decoded is returned with a zero Exp rather than an error, because an opaque token is still worth sending: the server, not PayCLI, decides whether it is valid. A structurally non-JWT string is an error, since that is a configuration mistake.
func (JWT) ExpiresIn ¶
ExpiresIn is the remaining lifetime, for `pay auth status`. It is zero when the expiry is unknown or already past.
func (JWT) Fingerprint ¶
Fingerprint is the §4.4 fingerprint of the token itself.
type Keyring ¶
type Keyring struct {
Mode KeyringMode
Timeout time.Duration
GetFunc func(service, account string) (string, error)
SetFunc func(service, account, secret string) error
DeleteFunc func(service, account string) error
}
Keyring is the opt-in OS keychain backend. The three function fields are injection points: unit tests never touch dbus or wincred (§17.1).
func NewKeyring ¶
func NewKeyring(mode KeyringMode) *Keyring
NewKeyring builds a keychain bound to the real OS backend.
Mode "off" short-circuits every call before the backend is touched at all, which is what PAY_KEYRING=off promises: no dbus, no wincred, no prompt.
type KeyringMode ¶
type KeyringMode string
KeyringMode mirrors PAY_KEYRING (§5.2). It is declared here rather than imported from internal/config so that this package stays independent of the configuration layer; the values are identical strings.
const ( KeyringAuto KeyringMode = "auto" KeyringOff KeyringMode = "off" KeyringForce KeyringMode = "force" )
Keyring modes.
type Mode ¶
type Mode string
Mode is §5.0's resolved auth mode. The string values match config.AuthMode and apierr's AuthMode* constants exactly; the type is re-declared here so internal/secret depends on neither package for its own vocabulary.
type PreviewInput ¶ added in v0.3.0
type PreviewInput struct {
Profile string
Env Env
// SecretEnv is the profile's preview_secret_env: the NAME of the variable
// the site itself reads (e.g. PREVIEW_SECRET), so an agent can reuse the
// app's own .env without copying the value anywhere.
SecretEnv string
Store *Store
}
PreviewInput is everything the preview-secret chain may consider.
type PreviewSecret ¶ added in v0.3.0
PreviewSecret is a resolved preview secret. Value must never be printed, logged or cached; Fingerprint (the §4.4 formula) is the only identity that may leave the process.
func ResolvePreviewSecret ¶ added in v0.3.0
func ResolvePreviewSecret(in PreviewInput) (*PreviewSecret, error)
ResolvePreviewSecret walks F9's chain, first hit wins:
- $PAY_PREVIEW_SECRET_<PROFILE_UPPER_SNAKE>
- $PAY_PREVIEW_SECRET
- the variable named by the profile's preview_secret_env
- the profile's credentials.json record (`pay auth preview-secret --stdin`)
Nothing resolved is (nil, nil): whether that is an error depends on whether the preview_path template needs a {secret} at all, which only the caller knows. The OS keychain is not consulted — the preview secret is stored in credentials.json (0600) only.
type Record ¶
type Record struct {
AuthMode Mode `json:"auth_mode"`
APIKey string `json:"api_key,omitempty"`
// Keyring is true when the real secret lives in the OS keychain and this
// record is only the pointer to it (§5.1 step 8, §5.2).
Keyring bool `json:"keyring,omitempty"`
Token string `json:"token,omitempty"`
TokenExp *time.Time `json:"token_exp,omitempty"`
LoginIdentifier string `json:"login_identifier,omitempty"`
LoginField string `json:"login_field,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
// PreviewSecret is the site's draft-preview secret (F9: the Payload
// website template's PREVIEW_SECRET). It is a credential in its own right
// and independent of the API credential: `pay auth login` replacing the
// key or token keeps it (see Put), and `pay auth logout` removes it with
// the rest of the record.
PreviewSecret string `json:"preview_secret,omitempty"`
}
Record is one profile's stored credential (§4.4).
There is deliberately no password field and never will be: `pay auth login --jwt` keeps only the minted token and its expiry (§5.0).
func (*Record) HasAPICredential ¶ added in v0.3.0
HasAPICredential reports whether the record carries an API credential at all. A record that holds only a preview secret does not authenticate anything.
type SaveResult ¶
type SaveResult struct {
Target string
Path string
Fingerprint string
// Warning is set when the keychain was tried and the file was used
// instead (PAY_KEYRING=auto only).
Warning string
}
SaveResult says where a credential actually landed. The caller prints it: §5.2's rule is that the user must always know where the secret went.
type Store ¶
Store is §5.2's storage policy: credentials.json first, the OS keychain only on explicit opt-in.
func NewStore ¶
func NewStore(credentialsPath string, mode KeyringMode) *Store
NewStore binds the default file store and a keychain in the given mode.
func (*Store) KeyringMode ¶
func (s *Store) KeyringMode() KeyringMode
KeyringMode reports the configured keychain policy (§5.2).
func (*Store) KeyringOnly ¶
KeyringOnly reads a keychain entry for a profile that has no file record at all, which is what PAY_KEYRING=force means for a machine that never wrote credentials.json.
func (*Store) Lookup ¶
Lookup returns a profile's stored record together with the secret it points at, resolving a "keyring": true record through the keychain (§5.1 steps 7 and 8).
The keychain is consulted only when the record opted in or the mode is force, exactly as §5.1 step 8 requires.