feat(membership): single-use invites + hard-delete in the Store (SQLite + KV)
Add the data layer for WhatsApp-style accounts on the wallet model: the admin mints a single-use invitation link, the new user redeems it by publishing only its public keys, and the admin can hard-delete a user. - Invite type and lifecycle (invites.go): 32-byte crypto/rand hex token, 7-day default TTL, fail-closed expiry parsing. Methods CreateInvite/GetInvite/ ListInvites/ConsumeInvite/CancelInvite on both backends. ConsumeInvite is atomic and single-use: SQLite uses a transaction guarded by `used = 0`, the KV store uses a compare-and-swap on the entry revision (mark-first). Both burn the token on claim, so an already-registered key surfaces ErrUserExists with the invite spent — identical semantics across backends. - DeleteUser (users.go + jetstream_store.go): hard-delete of the allowlist row, distinct from RevokeUser's status flip. Room memberships of the ex-user are intentionally left inert (they can no longer authenticate); no partial cleanup. - Migration 003_invites.sql (root + embedded copy, byte-identical): additive `invites` table with audit columns, per db_migrations rules. - Store interface gains DeleteUser, CreateInvite, GetInvite, ListInvites, ConsumeInvite, CancelInvite. New UNIBUS_invites KV bucket. - Consistency fix: SQLite GetUser now maps sql.ErrNoRows to ErrNotFound, matching the KV backend and the storage-agnostic contract documented in store.go. - ValidateKexPubHex added alongside ValidateSignPubHex for /register key checks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,9 +80,23 @@ type Store interface {
|
||||
GetUser(signPub string) (User, error)
|
||||
ListUsers() ([]User, error)
|
||||
RevokeUser(signPub string) error
|
||||
// DeleteUser hard-deletes a user (the purge counterpart of RevokeUser's
|
||||
// status flip): the row is removed, not just flagged. The ex-user can no
|
||||
// longer authenticate, so any room memberships they hold become inert.
|
||||
DeleteUser(signPub string) error
|
||||
IsAuthorized(signPub string) bool
|
||||
HasAdmin() bool
|
||||
|
||||
// Invites (single-use registration tokens; the wallet-model join path).
|
||||
// CreateInvite mints a token fixing handle+role; ConsumeInvite is the only
|
||||
// path that adds to the allowlist without an admin signature (the bearer
|
||||
// token is the authorization), spending the token exactly once.
|
||||
CreateInvite(handle, role string, ttlSecs int) (Invite, error)
|
||||
GetInvite(token string) (Invite, error)
|
||||
ListInvites() ([]Invite, error)
|
||||
ConsumeInvite(token, signPub, kexPub string) error
|
||||
CancelInvite(token string) error
|
||||
|
||||
// Lifecycle.
|
||||
Close() error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user