Documentation
¶
Overview ¶
Package credentials is the ONE on-disk store of control-plane access tokens, shared by every CLI that talks to a Reliant Labs control plane — `forge login` and `reliant auth login` both write it, and every command of both reads it.
── ONE FILE, KEYED BY ENDPOINT, NO "CURRENT" ─────────────────────────
The file maps a NORMALIZED endpoint URL, then a client id, to the credential that client holds for that endpoint:
{
"version": 1,
"credentials": {
"https://admin.reliantapi.com": {
"forge-cli": {"token": "rlat_…", "scopes": ["deploy:write", …], …}
},
"http://127.0.0.1:8090": {
"forge-cli": {"token": "rlat_…", …},
"reliant-cli": {"token": "rlat_…", …}
}
}
}
WHY THE CLIENT LEVEL. The endpoint is the server a token is PRESENTED to. Two CLIs can present tokens to the same origin with different scopes: in a dev stack one admin-server serves both forge's deploy API and reliant's proxied API. Keyed by endpoint alone, each CLI's login would silently replace the other's token with one that lacks its scopes. A client id is not a "current" selection; each CLI always reads its own.
It answers exactly one question — "what credential does client C hold for endpoint X" — and deliberately never "which endpoint should I use". WHICH endpoint is always decided by the caller from something explicit and reviewable (an env's KCL declaration for forge, --server / RELIANT_SERVER for reliant). A stored "current" entry would make which server a command hits depend on what a previous command did, which is the stateful-context model both CLIs have deleted.
── WHERE IT LIVES ────────────────────────────────────────────────────
Path resolves as:
$FORGE_HOME/credentials.json when FORGE_HOME is set $XDG_CONFIG_HOME/forge/credentials.json when XDG_CONFIG_HOME is set ~/.config/forge/credentials.json otherwise, on every OS
~/.config rather than os.UserConfigDir: on macOS that is ~/Library/ Application Support, a path with a space in it that users cannot type and that no other CLI tool uses. A file two CLIs share — and that a human is told to inspect or delete — belongs where CLI config conventionally lives. FORGE_HOME is honoured first so a test or a sandboxed CI job can redirect the file without touching a developer's real credentials.
DirsFromEnv (env.go) reads those three inputs from the process — the one place this package touches the environment, exempted from internal/pkgguard by name. Every CLI that shares the file calls it rather than reading the variables itself, which is what makes it one file. Dirs stays a plain value for callers (tests, a sandbox) that must place the file explicitly.
The directory is named for forge because forge OWNS the format: this package is its only reader and writer, and reliant imports it rather than re-implementing it. A second implementation of the same file is how two tools end up disagreeing about its shape.
── WHAT IS STORED ────────────────────────────────────────────────────
The token is opaque: nothing here parses it. Scopes, expiry and client id are the issuer's answer at login time, stored for DISPLAY and for the "expired, log in again" hint — never for an authorization decision, which only the server can make.
Index ¶
- Constants
- Variables
- func Normalize(endpoint string) (string, error)
- func PathFromEnv() (string, error)
- func Remove(path, endpoint, client string) (bool, error)
- func RemoveRevoked(path, endpoint, client, revokedToken string) (bool, error)
- func Save(path string, f *File) error
- func Store(path, endpoint, client string, c Credential) error
- func Update(path string, mutate func(*File) error) error
- type Claimable
- type Credential
- type Dirs
- type File
Constants ¶
const ( // EnvForgeHome redirects the file to $FORGE_HOME/credentials.json. It wins // over everything else, so a test or a sandboxed CI job can move the file // without touching a developer's real credentials. EnvForgeHome = "FORGE_HOME" // EnvXDGConfigHome places the file at $XDG_CONFIG_HOME/forge/credentials.json. EnvXDGConfigHome = "XDG_CONFIG_HOME" )
Environment variable names of the contract. Exported so docs and doctor checks refer to one spelling.
const FileName = "credentials.json"
FileName is the credentials file's base name.
Variables ¶
var ErrNotFound = errors.New("credentials: no stored credential for this endpoint")
ErrNotFound reports that the file holds no credential for an endpoint and client.
Functions ¶
func Normalize ¶
Normalize turns an endpoint URL into its store key: lower-cased scheme and host, the port only when it is not the scheme's default, and nothing else — no path, no query, no trailing slash.
Two spellings of one server must land on one entry, or a user who logged in to "https://Admin.example.com/" is told they are not logged in to "https://admin.example.com". The path is dropped because a credential is issued by an ORIGIN; two paths on one origin are one issuer.
func PathFromEnv ¶ added in v1.0.0
PathFromEnv is DirsFromEnv().Path(): where the credentials file lives for this process.
func Remove ¶
Remove is the one-call delete: drop (endpoint, client) from the file at path. Reports whether an entry existed. Every other entry is untouched.
func RemoveRevoked ¶ added in v1.0.0
RemoveRevoked removes only the exact session the caller revoked. A newer login or a different namespace is never erased by an older logout.
func Save ¶
Save writes f to path ATOMICALLY with mode 0600 in a 0700 directory.
Atomic (temp file + rename in the same directory) because two CLIs write this file and a torn write would lose every endpoint's credential, not just the one being updated. The modes matter because every value is a bearer credential: a world-readable file is a leak on any shared machine.
func Store ¶
func Store(path, endpoint, client string, c Credential) error
Store is the one-call write: put c for (endpoint, client) into the file at path and save it, preserving every other entry.
Types ¶
type Claimable ¶ added in v1.0.0
type Claimable struct {
// ClaimURL is the link a human opens to take the deploy over.
ClaimURL string `json:"claim_url"`
// OrgID is the anonymous user's personal org.
OrgID string `json:"org_id,omitempty"`
// URL is the deploy's live address, as last observed. Empty until the
// first deploy has gone live.
URL string `json:"url,omitempty"`
// ExpiresAt is when the deploy goes offline; PurgeAfter when it is
// deleted (and when the token stops being accepted).
ExpiresAt time.Time `json:"expires_at"`
PurgeAfter time.Time `json:"purge_after"`
}
Claimable is the anonymous deploy a claimable-deploy credential belongs to. Everything here is what `forge env status --claimable` reprints, so it is stored rather than re-fetched: the agent that holds it has no account to ask the control plane with.
type Credential ¶
type Credential struct {
// Kind distinguishes human identities from delegated capabilities.
Kind string `json:"kind,omitempty"`
// Token is the bearer credential. Opaque; never logged.
Token string `json:"token"`
// TokenPrefix is the issuer's display prefix (e.g. "rlat_A3f9Kd2p") —
// safe to print, and how a human matches this entry to a token listed
// in the web UI.
TokenPrefix string `json:"token_prefix,omitempty"`
// Scopes the issuer granted, as it reported them.
Scopes []string `json:"scopes,omitempty"`
// ExpiresAt is when the issuer said the token expires; nil when it did
// not say. Informational — the server decides.
ExpiresAt *time.Time `json:"expires_at,omitempty"`
// CreatedAt is when this entry was written.
CreatedAt time.Time `json:"created_at"`
// Issuer is the authorization server that minted it, when that differs
// from the endpoint it is presented to (reliant logs in at the control
// plane and presents the token to its API server). Informational.
Issuer string `json:"issuer,omitempty"`
// AccountID and AccountEmail name the account the login signed in as,
// as the issuer reported them (the token reply's account_id and
// account_email extension fields; AccountID is the issuer's stable
// external id for the person). Empty when the issuer did not say — an
// older issuer, or a token pasted by hand. Informational: they let a
// client show who it is signed in as, and tell two logins of one
// person from logins of two people, without a network call. The server
// decides who the token acts as.
AccountID string `json:"account_id,omitempty"`
AccountEmail string `json:"account_email,omitempty"`
// Claimable is set only on an entry stored under a claimable-deploy client
// id (forge-cli-claimable…): the anonymous deploy this token belongs to.
// It sits beside the token because the claim link is as sensitive as the
// token is (whoever opens it first owns the deploy), and the file is
// already 0600. Nil on every ordinary login.
Claimable *Claimable `json:"claimable,omitempty"`
}
Credential is what one endpoint's login produced.
func Lookup ¶
func Lookup(path, endpoint, client string) (Credential, error)
Lookup is the one-call read: client's credential for endpoint in the file at path, or ErrNotFound.
type Dirs ¶
Dirs are the inputs to the file's location: the values of $FORGE_HOME and $XDG_CONFIG_HOME, and the user's home directory. A caller fills them from its environment; see the package doc for the precedence.
func DirsFromEnv ¶ added in v1.0.0
func DirsFromEnv() Dirs
DirsFromEnv returns the Dirs this process's environment names: $FORGE_HOME, $XDG_CONFIG_HOME and the user's home directory. It is how `forge login` locates the file, so a consumer that calls it shares the file with forge by construction.
An unknown home directory leaves Dirs.Home empty; Path then fails only when neither variable is set either, which is the one case with nowhere to put the file.
type File ¶
type File struct {
Version int `json:"version"`
// Credentials is endpoint → client id → credential.
Credentials map[string]map[string]Credential `json:"credentials"`
}
File is the whole store.
func Load ¶
Load reads the file at path. A missing file is an empty store, not an error: "never logged in" is the ordinary first state.
func (*File) Delete ¶
Delete removes client's credential for endpoint and reports whether there was one. An endpoint left with no clients is dropped.