import "server-only";
import { NextResponse, type NextRequest } from "next/server";
import { z } from "zod";
import { getSession, assertSameOrigin, AuthError } from "./auth";
type Handler
= (req: NextRequest, ctx: { params: P; session: { id: string } }) => Promise;
/** Wraps a route handler: requires a valid session, checks Origin on mutations, maps errors to JSON. */
export function guarded>(fn: Handler
) {
return async (req: NextRequest, ctx: { params: Promise
}): Promise => {
try {
const session = await getSession();
if (!session) throw new AuthError();
if (req.method !== "GET" && req.method !== "HEAD") await assertSameOrigin();
return await fn(req, { params: await ctx.params, session });
} catch (e) { return errorResponse(e); }
};
}
export function errorResponse(e: unknown): Response {
const err = e as { status?: number; message?: string; issues?: unknown };
if (e instanceof z.ZodError) return NextResponse.json({ error: "RequĂȘte invalide", issues: e.issues }, { status: 400 });
const status = typeof err.status === "number" && err.status >= 400 && err.status < 600 ? err.status : 500;
if (status >= 500) console.error(e);
return NextResponse.json({ error: err.message ?? "Erreur interne" }, { status });
}
export const json = (data: unknown, init?: ResponseInit) => NextResponse.json(data, init);
export async function body(req: NextRequest, schema: T): Promise> {
return schema.parse(await req.json());
}
/** Server-Sent Events response. `run` receives an emit function and must resolve when finished. */
export function sse(run: (emit: (event: string, data: unknown) => void, signal: AbortSignal) => Promise, signal: AbortSignal): Response {
const enc = new TextEncoder();
let closed = false;
const stream = new ReadableStream({
start(controller) {
const emit = (event: string, data: unknown) => { if (closed) return; try { controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)); } catch { closed = true; } };
const ping = setInterval(() => { if (!closed) try { controller.enqueue(enc.encode(`: ping\n\n`)); } catch { closed = true; } }, 15_000);
run(emit, signal).catch(e => emit("error", { message: (e as Error).message ?? String(e) })).finally(() => { clearInterval(ping); closed = true; try { controller.close(); } catch { /* already closed */ } });
},
cancel() { closed = true; },
});
return new Response(stream, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" } });
}