Files
homarr/apps/nextjs/src/app/api/custom-widgets/configuration-request/body.ts
T

59 lines
1.9 KiB
TypeScript

import { z } from "zod/v4";
export const MAX_CONFIGURATION_REQUEST_BODY_BYTES = 24 * 1024;
const configurationRequestBodySchema = z
.strictObject({
baseUrl: z.string().optional(),
integrationId: z.string().min(1).max(100).optional(),
networkScope: z.enum(["public", "private", "loopback"]).optional(),
secrets: z.record(z.string(), z.string()),
})
.refine(
(value) => Boolean(value.integrationId) || (value.baseUrl !== undefined && value.networkScope !== undefined),
"Provide an integration or HTTP connection",
);
export async function readConfigurationRequestBody(
request: Request,
): Promise<
{ status: "ok"; data: z.infer<typeof configurationRequestBodySchema> } | { status: "invalid" | "too-large" }
> {
const contentLength = request.headers.get("content-length");
if (contentLength && Number(contentLength) > MAX_CONFIGURATION_REQUEST_BODY_BYTES) {
return { status: "too-large" };
}
if (!request.body) return { status: "invalid" };
const reader = request.body.getReader();
const chunks: Uint8Array[] = [];
let byteLength = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
byteLength += value.byteLength;
if (byteLength > MAX_CONFIGURATION_REQUEST_BODY_BYTES) {
await reader.cancel();
return { status: "too-large" };
}
chunks.push(value);
}
} catch {
return { status: "invalid" };
}
const bytes = new Uint8Array(byteLength);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
try {
const candidate = JSON.parse(new TextDecoder().decode(bytes)) as unknown;
const result = configurationRequestBodySchema.safeParse(candidate);
return result.success ? { status: "ok", data: result.data } : { status: "invalid" };
} catch {
return { status: "invalid" };
}
}