Files
timmy-tower/artifacts/api-server/src/routes/relay.ts

231 lines
7.7 KiB
TypeScript
Raw Normal View History

task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
/**
* relay.ts Nostr relay write-policy API
*
* POST /api/relay/policy
* Internal endpoint called exclusively by the relay-policy sidecar.
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
* Protected by RELAY_POLICY_SECRET Bearer token.
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
* Policy tiers:
* no relay_account row / "read" / "none" reject
* "write" + tier != "elite" enqueue + shadowReject
* "write" + tier == "elite" inject into strfry + accept
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
*
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
* GET /api/relay/policy
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
* Health + roundtrip probe (no auth).
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
*/
import { Router, type Request, type Response } from "express";
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 { db, nostrIdentities } from "@workspace/db";
import { eq } from "drizzle-orm";
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
import { makeLogger } from "../lib/logger.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";
import { injectEvent } from "../lib/strfry.js";
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
const logger = makeLogger("relay-policy");
const router = Router();
const RELAY_POLICY_SECRET = process.env["RELAY_POLICY_SECRET"] ?? "";
const IS_PROD = process.env["NODE_ENV"] === "production";
if (!RELAY_POLICY_SECRET) {
if (IS_PROD) {
logger.error(
"RELAY_POLICY_SECRET is not set in production — " +
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
"POST /api/relay/policy is open to any caller.",
);
} else {
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
logger.warn("RELAY_POLICY_SECRET not set — /api/relay/policy accepts local-only requests (dev mode)");
}
}
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
// ── Types ─────────────────────────────────────────────────────────────────────
type PolicyAction = "accept" | "reject" | "shadowReject";
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
interface NostrEventPayload {
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
id: string;
pubkey: string;
kind: number;
created_at: number;
tags: string[][];
content: string;
sig: string;
}
interface PolicyRequest {
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
event: NostrEventPayload;
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
receivedAt: number;
sourceType: string;
sourceInfo: string;
}
interface PolicyDecision {
id: string;
action: PolicyAction;
msg: string;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
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
function rejectDecision(id: string, msg: string): PolicyDecision {
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
return { id, action: "reject", msg };
}
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
function acceptDecision(id: string): PolicyDecision {
return { id, action: "accept", msg: "" };
}
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
function shadowRejectDecision(id: string): PolicyDecision {
return { id, action: "shadowReject", msg: "" };
}
// ── GET /relay/policy ─────────────────────────────────────────────────────────
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
router.get("/relay/policy", async (_req: Request, res: Response) => {
res.json({
ok: true,
secretConfigured: !!RELAY_POLICY_SECRET,
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
info: "Relay policy active. write+elite → accept; write+non-elite → moderation queue; read/none → reject.",
});
});
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
// ── Auth middleware ───────────────────────────────────────────────────────────
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
function checkRelayAuth(req: Request, res: Response): boolean {
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
if (RELAY_POLICY_SECRET) {
const authHeader = req.headers["authorization"] ?? "";
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
if (token !== RELAY_POLICY_SECRET) {
res.status(401).json({ error: "Unauthorized" });
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
return false;
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
return true;
}
const ip = req.ip ?? "";
const isLocal = ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
if (!isLocal) {
logger.warn("relay/policy: no secret configured, rejecting non-local call", { ip });
res.status(401).json({ error: "Unauthorized" });
return false;
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
logger.warn("relay/policy: RELAY_POLICY_SECRET not set — accepting local-only call");
return true;
}
// ── POST /relay/policy ────────────────────────────────────────────────────────
router.post("/relay/policy", async (req: Request, res: Response) => {
if (!checkRelayAuth(req, res)) return;
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
const body = req.body as Partial<PolicyRequest>;
const event = body.event;
if (!event || typeof event !== "object") {
res.status(400).json({ error: "Missing 'event' in request body" });
return;
}
const eventId = typeof event.id === "string" ? event.id : "unknown";
const pubkey = typeof event.pubkey === "string" ? event.pubkey : "";
const kind = typeof event.kind === "number" ? event.kind : -1;
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
const decision = await evaluatePolicy(event, eventId, pubkey, kind);
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
logger.info("relay policy decision", {
eventId: eventId.slice(0, 8),
pubkey: pubkey.slice(0, 8),
kind,
action: decision.action,
sourceType: body.sourceType,
});
res.json(decision);
});
/**
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
* Core write-policy evaluation.
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
* 1. No pubkey reject
* 2. Not in relay_accounts with "write" reject (or "read-only" msg)
* 3. "write" + elite tier inject into strfry + accept (elite bypass)
* 4. "write" + non-elite enqueue into moderation + shadowReject
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
*/
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
async function evaluatePolicy(
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
rawEvent: Partial<NostrEventPayload>,
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
eventId: string,
pubkey: string,
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
kind: number,
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
): Promise<PolicyDecision> {
if (!pubkey) {
return rejectDecision(eventId, "missing pubkey");
}
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
// ── Step 1: Check relay access ─────────────────────────────────────────────
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
let accessLevel: string;
try {
accessLevel = await relayAccountService.getAccess(pubkey);
} catch (err) {
logger.error("relay-accounts lookup failed — defaulting to reject", { err });
return rejectDecision(eventId, "policy service error — try again later");
}
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
if (accessLevel === "read") {
return rejectDecision(eventId, "read-only access — write not permitted");
}
if (accessLevel !== "write") {
return rejectDecision(eventId, "pubkey not whitelisted for this relay");
}
// ── Step 2: Check trust tier (elite bypass) ────────────────────────────────
let isElite = false;
try {
const rows = await db
.select({ tier: nostrIdentities.tier })
.from(nostrIdentities)
.where(eq(nostrIdentities.pubkey, pubkey))
.limit(1);
isElite = rows[0]?.tier === "elite";
} catch (err) {
logger.error("tier lookup failed — treating as non-elite", { err });
}
if (isElite) {
// Elite accounts bypass moderation — inject directly into strfry
const rawJson = JSON.stringify(rawEvent);
const injectResult = await injectEvent(rawJson);
if (!injectResult.ok) {
logger.warn("elite event inject failed — shadowReject as fallback", {
eventId: eventId.slice(0, 8),
error: injectResult.error,
});
return shadowRejectDecision(eventId);
}
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
return acceptDecision(eventId);
}
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
// ── Step 3: Non-elite write — enqueue for moderation ──────────────────────
try {
await moderationService.enqueue({
id: eventId,
pubkey,
kind,
rawJson: JSON.stringify(rawEvent),
});
} catch (err) {
logger.error("failed to enqueue event for moderation", {
eventId: eventId.slice(0, 8),
err,
});
// Fail-closed: reject if we can't queue
return rejectDecision(eventId, "moderation service error — try again later");
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/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +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
// shadowReject: strfry reports "ok" to the sender but doesn't publish
return shadowRejectDecision(eventId);
task/30: Sovereign Nostr relay infrastructure (strfry) ## Summary Deploys strfry (C++ Nostr relay) + relay-policy sidecar as a containerised stack on the VPS, wired to the API server for event-level access control. ## Files created - `infrastructure/strfry.conf` — strfry config: bind 0.0.0.0:7777, writePolicy plugin → /usr/local/bin/relay-policy-plugin, maxEventSize 65536, rejectEphemeral false, db /data/strfry-db - `infrastructure/relay-policy/plugin.sh` — strfry write-policy plugin (stdin/stdout bridge). Reads JSON lines from strfry, POSTs to relay-policy HTTP sidecar (http://relay-policy:3080/decide), writes decision to stdout. Safe fallback: reject on sidecar timeout/failure - `infrastructure/relay-policy/index.ts` — Node.js HTTP relay-policy sidecar: POST /decide receives strfry events, calls API server /api/relay/policy with Bearer RELAY_POLICY_SECRET, returns strfry decision JSON - `infrastructure/relay-policy/package.json + tsconfig.json` — TS build config - `infrastructure/relay-policy/Dockerfile` — multi-stage: builder (tsc) + runtime - `infrastructure/relay-policy/.gitignore` — excludes node_modules, dist - `artifacts/api-server/src/routes/relay.ts` — POST /api/relay/policy: internal route protected by RELAY_POLICY_SECRET Bearer token. Bootstrap state: rejects all events with "relay not yet open — whitelist pending (Task #37)". Stable contract — future tasks extend evaluatePolicy() without API shape changes ## Files modified - `infrastructure/docker-compose.yml` — adds relay-policy + strfry services on node-net; strfry_data volume (bind-mounted at /data/strfry); relay-policy healthcheck; strfry depends on relay-policy healthy - `infrastructure/ops.sh` — adds relay:logs, relay:restart, relay:status commands - `artifacts/api-server/src/routes/index.ts` — registers relayRouter ## Operator setup required on VPS mkdir -p /data/strfry && chmod 700 /data/strfry echo "RELAY_API_URL=https://alexanderwhitestone.com" >> /opt/timmy-node/.env echo "RELAY_POLICY_SECRET=$(openssl rand -hex 32)" >> /opt/timmy-node/.env # Also set RELAY_POLICY_SECRET in Replit secrets for API server ## Notes - TypeScript: 0 errors (API server + relay-policy sidecar both compile clean) - POST /api/relay/policy smoke test: correct bootstrap reject response - strfry image: ghcr.io/hoytech/strfry:latest
2026-03-19 20:02:00 +00:00
}
export default router;