Documentation
¶
Overview ¶
Package cli provides the IPC bridge to `aikey _internal *` and the shared HTTP error model used by every handler that talks to the cli.
Why a separate package: vault, vault-CRUD, and import all spawn the cli with the same envelope shape and report errors with the same I_* code surface. Centralising them here keeps the contract single-sourced; each consumer imports cli instead of redeclaring envelopes / error codes.
Index ¶
Constants ¶
const ( // Protocol + spawn layer ErrCliNotFound = "I_CLI_NOT_FOUND" // aikey binary missing in PATH and ~/.aikey/bin ErrCliSpawnFailed = "I_CLI_SPAWN_FAILED" // os/exec returned an error before cli ran ErrCliTimeout = "I_CLI_TIMEOUT" // ctx deadline reached while waiting for cli stdout ErrCliMalformedReply = "I_CLI_MALFORMED_REPLY" // stdout JSON unparseable or missing required fields // Vault session layer ErrVaultLocked = "I_VAULT_LOCKED" // request targets an unlock-required route while session is absent ErrVaultUnlockFailed = "I_VAULT_UNLOCK_FAILED" // password did not produce a verifying key (wraps cli I_VAULT_KEY_INVALID) ErrVaultNoSession = "I_VAULT_NO_SESSION" // session id cookie missing or expired // Request-level ErrBadRequest = "I_BAD_REQUEST" // malformed JSON body or missing required field // User Vault CRUD layer (Web page /user/vault — 2026-04-23 decision set) ErrOAuthAddViaCLI = "I_OAUTH_ADD_VIA_CLI" // POST /vault/entry called with target=oauth (OAuth add flow lives in CLI) ErrUnknownTarget = "I_UNKNOWN_TARGET" // target is not personal|oauth|team, or team is not yet implemented ErrAliasSuffixExhausted = "I_ALIAS_SUFFIX_EXHAUSTED" // auto -2/-3 retry ran 20× and still conflicted (extreme edge case) ErrUnlockRateLimited = "I_UNLOCK_RATE_LIMITED" // too many unlock attempts from one source; online brute-force defense )
I_* error codes surfaced to the Web UI. Mirror the set emitted by the Rust cli where applicable (aikey-cli/src/error_codes.rs); additional codes here cover Go-side orchestration failures (cli not found / spawn timeout / session missing) that have no cli analogue.
const PlaceholderHex = "0000000000000000000000000000000000000000000000000000000000000000"
PlaceholderHex is the 64-char all-zero vault_key_hex used for cli actions that perform only format validation (parse, metadata). See aikey-cli/src/commands_internal/parse.rs docblock: "only checks format, does not verify against vault".
Variables ¶
This section is empty.
Functions ¶
func WriteCliError ¶
func WriteCliError(w http.ResponseWriter, result *Result)
WriteCliError writes a 4xx/5xx response mirroring the cli Result error branch. It maps a small set of known cli codes to HTTP status and falls back to 500 for the rest.
func WriteEnvelope ¶
func WriteEnvelope(w http.ResponseWriter, r *Result)
WriteEnvelope relays a cli Result verbatim to the HTTP client. ok branch → 200 + {status, data}; error branch → status-mapped + {status, error_code, error_message}. Preserves request_id if the cli echoed one.
func WriteErr ¶
func WriteErr(w http.ResponseWriter, code, msg string)
WriteErr writes a JSON error response with a status chosen from the code.
Status-code mapping (kept deliberately narrow so the Web UI and CLI can both reason about it):
- 401 Unauthorized — missing session cookie; top-level auth problem.
- 422 Unprocessable — vault-specific business errors (wrong master password, cli says I_VAULT_KEY_INVALID). NOT 401 because the global httpClient interceptor treats 401 as "JWT expired" and redirects to /login, which would be wrong UX for a vault password typo. Self-review 2026-04-22 caught this.
- 400 Bad Request — malformed JSON / schema violation.
- 503 Service Unavail. — cli binary missing (production container).
- 504 Gateway Timeout — cli spawn timed out.
- 500 Internal — everything else.
func WriteInvokeError ¶
func WriteInvokeError(w http.ResponseWriter, err error)
WriteInvokeError maps a Bridge.Invoke error to an HTTP response with the correct status code. Uses the InvokeError.Code when available and falls back to ErrCliSpawnFailed (500) for anything else.
Types ¶
type Bridge ¶
type Bridge struct {
// BinaryPath is the resolved aikey executable. Empty => look up lazily.
BinaryPath string
// Timeout applied to each invocation unless the caller's ctx deadline is sooner.
Timeout time.Duration
Logger *slog.Logger
}
Bridge spawns `aikey _internal <subcommand>` with a stdin envelope and parses the single-line stdout Result. One spawn per call — the cli is stateless and re-exec cost is acceptable (measured ~30ms on macOS, per Stage 0.3 subprocess latency baseline).
func New ¶
New builds a bridge with a default 15s timeout. The binary is resolved lazily on first call so local-server can boot even if the cli isn't installed yet (the page still renders; only the action endpoints fail).
func (*Bridge) Invoke ¶
func (b *Bridge) Invoke( ctx context.Context, subcommand string, action string, vaultKeyHex string, requestID string, payload any, ) (*Result, error)
Invoke spawns one `aikey _internal <subcommand>` and returns the parsed Result. A non-nil error is returned when the envelope's Status is not "ok" wrt spawn / parse failure (the caller is expected to surface ErrorCode / ErrorMessage to the browser via WriteCliError). Note: a well-formed cli error reply (status="error" with codes) returns a Result with no error here — caller decides what to do.
subcommand is the top-level `_internal` subcommand name ("parse", "vault-op", "query", "update-alias"). action is the sub-action inside vault-op (e.g. "verify", "metadata", "batch_import"); for subcommands that don't use the action field (parse), pass "".
func (*Bridge) InvokeInit ¶
func (b *Bridge) InvokeInit( ctx context.Context, password string, requestID string, ) (*Result, error)
InvokeInit spawns `aikey _internal init --stdin-json` with `{password, request_id}` and returns the parsed Result. Used by the web-driven first-run flow (POST /api/user/vault/init) per 20260430-个人vault-Web首次设置-方案A.md.
Why a separate method rather than reusing Invoke: init.rs reads its own envelope shape (no vault_key_hex; vault doesn't exist yet) so the standard envelope wrapper would just add fields the cli ignores.
type InvokeError ¶
InvokeError carries both an I_* code and a human message so handlers can map to the correct HTTP status. Wrapping via fmt.Errorf with %s prefix worked for display but hid the code from WriteErr's status table, which made every spawn-level failure surface as 500 regardless of its actual cause (the bug this type fixes — 2026-04-22 self-review).
func (*InvokeError) Error ¶
func (e *InvokeError) Error() string
type JSONError ¶
type JSONError struct {
Status string `json:"status"` // always "error"
ErrorCode string `json:"error_code"`
ErrorMessage string `json:"error_message"`
}
JSONError is the HTTP-side error envelope. Shape intentionally matches the cli's ResultEnvelope error branch ({status, error_code, error_message}) so the Web UI has one parser for both layers.
type Result ¶
type Result struct {
RequestID string `json:"request_id,omitempty"`
Status string `json:"status"`
Data json.RawMessage `json:"data,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
Result mirrors ResultEnvelope on the cli side. Exported so handlers in vault / intake packages can pattern-match on Status / ErrorCode.