Files
smirk-backend-core/migrations/20260630000001_invite_codes.sql
jwinterm 610819bb5d feat(registration): composable gate framework + invite codes (gate #1)
First of the operator-configurable registration gates (PoW-priced restore and
pay-to-register follow). A NEW wallet must present a valid invite code when
REGISTRATION_REQUIRE_INVITE is on; returning wallets and self-hosting bypass it,
and the active gates are advertised via /capabilities so the wallet shapes
onboarding.

- migration + db: invite_codes table storing only sha256(code) (128-bit codes,
  no pepper needed). Redemption is an atomic single-statement claim
  (UPDATE ... WHERE used_at IS NULL RETURNING) — single-use even under concurrent
  submission (L1 test fires 8 concurrent claims; exactly one wins).
- core::invite: generate (base58, 128-bit) + sha256 hash.
- enforce_invite in extension_register, mirroring enforce_pow (returning bypass;
  ONE literal error for not-found/used/expired — never an oracle).
- RegistrationConfig.require_invite + RegistrationCapability in /capabilities.
- smirk-admin mint-invite --count/--label (prints raw codes once; stores hashes).
- L1 tests (single-use + concurrency race), .env.example, OpenAPI (drift clean).
2026-06-30 17:14:53 -04:00

21 lines
1.2 KiB
SQL

-- Operator-minted registration invite codes — one composable registration gate
-- (alongside PoW and, later, pay-to-register). v1 is single-use, operator-minted
-- via the `smirk-admin mint-invite` CLI; the schema leaves room for multi-use,
-- expiry, and referral without a breaking change.
--
-- Only sha256(code) is stored. The raw code is a bearer secret shown once at
-- mint time and never persisted, so a database leak is NOT a list of usable
-- codes — codes are >=128-bit random, so the hash is not brute-forceable and no
-- pepper is needed.
CREATE TABLE invite_codes (
code_hash TEXT PRIMARY KEY, -- hex sha256 of the raw code
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
label TEXT, -- optional operator note (batch / who-for)
expires_at TIMESTAMPTZ, -- NULL = never (reserved; v1 mints without expiry)
used_at TIMESTAMPTZ, -- NULL = unused (single-use)
used_by UUID REFERENCES users (id) ON DELETE SET NULL
);
-- Fast lookup of still-spendable codes (mint listing / future sweeps).
CREATE INDEX idx_invite_codes_unused ON invite_codes (created_at) WHERE used_at IS NULL;