secret

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
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.

View Source
const (
	EnvAPIKey = "PAY_API_KEY"
	EnvJWT    = "PAY_JWT"
)

Environment variable names of §4.6.

View Source
const (
	SourcePreviewEnvVar   = "env:preview_secret_env"
	SourcePreviewCredFile = "file:credentials.json (preview_secret)"
)

Preview-secret source strings. They name a location, never a value.

View Source
const (
	TargetFile    = "file"
	TargetKeyring = "keyring"
)

Storage targets reported by SaveResult.

View Source
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.

View Source
const CredentialsVersion = 1

CredentialsVersion is the schema version of the file.

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).

View Source
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>.

View Source
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.

View Source
const FilePerm fs.FileMode = 0o600

FilePerm is credentials.json's mandatory mode (§4.4).

View Source
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.

View Source
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.

View Source
const (
	KeyringService = "pay-cli"
)

Keychain identifiers (§5.1 step 8).

View Source
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

func APIKeyEnvNames(profile string) []string

APIKeyEnvNames lists the API-key variables in §5.1 order: the profile-specific one first, then the general one.

func CheckPerm

func CheckPerm(path string) error

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

func EnvSuffix(profile string) string

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

func Fingerprint(credential string) string

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

func FingerprintFor(mode Mode, credential string) string

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

func JWTEnvNames(profile string) []string

JWTEnvNames is APIKeyEnvNames for PAY_JWT.

func KeyringAccount

func KeyringAccount(profile string) string

KeyringAccount is the keychain account name for a profile.

func PreviewSecretEnvNames added in v0.3.0

func PreviewSecretEnvNames(profile string) []string

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

type Env interface {
	Lookup(name string) (string, bool)
}

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.

func MapEnv

func MapEnv(m map[string]string) Env

MapEnv adapts a plain map to Env.

type FileStore

type FileStore struct {
	Path string
}

FileStore is §5.2's default storage target: credentials.json, 0600, written atomically.

func NewFileStore

func NewFileStore(path string) *FileStore

NewFileStore binds a store to a path (config.Paths.CredentialsFile()).

func (*FileStore) ClearPreviewSecret added in v0.3.0

func (s *FileStore) ClearPreviewSecret(profile string) (bool, error)

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

func (s *FileStore) Delete(profile string) (bool, error)

Delete removes a profile's record. It reports whether anything was removed so `pay auth logout` can say so.

func (*FileStore) FixPerms

func (s *FileStore) FixPerms() error

FixPerms implements `pay auth fix-perms`: chmod 0600 the credentials file and 0700 its directory.

func (*FileStore) Get

func (s *FileStore) Get(profile string) (*Record, bool, error)

Get returns the record for one profile.

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) Profiles

func (s *FileStore) Profiles() ([]string, error)

Profiles lists the stored profile names, sorted.

func (*FileStore) Put

func (s *FileStore) Put(profile string, rec *Record, now time.Time) error

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) Rename

func (s *FileStore) Rename(oldName, newName string) (bool, error)

Rename moves a record between profile names (`pay auth rename`).

func (*FileStore) SetPreviewSecret added in v0.3.0

func (s *FileStore) SetPreviewSecret(profile, value string, now time.Time) error

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 NewHelper

func NewHelper(command string) *Helper

NewHelper builds a helper bound to the real shell.

func (*Helper) Configured

func (h *Helper) Configured() bool

Configured reports whether there is anything to run.

func (*Helper) Resolve

func (h *Helper) Resolve(ctx context.Context) (string, error)

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

type HelperRunner func(ctx context.Context, command string) (stdout, stderr []byte, err error)

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

type JWT struct {
	Token string
	Exp   time.Time
}

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

func NewJWT(token string, exp int64) JWT

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

func ParseJWT(token string) (JWT, error)

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) Expired

func (j JWT) Expired(now time.Time) bool

Expired is the negation of "not yet within the skew window", for reporting.

func (JWT) ExpiresIn

func (j JWT) ExpiresIn(now time.Time) time.Duration

ExpiresIn is the remaining lifetime, for `pay auth status`. It is zero when the expiry is unknown or already past.

func (JWT) Fingerprint

func (j JWT) Fingerprint() string

Fingerprint is the §4.4 fingerprint of the token itself.

func (JWT) Usable

func (j JWT) Usable(now time.Time) bool

Usable reports whether the token is worth sending at now: it must be non-empty and either have no known expiry or expire more than ExpirySkew from now (§5.0).

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.

func (*Keyring) Delete

func (k *Keyring) Delete(ctx context.Context, profile string) error

Delete removes a profile's secret; a missing entry is not an error.

func (*Keyring) Enabled

func (k *Keyring) Enabled() bool

Enabled reports whether the keychain may be touched at all.

func (*Keyring) Get

func (k *Keyring) Get(ctx context.Context, profile string) (string, bool, error)

Get reads a profile's secret. The boolean is false for "no entry", which is not an error.

func (*Keyring) Set

func (k *Keyring) Set(ctx context.Context, profile, value string) error

Set stores a profile's secret.

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.

const (
	ModeAuto      Mode = "auto"
	ModeAPIKey    Mode = "api-key"
	ModeJWT       Mode = "jwt"
	ModeAnonymous Mode = "anonymous"
)

Auth modes.

func ParseMode

func ParseMode(s string) Mode

ParseMode normalises a mode string; anything unrecognised becomes auto, because the configuration layer has already rejected invalid values.

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

type PreviewSecret struct {
	Value       string
	Source      string
	Fingerprint string
}

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:

  1. $PAY_PREVIEW_SECRET_<PROFILE_UPPER_SNAKE>
  2. $PAY_PREVIEW_SECRET
  3. the variable named by the profile's preview_secret_env
  4. 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

func (r *Record) HasAPICredential() bool

HasAPICredential reports whether the record carries an API credential at all. A record that holds only a preview secret does not authenticate anything.

func (Record) JWT

func (r Record) JWT() JWT

JWT returns the record's token as a JWT record.

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

type Store struct {
	File    *FileStore
	Keyring *Keyring
}

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

func (s *Store) KeyringOnly(ctx context.Context, profile string) (string, bool, error)

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) Logout

func (s *Store) Logout(ctx context.Context, profile string) (bool, error)

Logout removes a profile's credential from both backends.

func (*Store) Lookup

func (s *Store) Lookup(ctx context.Context, profile string) (*Record, string, error)

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.

func (*Store) Path

func (s *Store) Path() string

Path is the credentials file path, for messages.

func (*Store) SaveAPIKey

func (s *Store) SaveAPIKey(ctx context.Context, profile, key string, useKeyring bool, now time.Time) (SaveResult, error)

SaveAPIKey stores an API key for a profile (§5.2).

func (*Store) SaveJWT

func (s *Store) SaveJWT(ctx context.Context, profile string, token JWT, identifier, field string,
	useKeyring bool, now time.Time) (SaveResult, error)

SaveJWT stores a minted token and its expiry — never the password (§5.0).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL