120 lines
4.4 KiB
Go
120 lines
4.4 KiB
Go
package agentapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// Controller-driven escrow ceremony client methods (v0.127.0, agent ≥ v0.88.0). The claim call
|
|
// is the ONLY place the recovery code R crosses this client — its response body must never be
|
|
// logged (the shared helpers log path/status/duration only, never bodies) and the caller hands
|
|
// R straight to the wizard's claim XHR, nowhere else.
|
|
|
|
// EscrowPreflightItem mirrors one agent preflight checklist row.
|
|
type EscrowPreflightItem struct {
|
|
ID string `json:"id"`
|
|
OK bool `json:"ok"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
// EscrowPreflightResponse mirrors GET /escrow/preflight.
|
|
type EscrowPreflightResponse struct {
|
|
VMID int `json:"vmid"`
|
|
OK bool `json:"ok"`
|
|
Items []EscrowPreflightItem `json:"items"`
|
|
}
|
|
|
|
// EscrowPreflight fetches the agent's ceremony prerequisite checklist.
|
|
func (c *Client) EscrowPreflight(ctx context.Context) (EscrowPreflightResponse, error) {
|
|
var out EscrowPreflightResponse
|
|
body, err := c.get(ctx, "/escrow/preflight")
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return out, fmt.Errorf("agentapi: decode /escrow/preflight: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// EscrowCeremonyStartResponse mirrors the POST /escrow/ceremony 202 payload.
|
|
type EscrowCeremonyStartResponse struct {
|
|
JobID string `json:"job_id"`
|
|
Phase string `json:"phase"`
|
|
}
|
|
|
|
// EscrowCeremonyStart triggers the agent's detached root ceremony job. Status-aware: the HTTP
|
|
// status is returned so the caller can map the agent's 409 (a ceremony already running) to its
|
|
// own house-style refusal.
|
|
func (c *Client) EscrowCeremonyStart(ctx context.Context) (EscrowCeremonyStartResponse, int, error) {
|
|
var out EscrowCeremonyStartResponse
|
|
env, status, err := c.postWithStatus(ctx, "/escrow/ceremony", struct{}{})
|
|
if err != nil {
|
|
return out, status, err
|
|
}
|
|
if err := refusalError("/escrow/ceremony", status, env); err != nil {
|
|
return out, status, err
|
|
}
|
|
if err := json.Unmarshal(env.Data, &out); err != nil {
|
|
return out, status, fmt.Errorf("agentapi: decode /escrow/ceremony: %w", err)
|
|
}
|
|
return out, status, nil
|
|
}
|
|
|
|
// EscrowCeremonyStatusResponse mirrors GET /escrow/ceremony/status — the NON-SECRET job view
|
|
// (R is structurally absent from the agent's payload).
|
|
type EscrowCeremonyStatusResponse struct {
|
|
Phase string `json:"phase"` // none | running | done | failed | unclaimed_void
|
|
JobID string `json:"job_id"`
|
|
KeyFingerprint string `json:"key_fingerprint"`
|
|
EntropyBits float64 `json:"entropy_bits"`
|
|
ResticPwSealed bool `json:"restic_pw_sealed"`
|
|
Uploaded bool `json:"uploaded"`
|
|
Claimable bool `json:"claimable"`
|
|
Claimed bool `json:"claimed"`
|
|
ClaimExpiresInSec int `json:"claim_expires_in_sec"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
|
|
// EscrowCeremonyStatus polls the ceremony job.
|
|
func (c *Client) EscrowCeremonyStatus(ctx context.Context) (EscrowCeremonyStatusResponse, error) {
|
|
var out EscrowCeremonyStatusResponse
|
|
body, err := c.get(ctx, "/escrow/ceremony/status")
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return out, fmt.Errorf("agentapi: decode /escrow/ceremony/status: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// EscrowCeremonyClaim performs the ONE-SHOT R claim. Returns the recovery code + the agent's
|
|
// HTTP status (410 = already claimed / expired — the wizard's void state). The code must never
|
|
// be logged, persisted, or placed anywhere but the claim XHR response; the error path carries
|
|
// the agent's reason text, never the code.
|
|
func (c *Client) EscrowCeremonyClaim(ctx context.Context) (string, int, error) {
|
|
env, status, err := c.postWithStatus(ctx, "/escrow/ceremony/claim", struct{}{})
|
|
if err != nil {
|
|
return "", status, err
|
|
}
|
|
if status == http.StatusGone {
|
|
return "", status, fmt.Errorf("agentapi: POST /escrow/ceremony/claim: gone (claimed or expired)")
|
|
}
|
|
if err := refusalError("/escrow/ceremony/claim", status, env); err != nil {
|
|
return "", status, err
|
|
}
|
|
var out struct {
|
|
RecoveryCode string `json:"recovery_code"`
|
|
}
|
|
if err := json.Unmarshal(env.Data, &out); err != nil {
|
|
return "", status, fmt.Errorf("agentapi: decode /escrow/ceremony/claim: %w", err)
|
|
}
|
|
if out.RecoveryCode == "" {
|
|
return "", status, fmt.Errorf("agentapi: /escrow/ceremony/claim returned no code")
|
|
}
|
|
return out.RecoveryCode, status, nil
|
|
}
|