Documentation
¶
Overview ¶
Package keys manages named API keys: the registry (keys.json) and the lifecycle commands over it (add/bind/use/remove/rename/set-base-url/resolve).
The two files ¶
Key management is split between a registry and vaults, with one rule:
- keys.json (THIS package) is the registry — the single source of truth for which keys exist and everything non-secret about them: type, keystore backend, public key, the bound API key id, the default-key selection, an optional per-key base URL. No secret ever lives here.
- A keystore backend (package keystore) is a vault — a dumb name→secret store. keystore.json is just the FILE backend's vault; when a key uses the keychain backend its secret lives in the OS keyring instead, under the same name. Vaults hold no metadata and cannot enumerate keys.
Everything routes through the registry: to enumerate keys, to find where a key's secret lives, to decide how it signs. Nothing ever scans a vault.
The two per-key axes: `type` and `keystore` ¶
Every record carries two orthogonal fields:
- type — the signing scheme: what kind of secret the vault holds and how requests are signed with it ("ed25519" or "hmac-sha256").
- keystore — which backend holds this key's secret. Per key so that keys with different security needs coexist: an order-placing key in the OS keychain, read-only keys in the file vault. config.json's `keystore` is only the default for NEWLY CREATED keys; changing it never re-points an existing record. Records are re-pointed only by `keystore migrate`, which moves and verifies the material first (Manager.SetKeystoreBackend is its commit hook, not a casual setter).
Both fields are MANDATORY — a record missing either fails to load. Their VALUES are tolerant on load and strict at use: a record written by a newer CLI (unknown type or backend name) still LISTS fine, and only an operation that must interpret it fails — signing an unknown type, or migrating a key out of an unknown backend (which this build can't read; a `keystore migrate --all` skips it and moves the rest, naming it errors). One forward-version key must never brick the registry for the rest.
Invariants ¶
- A key's secret is expected ONLY in the backend its record names (`keystore migrate --keep-source` copies are explicitly non-authoritative). Resolve reads exactly there — no fallback probing of other backends.
- Keys may belong to different Korbit accounts, so nothing here ever falls back from one key to another: the default is set only explicitly (or on first creation), and removing the default clears it rather than silently re-pointing at another account. This holds for forced removal too (see below) — a stranded default is unset, never re-pointed.
- Remove deletes the secret then the record on the happy path; a key whose backend is unknown to this build or unavailable (or whose Delete errors) can't have its secret deleted, so a plain remove fails loudly and points at `key remove --force`. Force removes the record anyway (best-effort secret delete) so a forward-version or headless-stranded key can never squat in the registry with no escape hatch; it reports the orphaned secret and the backend it was left in (RemoveResult.SecretRemoved / Warning) rather than pretending the secret is gone.
- keys.json is written atomically (temp file + rename): it is the commit point of a keystore migration, so a crash must not truncate it.
- Concurrency contract: every MUTATING method (Add, Bind, Use, SetBaseURL, ClearBaseURL, SetKeystoreBackend, Remove, Rename) holds an exclusive cross-process advisory lock (keys.json.lock, via internal/fslock) across its whole load → modify → atomic-rename cycle, so two concurrent korbit-cli processes (a long-running monitor bot plus ad-hoc commands) can't last-writer-wins each other's update. READ-ONLY methods (List, Show, Names, Resolve, DefaultKeyName, BaseURLOf, MetaBaseURL) are deliberately LOCK-FREE: the atomic rename guarantees they observe a complete, self-consistent snapshot.
- Read cache: the read-only metadata peeks (List, Names, DefaultKeyName and the Meta* / MetaForSelection accessors) parse keys.json once per Manager and reuse it, so a single command — or a long-running mcp/tui/monitor session holding one Manager — doesn't re-read the file on every peek. Mutations bypass the cache (they load fresh under the registry lock) and invalidate it on save; Invalidate() drops it after an out-of-band in-process change (the mcp setup tool calls it). The trade-off: within one process a peek may observe an earlier — still complete and self-consistent — snapshot than a concurrent OTHER process's write, until the next invalidation. That is fine for these best-effort metadata peeks, and a long-running session resolves its signer once anyway.
- Lock ordering — registry → vault, NEVER the reverse. The mutating methods that drive the file vault (Add, Remove, Rename) hold the registry lock (keys.json.lock) WHILE calling keystore.FileKeystore.Set/Delete, which take the SEPARATE vault lock (keystore.json.lock). That nesting is deadlock-free only because the two are different lock files (flock and LockFileEx contend even between distinct fds within one process, so a single shared lock would self-deadlock). Nothing in the vault layer may ever acquire this registry lock.
- keystore.Migrate is NOT whole-run atomic: it is a sequence of individually-locked steps (each dst.Set takes the vault lock, each commit via SetKeystoreBackend takes the registry lock, both briefly). What is preserved is per-key consistency — the copy → verify → commit → delete contract — not atomicity across the whole run.
- Resolved exposes NO raw key material: Resolve parses the stored PEM into a signer eagerly and keeps it behind the unexported Resolved.signer field; the only way to obtain it is Resolved.Signer(), the single seam between stored material and a usable signer. A PEM that fails to parse is reported as a config problem (ConfigError, exit 4) by Resolve, not deferred to a caller's UsageError. The two-value Signer() signature lets the per-`type` switch resolve stored material into a signer — failing cleanly on a type it can't build — so no consumer ever holds the secret. Resolved is also fmt-safe: it implements fmt.Formatter to redact the signer under every verb, because unexporting alone does not stop fmt from dumping unexported fields (and an ed25519 key) via reflection under %+v/%#v.
Index ¶
- Constants
- func AssertSandboxKeyName(name string) error
- func InlineCredsPresent(getenv func(string) string) bool
- func RegistryPath(home string) string
- func ValidateBaseURL(raw string) error
- func ValidateWSBaseURL(raw string) error
- type KeyMeta
- type Manager
- func (m *Manager) Add(name, privatePEM, backend string) (NewKey, error)
- func (m *Manager) AddBound(name, privatePEM, apiKeyID, backend string) (NewKey, error)
- func (m *Manager) AddHMAC(name, secret, apiKeyID, backend string) (NewKey, error)
- func (m *Manager) BaseURLOf(name string) (string, error)
- func (m *Manager) Bind(name, apiKeyID string) error
- func (m *Manager) ClearBaseURL(name string) error
- func (m *Manager) DefaultBackend() string
- func (m *Manager) DefaultKeyName() (string, error)
- func (m *Manager) Invalidate()
- func (m *Manager) List() ([]Summary, error)
- func (m *Manager) MetaBaseURL(explicit string) string
- func (m *Manager) MetaDefaultAccountSeq(explicit string) string
- func (m *Manager) MetaForSelection(sel Selection) KeyMeta
- func (m *Manager) MetaWSBaseURL(explicit string) string
- func (m *Manager) Names() ([]string, error)
- func (m *Manager) Remove(name string, force bool) (RemoveResult, error)
- func (m *Manager) Rename(oldName, newName string) (RenameResult, error)
- func (m *Manager) Resolve(explicit string) (Resolved, error)
- func (m *Manager) ResolveSelection(sel Selection, getenv func(string) string) (Resolved, error)
- func (m *Manager) SetBaseURL(name, raw, wsRaw string) error
- func (m *Manager) SetDefaultAccountSeq(name string, accountSeq int) error
- func (m *Manager) SetKeystoreBackend(name, backend string) error
- func (m *Manager) Show(name string) (SummaryWithPublic, error)
- func (m *Manager) SignerFor(name string) (korbit.Signer, error)
- func (m *Manager) Use(name string) error
- type NewKey
- type OpenFunc
- type Record
- type RemoveResult
- type RenameResult
- type Resolved
- type Selection
- type Summary
- type SummaryWithPublic
Constants ¶
const ( TypeEd25519 = "ed25519" TypeHMACSHA256 = "hmac-sha256" )
TypeEd25519 and TypeHMACSHA256 are the signing schemes this build can sign with. The registry still loads records of OTHER types (so one future-typed key can't brick the file for the rest); those are rejected at use time instead.
- ed25519 — the user generates the keypair and registers the public key; the stored secret is a PKCS#8 PEM, and the signature is base64.
- hmac-sha256 — Korbit issues the api-key id AND a shared secret together; the stored secret is that opaque string, and the signature is hex. It has no public key and is not generated locally.
const ( // EnvKeyName selects a STORED key by its local name — the same selection the // --key flag makes. It is mutually exclusive with the inline-material vars. EnvKeyName = "KORBIT_CLI_KEY" // EnvAPIKeyID, EnvAPIKeySecret and EnvAPIKeyType provide a credential INLINE, // with no keystore entry: the portal-issued api-key id, the secret material // (an ED25519 PKCS#8 PEM, or the HMAC-SHA256 shared secret), and the signing // scheme. All three are required together, and the whole group is mutually // exclusive with a stored-key selection (--key / KORBIT_CLI_KEY). EnvAPIKeyID = "KORBIT_CLI_API_KEY_ID" EnvAPIKeySecret = "KORBIT_CLI_API_KEY_SECRET" EnvAPIKeyType = "KORBIT_CLI_API_KEY_TYPE" )
Environment variables that choose the signing credential for an invocation.
const InlineDisplayName = "(environment)"
InlineDisplayName is the Resolved.Name reported for an inline credential (it has no stored local name). It is what the "signing as key …" disclosure prints.
It is DISPLAY-ONLY: never compare a name against it to detect inline mode. The supported signal is Selection.Inline / Resolved.Inline.
const SandboxAPIKeyPrefix = "SANDBOX_"
SandboxAPIKeyPrefix is the prefix the local API sandbox gives every api-key id it issues (e.g. SANDBOX_ED25519_KEY_…). A real Korbit api-key id never matches it, so a key's bound id is the single, schema-free source of truth for "is this a sandbox key" — see IsSandbox. It is deliberately NOT the loopback base URL: a user may legitimately point a real key at a local proxy, so the endpoint says nothing about a key's origin; the api-key id does.
const SandboxNameToken = "sandbox"
SandboxNameToken is the substring every sandbox key's local name must carry. Sandbox keys are explicit-only (never the default — see IsSandbox), so a name that advertises the sandbox keeps every sandbox command self-evident on the command line: you select one with `--key <name>`, and the word "sandbox" is right there in the invocation. A real key is never forced to carry it.
Variables ¶
This section is empty.
Functions ¶
func AssertSandboxKeyName ¶
AssertSandboxKeyName checks that name is acceptable for a sandbox key — it must carry SandboxNameToken. The sandbox lifecycle calls this before it spawns a server, so a bad --key-name fails fast instead of after a successful start; the bind seams (AddBound/Bind/Rename) enforce the same rule as the persisted invariant, so a non-conforming sandbox key can never reach keys.json.
func InlineCredsPresent ¶
InlineCredsPresent reports whether any inline-credential env var (KORBIT_CLI_API_KEY_*) is set — i.e. the invocation is in inline-credential mode rather than using a stored key. Commands that manage stored keys (`setup`) use it to refuse rather than act on a keystore that is being bypassed.
func RegistryPath ¶
NewManager builds a key manager over the real keystore backends. RegistryPath is the key registry path under home (keys.json).
func ValidateBaseURL ¶
ValidateBaseURL reports whether raw is a well-formed REST base URL, for callers that pre-check --base-url before creating a key (so an invalid value fails before any key is written, not after).
func ValidateWSBaseURL ¶
ValidateWSBaseURL reports whether raw is a well-formed WebSocket base URL.
Types ¶
type KeyMeta ¶
type KeyMeta struct {
BaseURL string // per-key REST endpoint override; "" = none
WSBaseURL string // per-key WebSocket endpoint override; "" = derive
DefaultAccountSeq string // per-key sub-account default as a decimal string; "" = not configured
}
KeyMeta is the non-secret, per-key metadata peeked before signing: the endpoint overrides and the default sub-account. The zero value means nothing is configured — also what an inline credential (no stored metadata) yields.
type Manager ¶
type Manager struct {
// Log is an optional operational logger (nil = silent) for the registry's
// load/lock/save steps and lifecycle milestones (key created, default set,
// key removed/renamed/migrated). It logs the resolved key name + backend +
// api-key-id (all non-secret) — NEVER the secret. Set by NewManager from its
// log argument; NewManagerWithBackends leaves it nil unless a caller sets it.
Log *slog.Logger
// contains filtered or unexported fields
}
Manager owns keys.json and resolves each key's keystore backend through it.
func NewManager ¶
defaultBackend (config.json `keystore`) applies only to keys created from now on — existing keys carry their own backend in keys.json. now is injectable for tests. log (nil = silent) is threaded into the constructed keystore backends (so their per-key Set/Get/Delete and the file vault's lock/read/write are traceable) AND stored on the Manager for its own registry diagnostics.
func NewManagerWithBackends ¶
func NewManagerWithBackends(home, defaultBackend string, open OpenFunc, probe func(string) error, now func() int64) *Manager
NewManagerWithBackends is NewManager with the backend constructor and the availability probe injectable, so tests can run against mock keystores (platform keyrings aren't usable in unit tests).
func (*Manager) Add ¶
Add creates a new named key. If privatePEM is non-empty it imports that key; otherwise it generates a fresh keypair. backend selects the keystore for the new key's private material; "" uses the default (config.json `keystore`). The first key created becomes the default key automatically.
func (*Manager) AddBound ¶
AddBound creates a new named key that is already bound to apiKeyID, importing privatePEM (required — there is no fresh-keypair path here, since a bound key must match an already-issued api-key id). It exists so a key is born with its binding in place, which makes the default rule decidable at creation: the default becomes the first *non-sandbox* key, so a sandbox key (whose id carries SandboxAPIKeyPrefix) is NEVER even transiently the default.
This is why the sandbox import must NOT use Add+Bind: Add defaults the first key before Bind attaches the id, so on a fresh install the sandbox key would briefly BE the default — defeating the "sandbox use is explicit-only" safety property. AddBound closes that hole by attaching the id atomically.
func (*Manager) AddHMAC ¶
AddHMAC creates a new named HMAC-SHA256 key, already bound to apiKeyID. An HMAC key has no keypair and no public key: Korbit issues the api-key id and the shared secret together, so the key is born bound (there is nothing to register) — the same reason AddBound exists for an imported ED25519 key. secret is the opaque shared secret; it is stored verbatim in the chosen backend. backend "" uses the default for new keys.
Like AddBound it never becomes the default if it is a sandbox key, and a sandbox api-key id requires a sandbox-advertising name.
func (*Manager) BaseURLOf ¶
BaseURLOf returns the endpoint override stored for an exact key name ("" when none), erroring only if the key is unknown or keys.json is unreadable.
func (*Manager) ClearBaseURL ¶
ClearBaseURL removes a key's endpoint overrides (both REST and WebSocket), reverting it to the default.
func (*Manager) DefaultBackend ¶
DefaultBackend reports the keystore backend new keys go to.
func (*Manager) DefaultKeyName ¶
DefaultKeyName returns the default key name, or "" if none.
func (*Manager) Invalidate ¶
func (m *Manager) Invalidate()
Invalidate drops the cached keys.json so the next read-only peek re-parses disk. save calls it after every successful write; a long-running server that changes keys through a separate path (e.g. the mcp setup tool) calls it so subsequent peeks see the change without a restart.
func (*Manager) MetaBaseURL ¶
MetaBaseURL is a best-effort, metadata-only lookup of the endpoint override for the key that would be used (the explicit name, else the default). It never touches the keystore and never errors: an unknown/missing key or unreadable file yields "", so the caller's later Resolve produces the canonical error.
func (*Manager) MetaDefaultAccountSeq ¶
MetaDefaultAccountSeq is a best-effort, metadata-only lookup of the per-key default sub-account sequence for the key that would be used (the explicit name, else the default). Returns "" when not configured; the caller passes the value into accountseq.Inputs.Default.
func (*Manager) MetaForSelection ¶
MetaForSelection returns the per-key metadata for a key selection in one peek, honoring the inline rule: an inline (KORBIT_CLI_API_KEY_*) credential has no stored metadata, so it yields the zero KeyMeta and the per-key base-URL / defaultAccountSeq tiers are skipped. It is the single front door for the command paths (endpoint/tui/monitor) that need several of these at once.
func (*Manager) MetaWSBaseURL ¶
MetaWSBaseURL is the WebSocket counterpart of MetaBaseURL: "" when there is no stored override (the caller then derives it from the REST base URL).
func (*Manager) Names ¶
Names returns every key name, sorted. It is metadata-only (reads keys.json, never a keystore).
func (*Manager) Remove ¶
func (m *Manager) Remove(name string, force bool) (RemoveResult, error)
Remove deletes a key's record and, normally, its private key from the key's own backend. If the removed key was the default, the default becomes unset (no silent fallback — the default is never re-pointed at another, possibly different-account, key).
The secret is deleted BEFORE the record is dropped only on the happy path; the ordering matters because the record is the single source of truth for where a key's secret lives, so the record must outlive any failed delete that a user might want to retry. That makes a stranded key — one whose record names a backend this build doesn't know (written by a newer CLI), or whose backend is unavailable (the record points at the OS keychain on a headless host), or whose Delete errors — impossible to remove without an escape hatch: the record squats in `key list` forever and its name stays reserved.
force is that escape hatch. Without it, an undeletable secret is a hard error that points the user at --force (the no-fallback invariant is never the blocker — only the secret is). With it, secret deletion is best-effort:
- backend opens and Delete succeeds → full removal, SecretRemoved true.
- backend unknown/unavailable, or Delete errors → the record is removed (and the default unset if it was this key) anyway, SecretRemoved false, and Warning names the backend the orphaned secret was left in.
--force on a healthy key behaves exactly like a normal remove (secret deleted, SecretRemoved true, no warning).
func (*Manager) Rename ¶
func (m *Manager) Rename(oldName, newName string) (RenameResult, error)
Rename relabels a key: the registry record — its API-key binding, public key, keystore backend, per-key base URL, and creation time — is preserved under newName, and the private material is moved WITHIN the key's own backend. The keypair and the portal binding are unchanged, so no re-registration is needed (unlike remove + re-add, which destroys the keypair).
The vault move follows the keystore.Migrate safety ordering — copy into the new name → verify the read-back → commit the registry rename (atomic keys.json write) → only then delete the old vault entry. A failure before the commit rolls back the in-flight copy and leaves the key untouched under its old name; a failure of the post-commit delete leaves an orphaned secret under the old name (reported via Warning) but the rename itself has succeeded.
A key whose backend can't be opened or read here (unknown to this build, or an unavailable OS keychain) cannot be renamed — moving its secret would orphan it under the old name — so this fails loudly, the same stance as a non-forced Remove. There is deliberately no --force: use `key remove --force` for a stranded key.
func (*Manager) Resolve ¶
Resolve returns the credential to sign with: the explicit name if given, else the default. It fails loudly (never falls back) when anything is missing.
func (*Manager) ResolveSelection ¶
ResolveSelection resolves a Selection to a signing credential. The stored path defers to Resolve (keystore + registry); the inline path builds the credential straight from the KORBIT_CLI_API_KEY_* vars without any keystore access.
func (*Manager) SetBaseURL ¶
SetBaseURL pins a key to a specific API endpoint (validated http(s) URL) and its companion WebSocket endpoint (validated ws(s) URL). An empty wsRaw leaves the key with no stored WebSocket override, so it is derived from the REST base URL at use time. The caller (the set-base-url command) supplies wsRaw either from an explicit override or by deriving it from raw.
func (*Manager) SetDefaultAccountSeq ¶
SetDefaultAccountSeq stores a per-key default sub-account sequence number. accountSeq must be >= 1; pass 0 to clear the override (revert to main account).
func (*Manager) SetKeystoreBackend ¶
SetKeystoreBackend re-points a key's record at another keystore backend. It is the COMMIT step of `keystore migrate` — the engine calls it only after the key's material is verified in the target — and must not be used as a casual setter: flipping the record without moving the material orphans the key.
func (*Manager) Show ¶
func (m *Manager) Show(name string) (SummaryWithPublic, error)
Show returns one key's summary plus its public key.
func (*Manager) SignerFor ¶
SignerFor builds the signer for a key from its stored private material, WITHOUT requiring an API key id binding. It backs the pre-bind check interactive setup runs: signing a candidate id's whoami before persisting the binding. Resolve is the bound-key path used for real calls; this is signing material only.
type OpenFunc ¶
OpenFunc builds the keystore for a backend name (keystore.ByName in production; tests inject mock backends).
type Record ¶
type Record struct {
// Type is the signing scheme — what kind of secret the keystore holds and
// how requests are signed with it ("ed25519" or "hmac-sha256"). Unknown
// values load fine and fail at use time.
Type string `json:"type"`
// Keystore names the backend holding this key's private material ("file" |
// "keychain"). Per key on purpose: keys with different security needs live
// in different backends. Changed only by `keystore migrate`.
Keystore string `json:"keystore"`
APIKeyID *string `json:"apiKeyId"` // portal-issued X-KAPI-KEY id; null until bound
PublicKey string `json:"publicKey"`
CreatedAt int64 `json:"createdAt"`
// BaseURL optionally pins this key to a specific API endpoint, for the rare
// case where a key's account is served from an alternate Korbit API host
// rather than the default. Empty means "use the resolved default". It is
// non-secret metadata, so it lives here rather than in the keystore.
BaseURL string `json:"baseUrl,omitempty"`
// WSBaseURL optionally pins this key to a specific WebSocket endpoint
// (scheme ws/wss), the companion of BaseURL for the streaming `monitor`
// command. Empty means "derive it from BaseURL". Non-secret metadata.
WSBaseURL string `json:"wsBaseUrl,omitempty"`
// DefaultAccountSeq optionally sets the sub-account sequence number that
// accountseq resolution falls back to when the user omits --account-seq.
// Nil means "not configured" (fall through to main account 1).
DefaultAccountSeq *int `json:"defaultAccountSeq,omitempty"`
}
Record is the persisted metadata for one key. Type and Keystore are mandatory — a record without them fails to load (see doc.go for the two-axis model they encode).
func (Record) IsSandbox ¶
IsSandbox reports whether a record is a sandbox key: its bound api-key id carries SandboxAPIKeyPrefix. An unbound key is never a sandbox key (there is no id to judge). This one predicate is the ONLY place the distinction is decided — every "treat sandbox keys specially" rule (never the default, `key use` refuses them, setup/doctor skip them, `key list` tags them) keys off it, so there is no scattered special-casing and no key "class" field.
type RemoveResult ¶
type RemoveResult struct {
Removed string `json:"removed"`
DefaultKey *string `json:"defaultKey"`
// SecretRemoved reports whether the private key material was actually deleted
// from its backend. It is false only on a forced removal that had to leave the
// secret behind (unknown/unavailable backend, or a Delete that errored); a
// normal removal always deletes the secret or fails. The field is additive —
// a value of false with no Warning never occurs.
SecretRemoved bool `json:"secretRemoved"`
// Warning, when set, explains that the record was removed but its secret was
// left in its backend (only on a forced removal). Empty on a clean removal.
Warning string `json:"warning,omitempty"`
}
RemoveResult reports what happened to the default after a removal.
type RenameResult ¶
type RenameResult struct {
OldName string `json:"oldName"`
NewName string `json:"newName"`
// IsDefault is true when the renamed key was the default and so the default
// now follows it to the new name. This is the SAME key/account, so it is not
// the forbidden silent re-point to a different account.
IsDefault bool `json:"isDefault"`
// SecretMoved reports whether private material was actually moved in the vault.
// It is false only for an already-secretless record (renamed metadata-only).
SecretMoved bool `json:"secretMoved"`
// Warning, when set, reports that the rename committed but the OLD vault entry
// could not be deleted afterwards (the secret was left under the old name).
Warning string `json:"warning,omitempty"`
}
RenameResult reports the outcome of a rename.
type Resolved ¶
type Resolved struct {
Name string
Type string
Keystore string
APIKeyID string
// Inline is true when the credential came from inline environment material
// (KORBIT_CLI_API_KEY_*) rather than a stored key, mirroring Selection.Inline.
// It is the supported signal for "is this an inline credential?" — callers
// MUST branch on it, never compare Name against InlineDisplayName (a display
// string, not a sentinel).
Inline bool
// contains filtered or unexported fields
}
Resolved is the credential needed to sign a request. It carries no exported secret: the stored material is parsed into a signer eagerly (in Resolve or resolveInline) and kept behind the unexported signer field, reachable only through Signer(). Unexporting alone does NOT make the struct safe to print — fmt reflects unexported fields — so Resolved also implements fmt.Formatter (see Format) to redact every verb. The signer itself (korbit.Signer) also self-redacts, so the struct is safe even if Format is ever bypassed.
func (Resolved) Format ¶
Format makes Resolved safe to print. Because fmt reaches unexported fields by reflection, neither unexporting `signer` nor a plain String() stops a %+v or %#v from dumping the raw key bytes — only a Formatter intercepts EVERY verb. We render the non-secret fields and a redaction placeholder for the signer, matching the struct/Go-syntax shapes fmt would otherwise produce so the output still reads naturally. This is the leak-hardening seam for the credential.
func (Resolved) Signer ¶
Signer returns the signer to hand to the signing layer (korbit). It is the single seam between stored key material and a usable signer: the per-`type` switch lives in signerFor, which Resolve and resolveInline call to build this value. New code MUST obtain its signer through this method and never hold raw key material.
type Selection ¶
type Selection struct {
// Inline is true when KORBIT_CLI_API_KEY_* supplies the credential directly;
// no keystore is consulted and there is no per-key stored metadata (so the
// per-key base-URL and defaultAccountSeq tiers do not apply to it).
Inline bool
// Name is the stored-key name to resolve when !Inline ("" => the default key).
Name string
}
Selection is how a credential was chosen for one invocation: either inline material from the environment, or a stored key by name (Name "" => the default key). The two are mutually exclusive by construction — Select enforces it — so ResolveSelection has a single, unambiguous source.
func Select ¶
Select determines the credential plan from the --key flag value and the environment, enforcing that inline env material (KORBIT_CLI_API_KEY_*) and a stored-key selection (--key / KORBIT_CLI_KEY) are MUTUALLY EXCLUSIVE. It reads no secret and never touches the keystore, so it is safe to call early — before metadata-only per-key peeks such as base URL and defaultAccountSeq — and fails fast on a contradictory invocation. It is the single front door every signing command uses, so key selection behaves identically across the program.
type Summary ¶
type Summary struct {
Name string `json:"name"`
Type string `json:"type"`
Keystore string `json:"keystore"`
APIKeyID *string `json:"apiKeyId"`
Bound bool `json:"bound"`
IsDefault bool `json:"isDefault"`
// IsSandbox is true when this key's bound api-key id carries the sandbox
// prefix (Record.IsSandbox) — i.e. it signs against the local API sandbox,
// never production. `key list` surfaces it as a [sandbox] tag.
IsSandbox bool `json:"isSandbox"`
CreatedAt int64 `json:"createdAt"`
BaseURL string `json:"baseUrl,omitempty"` // per-key API endpoint override; empty = default
WSBaseURL string `json:"wsBaseUrl,omitempty"` // per-key WebSocket endpoint override; empty = derived from baseUrl
DefaultAccountSeq *int `json:"defaultAccountSeq,omitempty"` // per-key sub-account default; nil = not configured
}
Summary is the public view of a key (no private material).
type SummaryWithPublic ¶
SummaryWithPublic adds the public key for `key show`.