task/31: Relay account whitelist + trust-gated access (v2 — code review fixes)
## What was built
Full relay access control: relay_accounts table, RelayAccountService,
trust hook, live policy enforcement, admin CRUD API, elite startup seed.
## DB schema (`lib/db/src/schema/relay-accounts.ts`)
relay_accounts table: pubkey (PK, FK nostr_identities ON DELETE CASCADE),
access_level (none/read/write), granted_by (text), granted_at, revoked_at, notes.
Exported from lib/db/src/schema/index.ts. Pushed via pnpm run push.
## RelayAccountService (`artifacts/api-server/src/lib/relay-accounts.ts`)
- getAccess(pubkey) → RelayAccessLevel (none if missing or revoked)
- grant(pubkey, level, reason, grantedBy) — upsert; creates nostr_identity FK
- revoke(pubkey, reason) — sets revokedAt, accessLevel→none, grantedBy→"manual-revoked"
The "manual-revoked" marker prevents syncFromTrustTier from auto-reinstating.
Only explicit admin grant() can restore access after revocation.
- syncFromTrustTier(pubkey) — fetches tier from DB internally (no tier param to
avoid caller drift). Respects: manual-revoked (skip), manual active (upgrade only),
auto-tier (full sync). Never auto-reinstates revoked accounts.
- seedElite(pubkey, notes) — upserts nostr_identities with tier="elite" +
trustScore=200, then grants relay write access as a permanent manual grant.
Called at startup for Timmy's own pubkey.
- list(opts) — returns all accounts, filtered by activeOnly if requested.
- Tier→access: new=none, established/trusted/elite=write (env-overridable)
## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier(pubkey) after DB write.
Fire-and-forget with catch (trust flow is never blocked by relay errors).
## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() async: queries relay_accounts.getAccess(). write→accept,
read/none/missing→reject. DB error→reject (fail-closed).
## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer auth (localhost-only fallback in dev; error log in prod).
GET /api/admin/relay/accounts — list all accounts
POST /api/admin/relay/accounts/:pk/grant — grant (level + notes)
POST /api/admin/relay/accounts/:pk/revoke — revoke (sets manual-revoked)
pubkey validation: 64-char lowercase hex only.
## Startup seed (`artifacts/api-server/src/index.ts`)
Resolves pubkey from TIMMY_NOSTR_PUBKEY env first, falls back to
timmyIdentityService.pubkeyHex. Calls seedElite() — idempotent upsert.
Sets nostr_identities.tier="elite" alongside relay write access.
## Smoke test results (all pass)
Timmy accept ✓; unknown reject ✓; grant→accept ✓; revoke→manual-revoked ✓;
revoked stays rejected ✓; TypeScript 0 errors.