import type { FastifyPluginAsync } from "fastify"; import { z } from "zod"; type RateLimitRouteOptions = { config: { rateLimit: { max: number; timeWindow: number; keyGenerator?: (req: any) => string; }; }; }; type SiteAccessRoutesOptions = { underConstructionEnabled: boolean; breakGlassVerifyEnabled: boolean; breakGlassVerifyCode: string | null; siteAccessExpectedToken: string | null; cookieDomain?: string; secureCookie: boolean; siteAccessCookieName: string; siteAccessMaxAgeSeconds: number; authRateLimit: RateLimitRouteOptions; mutationRateLimit: RateLimitRouteOptions; hasSiteAccessBypass: (req: { cookies?: Record }) => boolean; safeEqual: (a: string, b: string) => boolean; }; const siteAccessRoutes: FastifyPluginAsync = async (app, opts) => { app.get("/site-access/status", async (req) => { if (!opts.underConstructionEnabled) { return { ok: true, enabled: false, unlocked: true }; } return { ok: true, enabled: true, unlocked: opts.hasSiteAccessBypass(req), }; }); app.post("/site-access/unlock", opts.authRateLimit, async (req, reply) => { const Body = z.object({ code: z.string().min(1).max(512), }); const parsed = Body.safeParse(req.body); if (!parsed.success) { return reply.code(400).send({ ok: false, code: "INVALID_PAYLOAD", message: "Invalid payload" }); } if (!opts.underConstructionEnabled) { return { ok: true, enabled: false, unlocked: true }; } if (!opts.breakGlassVerifyEnabled || !opts.siteAccessExpectedToken) { return reply.code(503).send({ ok: false, code: "UNDER_CONSTRUCTION_MISCONFIGURED", message: "Under-construction access is not configured.", }); } if (!opts.breakGlassVerifyCode || !opts.safeEqual(parsed.data.code, opts.breakGlassVerifyCode)) { return reply.code(401).send({ ok: false, code: "INVALID_ACCESS_CODE", message: "Invalid access code.", }); } reply.setCookie(opts.siteAccessCookieName, opts.siteAccessExpectedToken, { httpOnly: true, sameSite: "lax", secure: opts.secureCookie, path: "/", maxAge: opts.siteAccessMaxAgeSeconds, ...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}), }); return { ok: true, enabled: true, unlocked: true }; }); app.post("/site-access/lock", opts.mutationRateLimit, async (_req, reply) => { reply.clearCookie(opts.siteAccessCookieName, { path: "/", httpOnly: true, sameSite: "lax", secure: opts.secureCookie, ...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}), }); return { ok: true, enabled: opts.underConstructionEnabled, unlocked: false }; }); }; export default siteAccessRoutes;