## 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.
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { createServer } from "http";
|
|
import app from "./app.js";
|
|
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"];
|
|
|
|
if (!rawPort) {
|
|
throw new Error("PORT environment variable is required but was not provided.");
|
|
}
|
|
|
|
const port = Number(rawPort);
|
|
|
|
if (Number.isNaN(port) || port <= 0) {
|
|
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
|
}
|
|
|
|
const server = createServer(app);
|
|
attachWebSocketServer(server);
|
|
|
|
server.listen(port, () => {
|
|
rootLogger.info("server started", { port });
|
|
rootLogger.info("timmy identity", { npub: timmyIdentityService.npub });
|
|
const domain = process.env["REPLIT_DEV_DOMAIN"];
|
|
if (domain) {
|
|
rootLogger.info("public url", { url: `https://${domain}/api/ui` });
|
|
rootLogger.info("tower url", { url: `https://${domain}/tower` });
|
|
rootLogger.info("ws url", { url: `wss://${domain}/api/ws` });
|
|
}
|
|
startEngagementEngine();
|
|
|
|
// Seed Timmy's own pubkey with elite identity + relay write access.
|
|
// Resolves pubkey from TIMMY_NOSTR_PUBKEY env var if set (explicit override),
|
|
// otherwise falls back to the hex pubkey derived from TIMMY_NOSTR_NSEC.
|
|
// Idempotent — safe to run on every startup.
|
|
const timmyPubkey = process.env["TIMMY_NOSTR_PUBKEY"] ?? timmyIdentityService.pubkeyHex;
|
|
relayAccountService
|
|
.seedElite(timmyPubkey, "Timmy's own pubkey — elite access seeded at startup")
|
|
.then(() =>
|
|
rootLogger.info("relay: Timmy's pubkey seeded with elite access", {
|
|
pubkey: timmyPubkey.slice(0, 8),
|
|
source: process.env["TIMMY_NOSTR_PUBKEY"] ? "TIMMY_NOSTR_PUBKEY env" : "timmyIdentityService",
|
|
}),
|
|
)
|
|
.catch((err) =>
|
|
rootLogger.warn("relay: failed to seed Timmy's pubkey", { err }),
|
|
);
|
|
});
|