task/31: Relay account whitelist + trust-gated access

## What was built
Full relay access control system: relay_accounts table, RelayAccountService,
trust hook integration, live policy enforcement, admin CRUD API, Timmy seed.

## DB change
`lib/db/src/schema/relay-accounts.ts` — new `relay_accounts` table:
  pubkey (PK, FK → nostr_identities.pubkey ON DELETE CASCADE),
  access_level ("none"|"read"|"write"), granted_by ("manual"|"auto-tier"),
  granted_at, revoked_at (nullable), notes. Pushed via `pnpm run push`.
`lib/db/src/schema/index.ts` — exports relay-accounts.

## 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, access_level → none
- syncFromTrustTier(pubkey, tier) — auto-promotes by tier; never downgrades manual grants
- list(opts) — returns all accounts, optionally filtered to active
- Tier→access map: new=none, established/trusted/elite=write (env-overridable)

## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier after writing tier.
Failure is caught + logged (non-blocking — trust flow never fails on relay error).

## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() now async: queries relay_accounts.getAccess(pubkey).
"write" → accept; "read"/"none"/missing → reject with clear message.
DB error → reject with "policy service error" (safe fail-closed).

## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer token 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 body)
POST /api/admin/relay/accounts/:pk/revoke — revoke (reason body)
pubkey validation: must be 64-char lowercase hex.

## Startup seed (`artifacts/api-server/src/index.ts`)
On every startup: grants Timmy's own pubkeyHex "write" access ("manual").
Idempotent upsert — safe across restarts.

## Smoke test results (all pass)
- Timmy pubkey → accept ✓; unknown pubkey → reject ✓
- Admin grant → accept ✓; admin revoke → reject ✓; admin list shows accounts ✓
- TypeScript: 0 errors in API server + lib/db
This commit is contained in:
alexpaynex
2026-03-19 20:21:12 +00:00
parent faef1fe5e0
commit 94613019fc
8 changed files with 425 additions and 45 deletions

View File

@@ -4,6 +4,7 @@ import { attachWebSocketServer } from "./routes/events.js";
import { rootLogger } from "./lib/logger.js";
import { timmyIdentityService } from "./lib/timmy-identity.js";
import { startEngagementEngine } from "./lib/engagement.js";
import { relayAccountService } from "./lib/relay-accounts.js";
const rawPort = process.env["PORT"];
@@ -30,4 +31,17 @@ server.listen(port, () => {
rootLogger.info("ws url", { url: `wss://${domain}/api/ws` });
}
startEngagementEngine();
// Seed Timmy's own pubkey with elite relay access on every startup.
// This is idempotent — upsert is safe to run multiple times.
relayAccountService
.grant(timmyIdentityService.pubkeyHex, "write", "Timmy's own pubkey — elite access", "manual")
.then(() =>
rootLogger.info("relay: Timmy's pubkey seeded with write access", {
pubkey: timmyIdentityService.pubkeyHex.slice(0, 8),
}),
)
.catch((err) =>
rootLogger.warn("relay: failed to seed Timmy's pubkey", { err }),
);
});