re-vibecode
This commit is contained in:
+88
-47
@@ -1,77 +1,118 @@
|
||||
import { ZodError, type ZodSchema } from "zod";
|
||||
import { config } from "./config";
|
||||
import { HttpError, isHttpError } from "./errors";
|
||||
import { buildResponseHeaders } from "./security";
|
||||
import { HttpError } from "./errors";
|
||||
|
||||
export const jsonResponse = (request: Request, status: number, payload: unknown, init?: HeadersInit): Response =>
|
||||
const allowedOrigin = (request: Request): string | null => {
|
||||
const origin = request.headers.get("origin");
|
||||
if (!origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config.appOrigins.includes(origin.toLowerCase()) ? origin : null;
|
||||
};
|
||||
|
||||
const securityHeaders = (): Record<string, string> => ({
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Referrer-Policy": "no-referrer"
|
||||
});
|
||||
|
||||
const mergeHeaders = (request: Request, headers?: HeadersInit): Headers => {
|
||||
const result = new Headers(headers);
|
||||
const origin = allowedOrigin(request);
|
||||
|
||||
for (const [key, value] of Object.entries(securityHeaders())) {
|
||||
result.set(key, value);
|
||||
}
|
||||
|
||||
result.set("Vary", "Origin");
|
||||
|
||||
if (origin) {
|
||||
result.set("Access-Control-Allow-Origin", origin);
|
||||
result.set("Access-Control-Allow-Credentials", "true");
|
||||
result.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
result.set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const assertAllowedOrigin = (request: Request): void => {
|
||||
const origin = request.headers.get("origin");
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.appOrigins.includes(origin.toLowerCase())) {
|
||||
throw new HttpError(403, "Origin is not allowed.");
|
||||
}
|
||||
};
|
||||
|
||||
export const jsonResponse = (
|
||||
request: Request,
|
||||
status: number,
|
||||
payload: unknown,
|
||||
headers?: HeadersInit
|
||||
): Response =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: buildResponseHeaders(request, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
...init
|
||||
headers: mergeHeaders(request, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
...headers
|
||||
})
|
||||
});
|
||||
|
||||
export const emptyResponse = (request: Request, status = 204, init?: HeadersInit): Response =>
|
||||
export const emptyResponse = (request: Request, status = 204, headers?: HeadersInit): Response =>
|
||||
new Response(null, {
|
||||
status,
|
||||
headers: buildResponseHeaders(request, init)
|
||||
headers: mergeHeaders(request, headers)
|
||||
});
|
||||
|
||||
export const fileResponse = (request: Request, body: Blob | Uint8Array, contentType: string, init?: HeadersInit): Response =>
|
||||
new Response(body as BodyInit, {
|
||||
status: 200,
|
||||
headers: buildResponseHeaders(request, {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "private, max-age=300",
|
||||
...init
|
||||
})
|
||||
});
|
||||
export const optionsResponse = (request: Request): Response =>
|
||||
emptyResponse(request, 204);
|
||||
|
||||
export const parseJson = async <T>(request: Request, schema: { parse(data: unknown): T }): Promise<T> => {
|
||||
const contentType = request.headers.get("content-type") ?? "";
|
||||
if (!contentType.toLowerCase().includes("application/json")) {
|
||||
throw new HttpError(415, "Expected application/json request body.");
|
||||
}
|
||||
export const parseJson = async <T>(request: Request, schema: ZodSchema<T>): Promise<T> => {
|
||||
let body: unknown;
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await request.json();
|
||||
body = await request.json();
|
||||
} catch {
|
||||
throw new HttpError(400, "Malformed JSON request body.");
|
||||
throw new HttpError(400, "Request body must be valid JSON.");
|
||||
}
|
||||
|
||||
try {
|
||||
return schema.parse(payload);
|
||||
return schema.parse(body);
|
||||
} catch (error) {
|
||||
throw new HttpError(400, "Request validation failed.", { details: error });
|
||||
if (error instanceof ZodError) {
|
||||
throw new HttpError(400, error.issues[0]?.message ?? "Invalid request body.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const errorResponse = (request: Request, error: unknown): Response => {
|
||||
if (isHttpError(error)) {
|
||||
const retryAfterMs =
|
||||
typeof error.details === "object" &&
|
||||
error.details !== null &&
|
||||
"retryAfterMs" in error.details &&
|
||||
typeof (error.details as { retryAfterMs?: unknown }).retryAfterMs === "number"
|
||||
? Math.ceil(((error.details as { retryAfterMs: number }).retryAfterMs ?? 0) / 1000)
|
||||
: undefined;
|
||||
|
||||
return jsonResponse(
|
||||
request,
|
||||
error.status,
|
||||
{
|
||||
error: error.expose ? error.message : "An internal server error occurred."
|
||||
},
|
||||
retryAfterMs ? { "Retry-After": String(retryAfterMs) } : undefined
|
||||
);
|
||||
if (error instanceof HttpError) {
|
||||
return jsonResponse(request, error.status, {
|
||||
error: {
|
||||
message: error.message
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.nodeEnv !== "production") {
|
||||
console.error(error);
|
||||
if (error instanceof ZodError) {
|
||||
return jsonResponse(request, 400, {
|
||||
error: {
|
||||
message: error.issues[0]?.message ?? "Invalid request."
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
return jsonResponse(request, 500, {
|
||||
error: "An internal server error occurred."
|
||||
error: {
|
||||
message: "Unexpected server error."
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user