Files

63 lines
2.6 KiB
TypeScript
Raw Permalink Normal View History

import { createServer } from "http";
import app from "./app.js";
import { attachWebSocketServer } from "./routes/events.js";
import { rootLogger } from "./lib/logger.js";
task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement 1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts) - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode) - Generates and warns about ephemeral key if env var absent - sign(EventTemplate) → finalizeEvent() with Timmy's key - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt() - Logs npub at server startup 2. ZapService (artifacts/api-server/src/lib/zap.ts) - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware) - Logs every outbound event to timmy_nostr_events audit table - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess() - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10) - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0 3. Engagement engine (artifacts/api-server/src/lib/engagement.ts) - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled) - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold - Generates personalised DM via agentService.chatReply() - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set - First run delayed 60s after startup to avoid cold-start noise 4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts) - POST /api/identity/vouch: requires X-Nostr-Token with elite tier - Verifies optional Nostr event signature from voucher - Records relationship in nostr_trust_vouches table - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score - GET /api/identity/timmy: public endpoint returning npub + zap count 5. DB schema additions (lib/db/src/schema/) - timmy_nostr_events: audit log for all outbound Nostr events - nostr_trust_vouches: voucher/vouchee social graph with boost amount - Tables created in production DB via drizzle-kit push 6. Frontend identity card (the-matrix/) - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback - Refreshes every 60s to show live zap count - Wired into main.js on firstInit
2026-03-19 19:27:13 +00:00
import { timmyIdentityService } from "./lib/timmy-identity.js";
import { startEngagementEngine } from "./lib/engagement.js";
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
2026-03-19 20:21:12 +00:00
import { relayAccountService } from "./lib/relay-accounts.js";
task/32: Event moderation queue + Timmy AI review ## What was built Full moderation pipeline: relay_event_queue table, strfry inject helper, ModerationService with Claude haiku review, policy tier routing, 30s poll loop, admin approve/reject/list endpoints. ## DB schema (`lib/db/src/schema/relay-event-queue.ts`) relay_event_queue: event_id (PK), pubkey (FK → nostr_identities), kind, raw_event (text JSON), status (pending/approved/rejected/auto_approved), reviewed_by (timmy_ai/admin/null), review_reason, created_at, decided_at. Exported from schema/index.ts. Pushed via pnpm run push. ## strfry HTTP client (`artifacts/api-server/src/lib/strfry.ts`) injectEvent(rawEventJson) — POST {STRFRY_URL}/import (NDJSON). STRFRY_URL defaults to "http://strfry:7777" (Docker internal network). 5s timeout; graceful failure in dev when strfry not running; never throws. ## ModerationService (`artifacts/api-server/src/lib/moderation.ts`) - enqueue(event) — insert pending row; idempotent onConflictDoNothing - autoReview(eventId) — Claude haiku prompt: approve or flag. On flag, marks reviewedBy=timmy_ai and leaves pending for admin. On approve, calls decide(). - decide(eventId, status, reason, reviewedBy) — updates DB + calls injectEvent - processPending(limit=10) — batch poll: auto-review up to limit pending events - Stub mode: auto-approves all events when Anthropic key absent ## Policy endpoint update (`artifacts/api-server/src/routes/relay.ts`) Tier routing in evaluatePolicy: read/none → reject (unchanged) write + elite tier → injectEvent + accept (elite bypass; shadowReject if inject fails) write + non-elite → enqueue + shadowReject (held for moderation) Imports db/nostrIdentities directly for tier check. Both inject and enqueue errors are fail-closed (reject vs shadowReject respectively). ## Background poll loop (`artifacts/api-server/src/index.ts`) setInterval every 30s calling moderationService.processPending(10). Interval configurable via MODERATION_POLL_MS env var. Errors caught per-event; poll loop never crashes the server. ## Admin queue routes (`artifacts/api-server/src/routes/admin-relay-queue.ts`) ADMIN_SECRET Bearer auth (same pattern as admin-relay.ts). GET /api/admin/relay/queue?status=... — list all / by status POST /api/admin/relay/queue/:eventId/approve — approve + inject into strfry POST /api/admin/relay/queue/:eventId/reject — reject (no inject) 409 on duplicate decisions. Registered in routes/index.ts. ## Smoke tests (all pass) Unknown → reject ✓; elite → shadowReject (strfry unavailable in dev) ✓; non-elite write → shadowReject + pending in queue ✓; admin approve → approved ✓; moderation poll loop started ✓; TypeScript 0 errors.
2026-03-19 20:35:39 +00:00
import { moderationService } from "./lib/moderation.js";
2026-03-13 23:21:55 +00:00
const rawPort = process.env["PORT"];
if (!rawPort) {
throw new Error("PORT environment variable is required but was not provided.");
2026-03-13 23:21:55 +00:00
}
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 });
task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement 1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts) - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode) - Generates and warns about ephemeral key if env var absent - sign(EventTemplate) → finalizeEvent() with Timmy's key - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt() - Logs npub at server startup 2. ZapService (artifacts/api-server/src/lib/zap.ts) - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware) - Logs every outbound event to timmy_nostr_events audit table - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess() - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10) - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0 3. Engagement engine (artifacts/api-server/src/lib/engagement.ts) - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled) - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold - Generates personalised DM via agentService.chatReply() - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set - First run delayed 60s after startup to avoid cold-start noise 4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts) - POST /api/identity/vouch: requires X-Nostr-Token with elite tier - Verifies optional Nostr event signature from voucher - Records relationship in nostr_trust_vouches table - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score - GET /api/identity/timmy: public endpoint returning npub + zap count 5. DB schema additions (lib/db/src/schema/) - timmy_nostr_events: audit log for all outbound Nostr events - nostr_trust_vouches: voucher/vouchee social graph with boost amount - Tables created in production DB via drizzle-kit push 6. Frontend identity card (the-matrix/) - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback - Refreshes every 60s to show live zap count - Wired into main.js on firstInit
2026-03-19 19:27:13 +00:00
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` });
}
task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement 1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts) - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode) - Generates and warns about ephemeral key if env var absent - sign(EventTemplate) → finalizeEvent() with Timmy's key - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt() - Logs npub at server startup 2. ZapService (artifacts/api-server/src/lib/zap.ts) - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware) - Logs every outbound event to timmy_nostr_events audit table - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess() - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10) - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0 3. Engagement engine (artifacts/api-server/src/lib/engagement.ts) - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled) - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold - Generates personalised DM via agentService.chatReply() - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set - First run delayed 60s after startup to avoid cold-start noise 4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts) - POST /api/identity/vouch: requires X-Nostr-Token with elite tier - Verifies optional Nostr event signature from voucher - Records relationship in nostr_trust_vouches table - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score - GET /api/identity/timmy: public endpoint returning npub + zap count 5. DB schema additions (lib/db/src/schema/) - timmy_nostr_events: audit log for all outbound Nostr events - nostr_trust_vouches: voucher/vouchee social graph with boost amount - Tables created in production DB via drizzle-kit push 6. Frontend identity card (the-matrix/) - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback - Refreshes every 60s to show live zap count - Wired into main.js on firstInit
2026-03-19 19:27:13 +00:00
startEngagementEngine();
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
2026-03-19 20:21:12 +00:00
task/32: Event moderation queue + Timmy AI review ## What was built Full moderation pipeline: relay_event_queue table, strfry inject helper, ModerationService with Claude haiku review, policy tier routing, 30s poll loop, admin approve/reject/list endpoints. ## DB schema (`lib/db/src/schema/relay-event-queue.ts`) relay_event_queue: event_id (PK), pubkey (FK → nostr_identities), kind, raw_event (text JSON), status (pending/approved/rejected/auto_approved), reviewed_by (timmy_ai/admin/null), review_reason, created_at, decided_at. Exported from schema/index.ts. Pushed via pnpm run push. ## strfry HTTP client (`artifacts/api-server/src/lib/strfry.ts`) injectEvent(rawEventJson) — POST {STRFRY_URL}/import (NDJSON). STRFRY_URL defaults to "http://strfry:7777" (Docker internal network). 5s timeout; graceful failure in dev when strfry not running; never throws. ## ModerationService (`artifacts/api-server/src/lib/moderation.ts`) - enqueue(event) — insert pending row; idempotent onConflictDoNothing - autoReview(eventId) — Claude haiku prompt: approve or flag. On flag, marks reviewedBy=timmy_ai and leaves pending for admin. On approve, calls decide(). - decide(eventId, status, reason, reviewedBy) — updates DB + calls injectEvent - processPending(limit=10) — batch poll: auto-review up to limit pending events - Stub mode: auto-approves all events when Anthropic key absent ## Policy endpoint update (`artifacts/api-server/src/routes/relay.ts`) Tier routing in evaluatePolicy: read/none → reject (unchanged) write + elite tier → injectEvent + accept (elite bypass; shadowReject if inject fails) write + non-elite → enqueue + shadowReject (held for moderation) Imports db/nostrIdentities directly for tier check. Both inject and enqueue errors are fail-closed (reject vs shadowReject respectively). ## Background poll loop (`artifacts/api-server/src/index.ts`) setInterval every 30s calling moderationService.processPending(10). Interval configurable via MODERATION_POLL_MS env var. Errors caught per-event; poll loop never crashes the server. ## Admin queue routes (`artifacts/api-server/src/routes/admin-relay-queue.ts`) ADMIN_SECRET Bearer auth (same pattern as admin-relay.ts). GET /api/admin/relay/queue?status=... — list all / by status POST /api/admin/relay/queue/:eventId/approve — approve + inject into strfry POST /api/admin/relay/queue/:eventId/reject — reject (no inject) 409 on duplicate decisions. Registered in routes/index.ts. ## Smoke tests (all pass) Unknown → reject ✓; elite → shadowReject (strfry unavailable in dev) ✓; non-elite write → shadowReject + pending in queue ✓; admin approve → approved ✓; moderation poll loop started ✓; TypeScript 0 errors.
2026-03-19 20:35:39 +00:00
// ── Moderation poll loop ─────────────────────────────────────────────────
// Processes up to 10 pending relay events every 30 seconds via Claude haiku.
const MODERATION_POLL_MS = parseInt(process.env["MODERATION_POLL_MS"] ?? "", 10) || 30_000;
setInterval(() => {
moderationService.processPending(10).catch((err) =>
rootLogger.error("moderation poll error", { err }),
);
}, MODERATION_POLL_MS);
rootLogger.info("moderation poll loop started", { intervalMs: MODERATION_POLL_MS });
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.
2026-03-19 20:26:03 +00:00
// 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;
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
2026-03-19 20:21:12 +00:00
relayAccountService
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.
2026-03-19 20:26:03 +00:00
.seedElite(timmyPubkey, "Timmy's own pubkey — elite access seeded at startup")
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
2026-03-19 20:21:12 +00:00
.then(() =>
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.
2026-03-19 20:26:03 +00:00
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",
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
2026-03-19 20:21:12 +00:00
}),
)
.catch((err) =>
rootLogger.warn("relay: failed to seed Timmy's pubkey", { err }),
);
2026-03-13 23:21:55 +00:00
});