Documentation
¶
Overview ¶
Package importpkg implements the Go local-server HTTP surface for the bulk-import flow. It is a thin orchestration shell: every vault-touching operation (parse / verify / batch_import / query) is delegated to the Rust aikey CLI via stdin-JSON IPC (see aikey-cli/src/commands_internal). Go never reads or writes vault.db and never performs AES-GCM; only Argon2id key derivation runs on the Go side because the derived `vault_key_hex` must outlive the single unlock HTTP request and be reused by subsequent actions within the session.
Package name note: Go keyword `import` cannot be used for a package name, hence `importpkg` (documented decision in roadmap20260320/技术实现/阶段3-增强版KEY管理/批量导入-实施计划.md §Stage 4).
Index ¶
- Constants
- type CliBridge
- type Config
- type Handlers
- type ImportHandlers
- type InvokeError
- type SessionStore
- type VKCache
- type VaultCRUDHandlers
- func (h *VaultCRUDHandlers) AliasPatchHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultCRUDHandlers) EntryAddHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultCRUDHandlers) EntryDeleteHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultCRUDHandlers) ListHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultCRUDHandlers) UseHandler(w http.ResponseWriter, r *http.Request)
- type VaultHandlers
- func (h *VaultHandlers) LockHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultHandlers) RequireUnlock(next http.HandlerFunc) http.HandlerFunc
- func (h *VaultHandlers) StatusHandler(w http.ResponseWriter, r *http.Request)
- func (h *VaultHandlers) UnlockHandler(w http.ResponseWriter, r *http.Request)
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.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CliBridge ¶
type CliBridge 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
}
CliBridge spawns `aikey _internal <subcommand>` with a stdin envelope and parses the single-line stdout ResultEnvelope. 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 NewCliBridge ¶
NewCliBridge 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 (*CliBridge) Invoke ¶
func (b *CliBridge) Invoke( ctx context.Context, subcommand string, action string, vaultKeyHex string, requestID string, payload any, ) (*resultEnvelope, error)
Invoke spawns one `aikey _internal <subcommand>` and returns the parsed ResultEnvelope. A non-nil error is returned when the envelope's Status is not "ok", or when spawn / parse fails before the cli produced a valid reply; the caller is expected to surface ErrorCode / ErrorMessage to the browser via writeCliError.
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 "".
type Config ¶
type Config struct {
// SessionTTL is the idle timeout for an unlocked vault.
// v4.1 Stage 13: 默认 15 分钟(之前 10 分钟),让用户在一次导入流程里(粘贴 → 解析 →
// 编辑 → 逐个 OAuth 登录跳转)不会中途被强制 re-unlock。超过 15 分钟未操作仍自动锁,
// 保持闲置时的安全边界。
SessionTTL time.Duration
// VKCacheTTL is the per-entry TTL for the virtual-key list cache.
VKCacheTTL time.Duration
// CliTimeout bounds every `aikey _internal` subprocess invocation.
CliTimeout time.Duration
}
Config knobs the caller can tune at boot.
type Handlers ¶
type Handlers struct {
Vault *VaultHandlers
Import *ImportHandlers
VaultCRUD *VaultCRUDHandlers
}
Handlers is the top-level bundle mounted on /api/user/{import,vault}/*. Keeping the two sub-groups together simplifies wiring: the user package's Handlers struct only needs one Import *importpkg.Handlers field, and the router in internal/api/router.go only needs one Register call.
func NewHandlers ¶
NewHandlers constructs the Handlers bundle. A nil cfg triggers defaults.
func (*Handlers) Register ¶
Register attaches the HTTP routes to the given mux under the standard /api/user/ prefix. authMW should be the caller's JWT auth middleware (same one used for /accounts/me); we apply it to everything except /vault/status which the Web UI probes before login.
Route ownership:
POST /api/user/vault/unlock -> VaultHandlers.UnlockHandler POST /api/user/vault/lock -> VaultHandlers.LockHandler GET /api/user/vault/status -> VaultHandlers.StatusHandler (unauthed probe) GET /api/user/vault/list -> VaultCRUDHandlers.ListHandler (requires unlock) PATCH /api/user/vault/entry/alias -> VaultCRUDHandlers.AliasPatchHandler (requires unlock) POST /api/user/vault/entry -> VaultCRUDHandlers.EntryAddHandler (requires unlock) DELETE /api/user/vault/entry -> VaultCRUDHandlers.EntryDeleteHandler (requires unlock) POST /api/user/vault/use -> VaultCRUDHandlers.UseHandler (requires unlock)
The former POST /api/user/vault/reveal endpoint (plaintext secret read) was removed 2026-04-24 security review round 2; see vault_crud.go.
POST /api/user/import/parse -> ImportHandlers.ParseHandler POST /api/user/import/confirm -> ImportHandlers.ConfirmHandler (requires unlock) GET /api/user/import/rules -> ImportHandlers.RulesHandler
type ImportHandlers ¶
ImportHandlers bundles the /api/user/import/* endpoints. Each handler is a thin envelope around one cli subcommand; the cli owns the business logic (parse engine, vault writes, audit log) and this layer only marshals JSON.
func (*ImportHandlers) ConfirmHandler ¶
func (h *ImportHandlers) ConfirmHandler(w http.ResponseWriter, r *http.Request)
ConfirmHandler: POST /api/user/import/confirm Body: forwarded as the `payload` of cli `_internal vault-op` action="batch_import". Requires unlocked session (see router wiring via RequireUnlock).
func (*ImportHandlers) ParseHandler ¶
func (h *ImportHandlers) ParseHandler(w http.ResponseWriter, r *http.Request)
ParseHandler: POST /api/user/import/parse Body: forwarded as the `payload` of cli `_internal parse` envelope. No vault unlock required — parse runs on plaintext input only.
func (*ImportHandlers) RulesHandler ¶
func (h *ImportHandlers) RulesHandler(w http.ResponseWriter, r *http.Request)
RulesHandler: GET /api/user/import/rules
Delegates to `aikey _internal rules` so the YAML stays the single source of truth (the cli reads aikey-cli/data/provider_fingerprint.yaml).
Returned JSON shape:
{
"layer_versions": {"rules":"...", "crf":"...", "fingerprint":"..."},
"sample_providers": [...],
"family_base_urls": {"anthropic":"https://api.anthropic.com", ...},
"family_login_urls": {"anthropic":"https://claude.ai/login", ...}
}
The Web Import page harvests hosts from BOTH `family_base_urls` and `family_login_urls` to drive Use-Official auto-fill rules:
- empty / official-host-matched base_url → auto-fill official URL
- protocol multi-select transitions to length=1 with empty base_url → auto-fill that protocol's official URL
`family_login_urls` covers browser-facing pages (e.g. `aistudio.google.com/app/apikey`, `dashscope.console.aliyun.com/apiKey`) users frequently paste — without it the host-match rule would silently miss those.
FALLBACK: if the cli is missing or fails (binary not deployed yet, upgrade in flight, panic on startup), we serve a hardcoded snapshot so the Web UI keeps working with stale-but-valid data. The fallback is the last-known-good copy of the YAML maps; it WILL drift if the YAML is updated without redeploying the service binary, but a stale fallback is strictly better than a 5xx that breaks the import page entirely.
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 SessionStore ¶
type SessionStore struct {
// contains filtered or unexported fields
}
SessionStore is an in-memory session map keyed by session_id. Sessions survive process lifetime only; on restart users re-unlock. This is acceptable for Personal + Trial editions (local single-user) and intentional for Production (users unlock per session, no shared state).
func NewSessionStore ¶
func NewSessionStore(ttl time.Duration) *SessionStore
NewSessionStore returns a store with the given idle TTL. Use 10 minutes for Personal, shorter for high-security deployments.
type VKCache ¶
type VKCache struct {
// contains filtered or unexported fields
}
VKCache is a tiny in-memory TTL cache for the user's virtual-key list. The Web UI queries /api/user/virtual-keys while composing the import manifest to look up aliases + provider bindings; those queries repeat within one editing session, so a short cache avoids hitting the delivery handler on every keystroke.
Stage 4 ships the skeleton; actual wiring to the virtual-key delivery endpoint is Stage 5 work (together with the React interactions). The Get / Set methods are ready-to-call so Stage 5 can just bolt in a fetcher.
func NewVKCache ¶
NewVKCache returns a cache with the given per-entry TTL. Defaults recommended: 5 * time.Minute per UX v2 "sliding TTL on read".
func (*VKCache) Get ¶
Get returns the cached value and a cache-hit flag. Expired entries are evicted lazily on read.
func (*VKCache) Invalidate ¶
Invalidate drops a key (called after batch_import succeeds so the next VK list fetch reflects the newly imported aliases).
type VaultCRUDHandlers ¶
type VaultCRUDHandlers struct {
Store *SessionStore
Bridge *CliBridge
}
VaultCRUDHandlers bundles the five User-Vault-page endpoints. Depends on SessionStore + CliBridge already built by NewHandlers.
func NewVaultCRUDHandlers ¶
func NewVaultCRUDHandlers(store *SessionStore, bridge *CliBridge) *VaultCRUDHandlers
NewVaultCRUDHandlers wires a VaultCRUDHandlers with shared deps.
func (*VaultCRUDHandlers) AliasPatchHandler ¶
func (h *VaultCRUDHandlers) AliasPatchHandler(w http.ResponseWriter, r *http.Request)
AliasPatchHandler: PATCH /api/user/vault/entry/alias.
func (*VaultCRUDHandlers) EntryAddHandler ¶
func (h *VaultCRUDHandlers) EntryAddHandler(w http.ResponseWriter, r *http.Request)
EntryAddHandler: POST /api/user/vault/entry.
func (*VaultCRUDHandlers) EntryDeleteHandler ¶
func (h *VaultCRUDHandlers) EntryDeleteHandler(w http.ResponseWriter, r *http.Request)
EntryDeleteHandler: DELETE /api/user/vault/entry.
func (*VaultCRUDHandlers) ListHandler ¶
func (h *VaultCRUDHandlers) ListHandler(w http.ResponseWriter, r *http.Request)
ListHandler: GET /api/user/vault/list.
Dispatches to one of two cli paths based on whether the caller has an unlocked vault session:
Unlocked (valid session cookie in Store) → spawns `query list_personal_with_masked` + `query list_oauth` in parallel and merges the two into a single `records[]` array. Personal rows carry secret_prefix / secret_suffix / secret_len so the UI can render `sk-ant-api03-•••••-afef3`.
Locked (no cookie / expired session) → spawns the single `query list_metadata_locked` action, which reads ONLY plaintext columns from vault.db (no AES-GCM decryption, no password_hash check). Personal rows carry secret_prefix / secret_suffix / secret_len = null; the UI renders a pure-asterisk secret pill.
Why this handler does its own session lookup instead of running under RequireUnlock middleware: we intentionally SHOULD serve this endpoint when locked, so the middleware would be wrong for it. See also `list_metadata_locked` in aikey-cli/src/commands_internal/query.rs for the security reasoning (2026-04-23 user decision A).
func (*VaultCRUDHandlers) UseHandler ¶
func (h *VaultCRUDHandlers) UseHandler(w http.ResponseWriter, r *http.Request)
UseHandler: POST /api/user/vault/use.
Switches the default-profile provider binding(s) for the given key. Multi-provider semantics match `aikey use` non-interactive mode: a personal key with `supported_providers: ["anthropic","openai"]` promotes this single key across BOTH providers in one call. OAuth accounts always target exactly one provider (the OAuth issuer).
Unlock required — the underlying vault-op verifies the vault_key against password_hash. The routing binding table isn't encrypted, but unlock is still required because the operation also refreshes `~/.aikey/active.env` which integrates provider-scoped sentinel tokens that we don't want to regenerate without a session.
type VaultHandlers ¶
type VaultHandlers struct {
Store *SessionStore
Bridge *CliBridge
// contains filtered or unexported fields
}
VaultHandlers bundles the /api/user/vault/{unlock,lock,status} endpoints.
func (*VaultHandlers) LockHandler ¶
func (h *VaultHandlers) LockHandler(w http.ResponseWriter, r *http.Request)
LockHandler: POST /api/user/vault/lock (explicit lock; session is dropped)
func (*VaultHandlers) RequireUnlock ¶
func (h *VaultHandlers) RequireUnlock(next http.HandlerFunc) http.HandlerFunc
RequireUnlock wraps a handler so it runs only when the request carries a valid session cookie. The handler can retrieve the hex via vaultKeyFrom(ctx).
func (*VaultHandlers) StatusHandler ¶
func (h *VaultHandlers) StatusHandler(w http.ResponseWriter, r *http.Request)
StatusHandler: GET /api/user/vault/status (unauthenticated probe; used by the Web UI to render the locked vs unlocked banner).
func (*VaultHandlers) UnlockHandler ¶
func (h *VaultHandlers) UnlockHandler(w http.ResponseWriter, r *http.Request)
UnlockHandler: POST /api/user/vault/unlock Body: {"password": "..."} 1) call cli `vault-op metadata` to fetch salt + KDF params (no secret revealed) 2) derive Argon2id(password, salt) locally -> 32-byte key -> hex 3) call cli `vault-op verify` with the hex; on "ok", mint session 4) zero password bytes asap