Files
gunshiz 87e6bcd96f
CI / Verify and audit (push) Successful in 2m33s
CI / Build, scan and deploy immutable images (push) Failing after 1m29s
feat : 6 astra improve it
2026-09-22 18:28:18 +07:00

70 lines
2.7 KiB
TypeScript

export class HttpError extends Error {
constructor(readonly status: number, message: string, readonly retryAfter?: number) {
super(message);
this.name = "HttpError";
}
}
export function securityLog(event: string, details: { actorId?: string; targetId?: string; status?: number } = {}) {
console.info(JSON.stringify({ type: "security", event, ...details, at: new Date().toISOString() }));
}
export function errorResponse(cause: unknown): Response {
const error = cause instanceof HttpError ? cause : new HttpError(503, "service-unavailable");
if (!(cause instanceof HttpError)) securityLog("dependency-unavailable", { status: 503 });
return Response.json({ error: error.message }, {
status: error.status,
headers: {
"Cache-Control": "no-store",
...(error.retryAfter === undefined ? {} : { "Retry-After": String(error.retryAfter) }),
},
});
}
/** Mutations must come from the configured application, never the request Host. */
export function requireSameOrigin(request: Request) {
const configured = process.env.BETTER_AUTH_URL;
if (!configured) throw new HttpError(503, "origin-not-configured");
const origin = request.headers.get("origin");
if (origin !== new URL(configured).origin || request.headers.get("sec-fetch-site") === "cross-site") {
throw new HttpError(403, "cross-origin-request");
}
}
/** Count actual streamed bytes as well as checking the untrusted Content-Length. */
export function boundedBody(request: Request, maximum: number): Response {
const length = request.headers.get("content-length");
if (length && (!/^\d+$/u.test(length) || Number(length) > maximum)) {
throw new HttpError(413, "body-too-large");
}
let bytes = 0;
const stream = request.body?.pipeThrough(new TransformStream<Uint8Array, Uint8Array>({
transform(chunk, controller) {
bytes += chunk.byteLength;
if (bytes > maximum) throw new HttpError(413, "body-too-large");
controller.enqueue(chunk);
},
}));
return new Response(stream ?? null, { headers: request.headers });
}
export async function readJson(request: Request, maximum = 16 * 1024): Promise<unknown> {
if (request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
throw new HttpError(415, "expected-json");
}
try {
return await boundedBody(request, maximum).json();
} catch (cause) {
if (cause instanceof HttpError) throw cause;
throw new HttpError(400, "invalid-json");
}
}
let uploads = 0;
/** Acquire before buffering multipart bodies or decoding images. */
export async function withUploadSlot<T>(task: () => Promise<T>): Promise<T> {
if (uploads >= 2) throw new HttpError(429, "uploads-busy", 5);
uploads += 1;
try { return await task(); } finally { uploads -= 1; }
}