// Package auth resolves SSH connections into AgentBBS identities (PRD §4.4). package auth import ( "os" "strings" "github.com/charmbracelet/ssh" gossh "golang.org/x/crypto/ssh" ) // Kind classifies an identity. type Kind string const ( Guest Kind = "guest" Member Kind = "member" Agent Kind = "agent" ) // User is the resolved identity for one session. type User struct { Name string Kind Kind PubKeyFP string // SHA256 fingerprint, empty for guests without a key StoreID int64 // 0 for guests } // GuestNames are usernames that always map to an anonymous guest hub session. var GuestNames = map[string]bool{"bbs": true, "play": true, "guest": true} // PodNames are usernames that route to a personal pod instead of the hub. // Pod access requires an active paid membership (PRD pods addendum). var PodNames = map[string]bool{"pod": true} // JoinNames are usernames that trigger the onboarding flow: register the // visitor's public key, print instructions, and disconnect. var JoinNames = map[string]bool{"join": true, "signup": true, "register": true} // DomainNames are usernames that route to the custom-domain self-service flow: // list/add/remove the domains pointed at a member's homepage. var DomainNames = map[string]bool{"domain": true, "domains": true} // AdminNames are usernames that route to the privileged admin console (PRD §6). // The route only opens for accounts whose name is in the operator allowlist // (see IsAdmin); the name itself confers nothing. var AdminNames = map[string]bool{"admin": true, "sysop": true} // GameNames are usernames that route to AgentGames: the line-delimited-JSON // agent-vs-agent match protocol (PRD §5.2). `play@` stays a guest hub alias. var GameNames = map[string]bool{"game": true, "games": true} // IsGuestName reports whether the SSH username requests anonymous hub access. func IsGuestName(u string) bool { return GuestNames[strings.ToLower(u)] } // IsPodName reports whether the SSH username requests the pod route. func IsPodName(u string) bool { return PodNames[strings.ToLower(u)] } // IsJoinName reports whether the SSH username requests onboarding. func IsJoinName(u string) bool { return JoinNames[strings.ToLower(u)] } // IsDomainName reports whether the SSH username requests the custom-domain flow. func IsDomainName(u string) bool { return DomainNames[strings.ToLower(u)] } // IsAdminName reports whether the SSH username requests the admin console. func IsAdminName(u string) bool { return AdminNames[strings.ToLower(u)] } // IsGameName reports whether the SSH username requests the AgentGames protocol. func IsGameName(u string) bool { return GameNames[strings.ToLower(u)] } // Admins returns the operator-configured admin allowlist: the lowercased, // comma/space-separated account names in $AGENTBBS_ADMINS. Admin status can // only be granted by the operator (via env), never self-assigned in-band. func Admins() map[string]bool { out := map[string]bool{} for _, f := range strings.FieldsFunc(os.Getenv("AGENTBBS_ADMINS"), func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '\n' }) { out[strings.ToLower(f)] = true } return out } // IsAdmin reports whether the account name is in the operator allowlist. func IsAdmin(name string) bool { return Admins()[strings.ToLower(name)] } // KindFor infers the identity kind from a (non-guest) username. // Usernames prefixed "agent-" are automated clients (PRD §3). func KindFor(username string) Kind { if strings.HasPrefix(strings.ToLower(username), "agent-") { return Agent } return Member } // Fingerprint returns the SHA256 fingerprint for a session public key, or "". func Fingerprint(key ssh.PublicKey) string { if key == nil { return "" } return gossh.FingerprintSHA256(key) }