This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
token-gated-economy/artifacts/api-server/src/index.ts

52 lines
1.9 KiB
TypeScript
Raw 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";
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/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
});