diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts index 7a66834..bd272cb 100644 --- a/artifacts/api-server/src/app.ts +++ b/artifacts/api-server/src/app.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "url"; import router from "./routes/index.js"; import bootstrapRouter from "./routes/bootstrap.js"; // New: Bootstrap routes import adminRelayPanelRouter from "./routes/admin-relay-panel.js"; +import relayPolicyRouter from "./routes/relay-policy.js"; import { requestIdMiddleware } from "./middlewares/request-id.js"; import { responseTimeMiddleware } from "./middlewares/response-time.js"; @@ -57,6 +58,7 @@ app.use(responseTimeMiddleware); app.use("/api", router); app.use("/api", bootstrapRouter); // New: Mount bootstrap routes +app.use("/api", relayPolicyRouter); // ── Relay admin panel at /admin/relay ──────────────────────────────────────── // Served outside /api so the URL is clean: /admin/relay (not /api/admin/relay). diff --git a/artifacts/api-server/src/routes/relay-policy.ts b/artifacts/api-server/src/routes/relay-policy.ts new file mode 100644 index 0000000..f775a0f --- /dev/null +++ b/artifacts/api-server/src/routes/relay-policy.ts @@ -0,0 +1,79 @@ +import { type Express, Router } from "express"; +import { z } from "zod"; +import { Status } from "../lib/http.js"; +import { rootLogger } from "../lib/logger.js"; + +const router = Router(); +const log = rootLogger.child({ service: "relay-policy" }); + +// ── Auth ────────────────────────────────────────────────────────────────────── + +const RELAY_POLICY_SECRET = process.env["RELAY_POLICY_SECRET"] ?? ""; + +if (!RELAY_POLICY_SECRET) { + log.warn("RELAY_POLICY_SECRET is not set — /api/relay/policy will be unauthenticated!"); +} + +function isAuthenticated(req: Express.Request): boolean { + if (!RELAY_POLICY_SECRET) { + return true; // No secret configured, so no auth. + } + const authz = req.headers["authorization"]; + if (!authz) { + return false; + } + const [scheme, token] = authz.split(" "); + if (scheme !== "Bearer" || token !== RELAY_POLICY_SECRET) { + return false; + } + return true; +} + +// ── POST /api/relay/policy ──────────────────────────────────────────────────── + +const relayPolicyRequestSchema = z.object({ + event: z.object({ + id: z.string(), + pubkey: z.string(), + kind: z.number(), + created_at: z.number(), + tags: z.array(z.array(z.string())), + content: z.string(), + sig: z.string(), + }), + receivedAt: z.number(), + sourceType: z.string(), + sourceInfo: z.string(), +}); + +type StrfryAction = "accept" | "reject" | "shadowReject"; + +router.post("/relay/policy", (req, res) => { + if (!isAuthenticated(req)) { + return res.status(Status.UNAUTHORIZED).json({ + action: "reject", + msg: "unauthorized", + }); + } + + const parse = relayPolicyRequestSchema.safeParse(req.body); + if (!parse.success) { + log.warn("invalid /relay/policy request", { error: parse.error.format() }); + return res.status(Status.BAD_REQUEST).json({ + action: "reject", + msg: "invalid request", + }); + } + + const eventId = parse.data.event.id; + + // Bootstrap state: reject everything. + // This will be extended by whitelist + moderation tasks. + const action: StrfryAction = "reject"; + const msg = "bootstrapped: all events rejected"; + + log.info("policy decision", { eventId: eventId.slice(0, 8), action, msg }); + res.json({ id: eventId, action, msg }); +}); + +export default router;