Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #64.
This commit is contained in:
59
.env.example
Normal file
59
.env.example
Normal file
@@ -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://<node-name>.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=<auto-set by Replit>
|
||||||
@@ -6,6 +6,7 @@ import { timmyIdentityService } from "./lib/timmy-identity.js";
|
|||||||
import { startEngagementEngine } from "./lib/engagement.js";
|
import { startEngagementEngine } from "./lib/engagement.js";
|
||||||
import { relayAccountService } from "./lib/relay-accounts.js";
|
import { relayAccountService } from "./lib/relay-accounts.js";
|
||||||
import { moderationService } from "./lib/moderation.js";
|
import { moderationService } from "./lib/moderation.js";
|
||||||
|
import { lnbitsService } from "./lib/lnbits.js";
|
||||||
|
|
||||||
const rawPort = process.env["PORT"];
|
const rawPort = process.env["PORT"];
|
||||||
|
|
||||||
@@ -25,6 +26,16 @@ attachWebSocketServer(server);
|
|||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
rootLogger.info("server started", { port });
|
rootLogger.info("server started", { port });
|
||||||
rootLogger.info("timmy identity", { npub: timmyIdentityService.npub });
|
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"];
|
const domain = process.env["REPLIT_DEV_DOMAIN"];
|
||||||
if (domain) {
|
if (domain) {
|
||||||
rootLogger.info("public url", { url: `https://${domain}/api/ui` });
|
rootLogger.info("public url", { url: `https://${domain}/api/ui` });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router, type IRouter, type Request, type Response } from "express";
|
|||||||
import { db, jobs } from "@workspace/db";
|
import { db, jobs } from "@workspace/db";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { makeLogger } from "../lib/logger.js";
|
import { makeLogger } from "../lib/logger.js";
|
||||||
|
import { lnbitsService } from "../lib/lnbits.js";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
const logger = makeLogger("health");
|
const logger = makeLogger("health");
|
||||||
@@ -13,12 +14,24 @@ router.get("/healthz", async (_req: Request, res: Response) => {
|
|||||||
const rows = await db.select({ total: sql<number>`cast(count(*) as int)` }).from(jobs);
|
const rows = await db.select({ total: sql<number>`cast(count(*) as int)` }).from(jobs);
|
||||||
const jobsTotal = Number(rows[0]?.total ?? 0);
|
const jobsTotal = Number(rows[0]?.total ?? 0);
|
||||||
const uptimeS = Math.floor((Date.now() - START_TIME) / 1000);
|
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) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : "Health check failed";
|
const message = err instanceof Error ? err.message : "Health check failed";
|
||||||
logger.error("healthz db query failed", { error: message });
|
logger.error("healthz db query failed", { error: message });
|
||||||
const uptimeS = Math.floor((Date.now() - START_TIME) / 1000);
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user