// 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"` } // CreateInviteReq is the create-invite payload from the SPA. The admin fixes the // handle and role the future user will receive; TTLSecs is optional (0 uses the // bus default of 7 days). The admin never supplies a key — the user's client // generates its own keypair and publishes only its public keys at /register. type CreateInviteReq struct { Handle string `json:"handle"` Role string `json:"role"` TTLSecs int `json:"ttl_secs"` } // InviteView is a single-use registration invite as the admin panel sees it. The // token is the bearer secret the admin turns into a join link; JoinURL is that // link, pre-built by the gateway from the configured client base URL so the SPA // does not have to know where the client lives. type InviteView struct { Token string `json:"token"` Handle string `json:"handle"` Role string `json:"role"` ExpiresAt string `json:"expires_at"` Used bool `json:"used"` CreatedAt string `json:"created_at"` JoinURL string `json:"join_url"` } // 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"` // JoinBaseURL is the base URL of the END-USER client (the page that hosts // /join?token=…), configured on the gateway (--join-base-url / env // UNIBUS_JOIN_BASE_URL). It is NOT the admin panel's own URL: the join link // the admin shares points at the user-facing client, a separate app. Empty // when unconfigured; the SPA then falls back to its own origin and warns. JoinBaseURL string `json:"join_base_url"` } // 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 // DeleteUser hard-deletes a user (purge), distinct from RevokeUser's status // flip. The admin panel maps its "Eliminar (permanente)" action here. DeleteUser(ctx context.Context, signPub string) error // Invites (the wallet-model account-creation path). CreateInvite mints a // single-use registration link the admin shares; the user redeems it from // their own client without the admin ever handling a private key. ListInvites // returns the pending links. CreateInvite(ctx context.Context, req CreateInviteReq) (InviteView, error) ListInvites(ctx context.Context) ([]InviteView, error) }