import type { IncomingMessage } from 'node:http'; export const MAX_CURSOR_LENGTH = 1_024; export const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/; export const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/; export type ParserFailure = ( status: number, code: string, message: string, ) => never; export type ParserMessages = Readonly<{ invalidRequest: string; invalidRevision: string; requestTooLarge: string; }>; export function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } export function boundedString(value: unknown, maximum: number): string | null { if (typeof value !== 'string') return null; return value && value.length <= maximum ? value : null; } export async function readBoundedJson( req: IncomingMessage, maxBytes: number, fail: ParserFailure, messages: ParserMessages, ): Promise> { const declared = req.headers['content-length']; const declaredValue = Array.isArray(declared) ? declared[0] : declared; if (declaredValue && /^\d+$/.test(declaredValue) && Number(declaredValue) > maxBytes) { return fail(413, 'request_too_large', messages.requestTooLarge); } const chunks: Buffer[] = []; let size = 0; for await (const chunk of req) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); size += buffer.byteLength; if (size > maxBytes) { return fail(413, 'request_too_large', messages.requestTooLarge); } chunks.push(buffer); } try { const value = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown; if (!isRecord(value)) throw new Error('object required'); return value; } catch { return fail(422, 'invalid_request', messages.invalidRequest); } } export function requireExactKeys( value: Record, keys: readonly string[], fail: ParserFailure, message: string, ): void { const expected = new Set(keys); if (Object.keys(value).some((key) => !expected.has(key)) || keys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) { fail(422, 'invalid_request', message); } } function decodeSegment( value: string, maximum: number, pattern: RegExp, fail: ParserFailure, message: string, ): string { let decoded: string; try { decoded = decodeURIComponent(value); } catch { return fail(422, 'invalid_request', message); } if (decoded.length > maximum || !pattern.test(decoded)) { return fail(422, 'invalid_request', message); } return decoded; } export function parseCollection( value: string, fail: ParserFailure, message: string, ): string { return decodeSegment(value, 48, COLLECTION_PATTERN, fail, message); } export function parseDocumentId( value: string, fail: ParserFailure, message: string, ): string { const documentId = decodeSegment(value, 128, DOCUMENT_ID_PATTERN, fail, message); if (documentId === '.' || documentId === '..') { return fail(422, 'invalid_request', message); } return documentId; } export function requireQueryKeys( url: URL, allowed: readonly string[], fail: ParserFailure, message: string, ): void { const accepted = new Set(allowed); if ([...url.searchParams.keys()].some((key) => !accepted.has(key))) { fail(422, 'invalid_request', message); } } export function parseLimit( url: URL, fail: ParserFailure, message: string, ): number | undefined { const values = url.searchParams.getAll('limit'); if (values.length === 0) return undefined; if (values.length !== 1 || !/^[1-9]\d*$/.test(values[0])) { return fail(422, 'invalid_request', message); } const limit = Number(values[0]); if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) { return fail(422, 'invalid_request', message); } return limit; } export function parseCursor( url: URL, fail: ParserFailure, message: string, ): string | undefined { const values = url.searchParams.getAll('cursor'); if (values.length === 0) return undefined; const cursor = values.length === 1 ? boundedString(values[0], MAX_CURSOR_LENGTH) : null; if (!cursor) return fail(422, 'invalid_request', message); return cursor; } export function parseIfMatch( req: IncomingMessage, fail: ParserFailure, message: string, ): number | undefined { const value = req.headers['if-match']; if (value === undefined) return undefined; if (Array.isArray(value) || !/^"[1-9]\d*"$/.test(value)) { return fail(422, 'invalid_revision', message); } const revision = Number(value.slice(1, -1)); if (!Number.isSafeInteger(revision) || revision < 1) { return fail(422, 'invalid_revision', message); } return revision; } export function parseData( value: unknown, fail: ParserFailure, message: string, ): Record { if (!isRecord(value)) { return fail(422, 'invalid_request', message); } return value; }