import { Elysia, t } from "elysia"; import { requireAuth } from "./auth"; import sql from "../db"; import { validateMonitorUrl } from "../utils/ssrf"; import { getPlanLimits } from "../utils/plans"; const MonitorBody = t.Object({ name: t.String({ maxLength: 200, description: "Human-readable name" }), url: t.String({ format: "uri", maxLength: 2048, description: "URL to check" }), method: t.Optional(t.String({ default: "GET", description: "HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD" })), request_headers: t.Optional(t.Any({ description: "Request headers as key-value object" })), request_body: t.Optional(t.Nullable(t.String({ maxLength: 65536, description: "Request body for POST/PUT/PATCH (max 64KB)" }))), timeout_ms: t.Optional(t.Number({ minimum: 1000, maximum: 60000, default: 10000, description: "Request timeout in ms" })), interval_s: t.Optional(t.Number({ minimum: 2, default: 30, description: "Check interval in seconds (minimum 2)" })), query: t.Optional(t.Any({ description: "PingQL query — filter conditions for up/down" })), regions: t.Optional(t.Array(t.String(), { description: "Regions to run checks from. Empty array = all regions." })), }); export const monitors = new Elysia({ prefix: "/monitors" }) .use(requireAuth) // List monitors .get("/", async ({ accountId }) => { return sql`SELECT * FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC`; }, { detail: { summary: "List monitors", tags: ["monitors"] } }) // Create monitor .post("/", async ({ accountId, plan, body, error }) => { const limits = getPlanLimits(plan); // Enforce monitor count limit const [{ count }] = await sql`SELECT COUNT(*)::int as count FROM monitors WHERE account_id = ${accountId}`; if (count >= limits.maxMonitors) { return error(403, { error: `Plan limit reached: ${limits.maxMonitors} monitors (${plan}). Upgrade to create more.` }); } // Enforce minimum interval for plan const interval = body.interval_s ?? 30; if (interval < limits.minIntervalS) { return error(400, { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }); } // SSRF protection const ssrfError = await validateMonitorUrl(body.url); if (ssrfError) return error(400, { error: ssrfError }); const regions = body.regions ?? []; const [monitor] = await sql` INSERT INTO monitors (account_id, name, url, method, request_headers, request_body, timeout_ms, interval_s, query, regions) VALUES ( ${accountId}, ${body.name}, ${body.url}, ${(body.method ?? 'GET').toUpperCase()}, ${body.request_headers ? sql.json(body.request_headers) : null}, ${body.request_body ?? null}, ${body.timeout_ms ?? 10000}, ${interval}, ${body.query ? sql.json(body.query) : null}, ${sql.array(regions)} ) RETURNING * `; return monitor; }, { body: MonitorBody, detail: { summary: "Create monitor", tags: ["monitors"] } }) // Get monitor + recent status .get("/:id", async ({ accountId, params, error }) => { const [monitor] = await sql` SELECT * FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} `; if (!monitor) return error(404, { error: "Not found" }); const results = await sql` SELECT * FROM pings WHERE monitor_id = ${params.id} ORDER BY checked_at DESC LIMIT 100 `; return { ...monitor, results }; }, { detail: { summary: "Get monitor with results", tags: ["monitors"] } }) // Update monitor .patch("/:id", async ({ accountId, plan, params, body, error }) => { // Enforce minimum interval for plan if (body.interval_s != null) { const limits = getPlanLimits(plan); if (body.interval_s < limits.minIntervalS) { return error(400, { error: `Minimum interval for ${plan} plan is ${limits.minIntervalS}s` }); } } // SSRF protection on URL change if (body.url) { const ssrfError = await validateMonitorUrl(body.url); if (ssrfError) return error(400, { error: ssrfError }); } const [monitor] = await sql` UPDATE monitors SET name = COALESCE(${body.name ?? null}, name), url = COALESCE(${body.url ?? null}, url), method = COALESCE(${body.method ? body.method.toUpperCase() : null}, method), request_headers = COALESCE(${body.request_headers ? sql.json(body.request_headers) : null}, request_headers), request_body = COALESCE(${body.request_body ?? null}, request_body), timeout_ms = COALESCE(${body.timeout_ms ?? null}, timeout_ms), interval_s = COALESCE(${body.interval_s ?? null}, interval_s), query = COALESCE(${body.query ? sql.json(body.query) : null}, query), regions = COALESCE(${body.regions ? sql.array(body.regions) : null}, regions) WHERE id = ${params.id} AND account_id = ${accountId} RETURNING * `; if (!monitor) return error(404, { error: "Not found" }); return monitor; }, { body: t.Partial(MonitorBody), detail: { summary: "Update monitor", tags: ["monitors"] } }) // Delete monitor .delete("/:id", async ({ accountId, params, error }) => { const [deleted] = await sql` DELETE FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id `; if (!deleted) return error(404, { error: "Not found" }); return { deleted: true }; }, { detail: { summary: "Delete monitor", tags: ["monitors"] } }) // Toggle enabled .post("/:id/toggle", async ({ accountId, params, error }) => { const [monitor] = await sql` UPDATE monitors SET enabled = NOT enabled WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id, enabled `; if (!monitor) return error(404, { error: "Not found" }); return monitor; }, { detail: { summary: "Toggle monitor on/off", tags: ["monitors"] } }) // Check history .get("/:id/pings", async ({ accountId, params, query, error }) => { const [monitor] = await sql` SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} `; if (!monitor) return error(404, { error: "Not found" }); const limit = Math.min(Number(query.limit) || 100, 1000); return sql` SELECT * FROM pings WHERE monitor_id = ${params.id} ORDER BY checked_at DESC LIMIT ${limit} `; }, { detail: { summary: "Get ping history", tags: ["monitors"] } });