// Package admin is the gateway behind the unibus admin panel: it holds the // operator's ADMIN identity, talks to the unibus control plane (signing every // request), and exposes a small REST API the embedded SPA consumes. The browser // never signs, never touches NATS, and never sees a private key — every // privileged action is mediated here. package admin import ( "context" ) // Posture is the security posture a membershipd node publishes on /healthz. It // mirrors membership.Posture but is duplicated here so the wire shape the SPA // consumes is owned by the gateway, not coupled to the bus package's struct tags. type Posture struct { Enforce bool `json:"enforce"` ACL bool `json:"acl"` TLS bool `json:"tls"` Cluster bool `json:"cluster"` Store string `json:"store"` } // NodeHealth is one cluster node's liveness + posture as seen from the gateway. type NodeHealth struct { Name string `json:"name"` URL string `json:"url"` Up bool `json:"up"` Posture Posture `json:"posture"` LatencyMs int64 `json:"latency_ms"` Error string `json:"error,omitempty"` } // RoomView is a room as the admin sees it (a room the admin owns or belongs to). type RoomView struct { RoomID string `json:"room_id"` Subject string `json:"subject"` Epoch int `json:"epoch"` Encrypt bool `json:"encrypt"` Persist bool `json:"persist"` SignMsgs bool `json:"sign_msgs"` Role string `json:"role"` } // MemberView is one member of a room with public keys rendered as hex (the // browser never needs the raw bytes). type MemberView struct { Endpoint string `json:"endpoint"` Role string `json:"role"` SignPub string `json:"sign_pub"` KexPub string `json:"kex_pub"` } // UserView is one bus allowlist entry. type UserView struct { SignPub string `json:"sign_pub"` Handle string `json:"handle"` Role string `json:"role"` Status string `json:"status"` CreatedAt string `json:"created_at"` RevokedAt string `json:"revoked_at,omitempty"` } // CreateRoomReq is the room-creation payload from the SPA. type CreateRoomReq struct { Subject string `json:"subject"` Encrypt bool `json:"encrypt"` Persist bool `json:"persist"` SignMsgs bool `json:"sign_msgs"` } // InviteReq is the invite payload. The invitee's public keys are supplied as hex // because an encrypted room seals the room key to the invitee's X25519 key, and // that key is not derivable from the endpoint id alone. type InviteReq struct { Endpoint string `json:"endpoint"` SignPub string `json:"sign_pub"` KexPub string `json:"kex_pub"` } // AddUserReq is the user-registration payload. type AddUserReq struct { SignPub string `json:"sign_pub"` Handle string `json:"handle"` Role string `json:"role"` } // MeInfo describes the gateway's own identity and which capabilities are wired, // so the SPA can render the operator endpoint and label the Users tab's backend. type MeInfo struct { Endpoint string `json:"endpoint"` SignPub string `json:"sign_pub"` UsersBackend string `json:"users_backend"` // "control-plane" (signed HTTP) | "sqlite" (single-node fallback) Mock bool `json:"mock"` } // Repo is the data source behind the REST API. Two implementations exist: // busRepo (the real control-plane + store gateway) and mockRepo (sample data for // UI iteration). Keeping it an interface lets the SPA be developed and demoed // against mock data with the exact same handlers the live bus uses. type Repo interface { Me(ctx context.Context) MeInfo // Cluster liveness + posture of every configured node. Cluster(ctx context.Context) []NodeHealth // Rooms the admin owns / belongs to, plus mutations the control plane allows. ListRooms(ctx context.Context) ([]RoomView, error) CreateRoom(ctx context.Context, req CreateRoomReq) (RoomView, error) ListMembers(ctx context.Context, roomID string) ([]MemberView, error) Invite(ctx context.Context, roomID string, req InviteReq) error // KickMember removes a member and rotates the room key to a new epoch // (forward secrecy). This is the rekey-on-kick primitive the bus exposes. KickMember(ctx context.Context, roomID, endpoint string) error // Users (the bus allowlist). The live gateway manages these against the bus // control plane's admin-only user endpoints, signing each request as the // operator's admin identity — so user management works in cluster without // direct store/KV access. A single-node deployment may instead point the // gateway at the SQLite store directly (--db) as an explicit fallback. UsersWritable() bool ListUsers(ctx context.Context) ([]UserView, error) AddUser(ctx context.Context, req AddUserReq) error RevokeUser(ctx context.Context, signPub string) error }