keys

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

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

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

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

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

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

func AssertSandboxKeyName(name string) error

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

func InlineCredsPresent(getenv func(string) string) bool

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

func RegistryPath(home string) string

NewManager builds a key manager over the real keystore backends. RegistryPath is the key registry path under home (keys.json).

func ValidateBaseURL

func ValidateBaseURL(raw string) error

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

func ValidateWSBaseURL(raw string) error

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

func NewManager(home, defaultBackend string, now func() int64, log *slog.Logger) *Manager

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

func (m *Manager) Add(name, privatePEM, backend string) (NewKey, error)

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

func (m *Manager) AddBound(name, privatePEM, apiKeyID, backend string) (NewKey, error)

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

func (m *Manager) AddHMAC(name, secret, apiKeyID, backend string) (NewKey, error)

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

func (m *Manager) BaseURLOf(name string) (string, error)

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

func (m *Manager) Bind(name, apiKeyID string) error

Bind attaches the portal-issued API key id to a named key.

func (*Manager) ClearBaseURL

func (m *Manager) ClearBaseURL(name string) error

ClearBaseURL removes a key's endpoint overrides (both REST and WebSocket), reverting it to the default.

func (*Manager) DefaultBackend

func (m *Manager) DefaultBackend() string

DefaultBackend reports the keystore backend new keys go to.

func (*Manager) DefaultKeyName

func (m *Manager) DefaultKeyName() (string, error)

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

func (m *Manager) List() ([]Summary, error)

List returns all keys, sorted by name.

func (*Manager) MetaBaseURL

func (m *Manager) MetaBaseURL(explicit string) string

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

func (m *Manager) MetaDefaultAccountSeq(explicit string) string

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

func (m *Manager) MetaForSelection(sel Selection) KeyMeta

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

func (m *Manager) MetaWSBaseURL(explicit string) string

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

func (m *Manager) Names() ([]string, error)

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

func (m *Manager) Resolve(explicit string) (Resolved, error)

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

func (m *Manager) ResolveSelection(sel Selection, getenv func(string) string) (Resolved, error)

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

func (m *Manager) SetBaseURL(name, raw, wsRaw string) error

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

func (m *Manager) SetDefaultAccountSeq(name string, accountSeq int) error

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

func (m *Manager) SetKeystoreBackend(name, backend string) error

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

func (m *Manager) SignerFor(name string) (korbit.Signer, error)

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.

func (*Manager) Use

func (m *Manager) Use(name string) error

Use sets the default key.

type NewKey

type NewKey struct {
	Name      string
	Type      string
	Keystore  string
	PublicKey string
	IsDefault bool
}

NewKey is the result of add/setup.

type OpenFunc

type OpenFunc func(backend string) (keystore.Keystore, error)

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

func (r Record) IsSandbox() bool

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

func (r Resolved) Format(f fmt.State, verb rune)

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

func (r Resolved) Signer() (korbit.Signer, error)

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

func Select(flagKey string, getenv func(string) string) (Selection, error)

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

type SummaryWithPublic struct {
	Summary
	PublicKey string `json:"publicKey"`
}

SummaryWithPublic adds the public key for `key show`.

Jump to

Keyboard shortcuts

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