diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4411fa1 --- /dev/null +++ b/.env.example @@ -0,0 +1,59 @@ +# ============================================================ +# Timmy API Server — Environment Variables +# Copy this file to .env and fill in your values. +# Never commit .env to version control. +# ============================================================ + +# ── Server ─────────────────────────────────────────────────── +PORT=8080 +NODE_ENV=development + +# ── Lightning / LNbits ─────────────────────────────────────── +# Set both variables to connect the API to a real Lightning node. +# Leave blank to run in stub mode (invoices simulated in-memory). +# +# After running infrastructure/lnd-init.sh on the droplet, it will +# print both values. Copy them here. +# +# LNBITS_URL is the public HTTPS URL exposed via Tailscale Funnel, +# e.g. https://.tail12345.ts.net +LNBITS_URL= +LNBITS_API_KEY= + +# ── Database ───────────────────────────────────────────────── +DATABASE_URL=postgresql://localhost:5432/timmy + +# ── Nostr identity ─────────────────────────────────────────── +# nsec for Timmy's Nostr keypair (signs relay events). +# Generate with: openssl rand -hex 32 +TIMMY_NOSTR_NSEC= +# Optional: override the derived pubkey explicitly (hex, not npub). +TIMMY_NOSTR_PUBKEY= + +# ── AI integrations ────────────────────────────────────────── +# Anthropic Claude — leave blank to use canned AI stub responses. +AI_INTEGRATIONS_ANTHROPIC_API_KEY= + +# Gemini (optional) +GEMINI_API_KEY= + +# ── CORS ───────────────────────────────────────────────────── +# Comma-separated list of allowed origins in production. +# Default: alexanderwhitestone.com variants. +# Leave blank to allow all origins (development default). +# CORS_ORIGINS=https://alexanderwhitestone.com,https://www.alexanderwhitestone.com + +# ── Relay policy ───────────────────────────────────────────── +# Shared secret between the API server and the strfry relay-policy sidecar. +RELAY_POLICY_SECRET= + +# ── Rate limiting / misc ───────────────────────────────────── +# Milliseconds between moderation poll cycles (default: 30000). +# MODERATION_POLL_MS=30000 + +# ── BTC price oracle ───────────────────────────────────────── +# Optional CoinGecko API key for higher rate limits. +# COINGECKO_API_KEY= + +# ── Replit (auto-set in Replit environment) ────────────────── +# REPLIT_DEV_DOMAIN= diff --git a/artifacts/api-server/src/index.ts b/artifacts/api-server/src/index.ts index 96dfc67..3e82a28 100644 --- a/artifacts/api-server/src/index.ts +++ b/artifacts/api-server/src/index.ts @@ -6,6 +6,7 @@ import { timmyIdentityService } from "./lib/timmy-identity.js"; import { startEngagementEngine } from "./lib/engagement.js"; import { relayAccountService } from "./lib/relay-accounts.js"; import { moderationService } from "./lib/moderation.js"; +import { lnbitsService } from "./lib/lnbits.js"; const rawPort = process.env["PORT"]; @@ -25,6 +26,16 @@ attachWebSocketServer(server); server.listen(port, () => { rootLogger.info("server started", { port }); rootLogger.info("timmy identity", { npub: timmyIdentityService.npub }); + + // Warn loudly in production if Lightning is not connected. + if (lnbitsService.stubMode && process.env["NODE_ENV"] === "production") { + rootLogger.warn( + "LNBITS_URL / LNBITS_API_KEY not set — running in STUB mode. " + + "Invoices are simulated in-memory. Set these env vars to connect to a real Lightning node. " + + "See infrastructure/lnd-init.sh for setup instructions.", + { lnbits_stub: true }, + ); + } const domain = process.env["REPLIT_DEV_DOMAIN"]; if (domain) { rootLogger.info("public url", { url: `https://${domain}/api/ui` }); diff --git a/artifacts/api-server/src/routes/health.ts b/artifacts/api-server/src/routes/health.ts index c6bae80..65c5bb5 100644 --- a/artifacts/api-server/src/routes/health.ts +++ b/artifacts/api-server/src/routes/health.ts @@ -2,6 +2,7 @@ import { Router, type IRouter, type Request, type Response } from "express"; import { db, jobs } from "@workspace/db"; import { sql } from "drizzle-orm"; import { makeLogger } from "../lib/logger.js"; +import { lnbitsService } from "../lib/lnbits.js"; const router: IRouter = Router(); const logger = makeLogger("health"); @@ -13,12 +14,24 @@ router.get("/healthz", async (_req: Request, res: Response) => { const rows = await db.select({ total: sql`cast(count(*) as int)` }).from(jobs); const jobsTotal = Number(rows[0]?.total ?? 0); const uptimeS = Math.floor((Date.now() - START_TIME) / 1000); - res.json({ status: "ok", uptime_s: uptimeS, jobs_total: jobsTotal }); + res.json({ + status: "ok", + uptime_s: uptimeS, + jobs_total: jobsTotal, + lnbits_stub: lnbitsService.stubMode, + lnbits_url: lnbitsService.stubMode ? null : process.env["LNBITS_URL"] ?? null, + }); } catch (err) { const message = err instanceof Error ? err.message : "Health check failed"; logger.error("healthz db query failed", { error: message }); const uptimeS = Math.floor((Date.now() - START_TIME) / 1000); - res.status(503).json({ status: "error", uptime_s: uptimeS, error: message }); + res.status(503).json({ + status: "error", + uptime_s: uptimeS, + error: message, + lnbits_stub: lnbitsService.stubMode, + lnbits_url: lnbitsService.stubMode ? null : process.env["LNBITS_URL"] ?? null, + }); } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1007a83..9c533ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,6 +625,10 @@ importers: version: 7.1.1 scripts: + dependencies: + nostr-tools: + specifier: ^2.23.3 + version: 2.23.3(typescript@5.9.3) devDependencies: '@types/node': specifier: 'catalog:'