import { createHmac } from "node:crypto"; import { auth } from "@/lib/auth"; import { resolveWorkspace } from "@/server/lib/resolve-workspace"; import { z } from "zod"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const input = z.object({ workspace: z.string().min(1).max(200) }).strict(); const noStore = { "Cache-Control": "no-store" }; function trustedOrigin(request: Request): string | null { // Docker's request URL may be internal. Pin production to deployment configuration, // not Host/forwarded headers that a caller could supply outside the trusted proxy. const configured = process.env.AUTH_URL || process.env.NEXTAUTH_URL; if (!configured && process.env.NODE_ENV === "production") return null; const parsed = z .string() .url() .safeParse(configured || request.url); if (!parsed.success) return null; const url = new URL(parsed.data); if ( !["https:", "http:"].includes(url.protocol) || url.username || url.password ) return null; return url.origin; } export async function POST(request: Request): Promise { // A browser session cookie must never be usable by a foreign origin to mint tickets. const origin = request.headers.get("origin"); if (!origin || origin !== trustedOrigin(request)) return new Response(null, { status: 403, headers: noStore }); const session = await auth(); if (!session?.user?.id) return new Response(null, { status: 401, headers: noStore }); const secret = process.env.CHAT_TICKET_SECRET?.trim(); if (!secret || secret.length < 32) return new Response(null, { status: 503, headers: noStore }); let body: unknown; try { body = await request.json(); } catch { return new Response(null, { status: 400, headers: noStore }); } const parsed = input.safeParse(body); if (!parsed.success) return new Response(null, { status: 400, headers: noStore }); try { const workspace = await resolveWorkspace({ handle: parsed.data.workspace, userId: session.user.id, }); const payload = Buffer.from( JSON.stringify({ iss: "echodo-web", aud: "echodo-chat-v1", scope: "chat:subscribe", sub: session.user.id, workspaceId: workspace.id, exp: Math.floor(Date.now() / 1000) + 60, }), ).toString("base64url"); const signature = createHmac("sha256", secret) .update(payload) .digest("base64url"); return Response.json( { ticket: `${payload}.${signature}` }, { headers: noStore }, ); } catch { // Do not distinguish nonexistent workspaces from denied or failed membership checks. return new Response(null, { status: 404, headers: noStore }); } }