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:
@@ -0,0 +1,28 @@
|
||||
-- 003_invites.sql — single-use registration invites (issue: user accounts / wallet model).
|
||||
--
|
||||
-- An admin mints an invite so a brand-new identity can join the bus allowlist
|
||||
-- WITHOUT the admin ever handling its private key. The token is the bearer
|
||||
-- secret that authorizes POST /register: the registering client generates its
|
||||
-- keypair locally and publishes only its public keys, fixing the link between an
|
||||
-- invite and the identity it creates via the audit columns below. The handle and
|
||||
-- role are fixed by the admin at mint time and cannot be changed by the client
|
||||
-- (no privilege escalation).
|
||||
--
|
||||
-- Additive and idempotent: safe to apply repeatedly. Never modify this file;
|
||||
-- further schema changes go in new numbered migrations (see
|
||||
-- .claude/rules/db_migrations.md). The embedded copy under
|
||||
-- pkg/membership/migrations/003_invites.sql mirrors this file byte-for-byte.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invites (
|
||||
token TEXT PRIMARY KEY, -- 32 random bytes in lowercase hex (the bearer secret)
|
||||
handle TEXT NOT NULL, -- handle the new user will get (fixed by admin)
|
||||
role TEXT NOT NULL DEFAULT 'member', -- 'admin' | 'member' (fixed by admin)
|
||||
expires_at TEXT NOT NULL, -- RFC3339; past this the invite is dead
|
||||
used INTEGER NOT NULL DEFAULT 0, -- 0 pending, 1 consumed (single-use)
|
||||
created_at TEXT NOT NULL,
|
||||
used_at TEXT, -- RFC3339 when consumed (NULL until used)
|
||||
used_sign_pub TEXT, -- Ed25519 key that consumed it (audit; NULL until used)
|
||||
used_kex_pub TEXT -- X25519 key presented at registration (audit; NULL until used)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_invites_used ON invites(used);
|
||||
Reference in New Issue
Block a user