Build API v2

This commit is contained in:
NeilClack
2026-08-07 14:06:20 -05:00
parent b2091deae0
commit 64f7070ad7
50 changed files with 25534 additions and 26502 deletions
+64
View File
@@ -0,0 +1,64 @@
import { error } from '@sveltejs/kit';
export const MAX_JSON_BODY_BYTES = 64 * 1024;
export function jsonOk(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'content-type': 'application/json' }
});
}
export function jsonError(
status: number,
message: string,
headers?: Record<string, string>
): Response {
return new Response(JSON.stringify({ message }), {
status,
headers: { 'content-type': 'application/json', ...headers }
});
}
/** Read and parse a JSON request body with a size cap. Throws 413 for
* oversized bodies and 400 for invalid JSON / wrong content type. */
export async function readJsonBody(request: Request): Promise<unknown> {
const contentType = request.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
throw error(400, 'content-type must be application/json');
}
const contentLength = Number(request.headers.get('content-length') ?? 0);
if (contentLength > MAX_JSON_BODY_BYTES) {
throw error(413, 'request body too large');
}
const raw = await request.text();
if (raw.length === 0) {
throw error(400, 'request body is required');
}
if (raw.length > MAX_JSON_BODY_BYTES) {
throw error(413, 'request body too large');
}
try {
return JSON.parse(raw);
} catch {
throw error(400, 'invalid JSON body');
}
}
/** Build a 429 response with Retry-After set in seconds. */
export function rateLimited(retryAfterMs: number): Response {
const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
return jsonError(429, 'rate limit exceeded', { 'retry-after': String(retryAfterSeconds) });
}
export function clientIp(request: Request): string {
const fwd = request.headers.get('x-forwarded-for');
if (fwd) {
const first = fwd.split(',')[0]?.trim();
if (first) return first;
}
return request.headers.get('x-real-ip') ?? 'unknown';
}