Repository Public
1import { apiUser, sameOrigin, type Role, type User } from "./auth";2import { rateLimit } from "./ratelimit";3import { OpError } from "./studio-ops";45const noStore = { "Cache-Control": "no-store" };6export const out = (data: unknown, status = 200) => Response.json(data, { status, headers: noStore });78/**9 * Wrapper for /api/studio routes: session + role, same-origin check on mutations (CSRF), per-user rate limit,10 * and uniform JSON errors. Authorization is always decided on the server.11 */12export async function studio(req: Request, min: Role, fn: (user: User, body: Record<string, unknown>) => Promise<Response>): Promise<Response> {13 const mutating = req.method !== "GET";14 if (mutating && !sameOrigin(req)) return out({ error: "bad_origin" }, 403);15 const auth = await apiUser(min);16 if (auth.error) return auth.error;17 if (!(await rateLimit(`api:${auth.user.id}:${mutating ? "w" : "r"}`, mutating ? 60 : 600, 60))) return out({ error: "rate_limited" }, 429);18 let body: Record<string, unknown> = {};19 if (mutating) {20 const text = await req.text();21 if (text) { try { body = JSON.parse(text); } catch { return out({ error: "invalid_json" }, 400); } }22 if (!body || typeof body !== "object" || Array.isArray(body)) return out({ error: "invalid_body" }, 400);23 }24 try { return await fn(auth.user, body); }25 catch (e) {26 if (e instanceof OpError) return out({ error: e.message }, e.status);27 return out({ error: "internal_error" }, 500);28 }29}30export const isUuid = (s: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s);31