1import type { NextFunction, Request, Response } from "express";
2import { v4 as uuid } from "uuid";
3import { API_PREFIX, MAX_WHITEBOARD_BODY_BYTES } from "../constants.js";
4
5
16
17const CANVAS_SAVE_PATH = new RegExp(
18 `^/${API_PREFIX.replace(/\//g, "\\/")}/pages/[^/]+/whiteboard$`,
19);
20
21function envelope(req: Request, res: Response, code: string, status: number, message: string): void {
22 const requestId = (req.headers["x-request-id"] as string | undefined) ?? uuid();
23 req.id = requestId;
24 res.setHeader("x-request-id", requestId);
25 res.status(status).json({ error: { code, message, requestId } });
26}
27
28export function whiteboardBodyParser(req: Request, res: Response, next: NextFunction): void {
29 if (req.method !== "PUT" || !CANVAS_SAVE_PATH.test(req.path)) {
30 next();
31 return;
32 }
33
34 const declared = Number(req.headers["content-length"] ?? 0);
35 if (declared > MAX_WHITEBOARD_BODY_BYTES) {
36 req.resume();
37 envelope(req, res, "PAYLOAD_TOO_LARGE", 413, `Whiteboard canvas exceeds the ${MAX_WHITEBOARD_BODY_BYTES} byte limit.`);
38 return;
39 }
40 const chunks: Buffer[] = [];
41 let size = 0;
42 let done = false;
43 const finish = (err?: unknown): void => {
44 if (done) return;
45 done = true;
46 if (err) {
47 next(err);
48 return;
49 }
50 const raw = Buffer.concat(chunks);
51 req.rawBody = raw;
52 try {
53 req.body = raw.length > 0 ? JSON.parse(raw.toString("utf8")) : null;
54 } catch {
55 envelope(req, res, "VALIDATION_ERROR", 400, "Request body must be valid JSON.");
56 return;
57 }
58 (req as unknown as Record<string, unknown>)._body = true;
59 next();
60 };
61 req.on("data", (chunk: Buffer) => {
62 size += chunk.length;
63 if (size > MAX_WHITEBOARD_BODY_BYTES) {
64 done = true;
65 req.resume();
66 envelope(req, res, "PAYLOAD_TOO_LARGE", 413, `Whiteboard canvas exceeds the ${MAX_WHITEBOARD_BODY_BYTES} byte limit.`);
67 return;
68 }
69 chunks.push(chunk);
70 });
71 req.on("end", () => finish());
72 req.on("error", (err) => finish(err));
73}
74