import { open } from 'node:fs/promises'; import { isDeepStrictEqual } from 'node:util'; import { z } from 'zod'; const MAX_BYTES = 1_048_576; const MAX_ROWS = 1_000; const MAX_LINE_BYTES = 16_384; const drawDate = z.iso.date(); const timestamp = z.iso.datetime({ offset: true }); const sourceRole = z.enum(['official', 'verified', 'observer', 'fallback', 'manual', 'unknown']); const nonempty = z .string() .min(1) .max(512) .refine((value) => value.trim() === value); const digits = z .array(z.string().regex(/^[0-9]$/)) .min(3) .max(5); const rowSchema = z .object({ game_code: z .string() .regex(/^[a-z0-9]+(?:-[a-z0-9]+)+$/) .max(120), name: nonempty, country: z.string().regex(/^[A-Z]{2,3}$/), state: nonempty, state_code: z .string() .regex(/^[A-Z]{2}$/) .optional(), ball_count: z.union([z.literal(3), z.literal(4), z.literal(5)]), draw_date: drawDate, numbers: digits, numbers_formatted: z.string(), numbers_string: z.string().optional(), bonus_numbers: z.array(z.never()).optional(), bonus_numbers_formatted: z.literal('').nullable().optional(), status: nonempty, source: nonempty.optional(), fetched_at: timestamp.optional(), inserted_at: timestamp.nullable().optional(), source_confidence: z.object({ level: sourceRole, label: nonempty, policy: nonempty.optional(), source: nonempty.optional() }), provenance: z.object({ label: nonempty, role: sourceRole, display: nonempty, source: nonempty.optional(), provider: nonempty.optional() }), correction: z .object({ label: nonempty, description: nonempty, previous_numbers: digits.optional(), previous_numbers_formatted: z.string().optional(), version_count: z.number().int().min(1).optional(), corrected_at: timestamp.optional(), corrected_by: nonempty.optional(), reason: nonempty.optional() }) .optional() }) .superRefine((row, context) => { if ( row.numbers.length !== row.ball_count || row.numbers_formatted !== row.numbers.join('-') || (row.numbers_string !== undefined && row.numbers_string !== row.numbers_formatted) ) { context.addIssue({ code: 'custom', message: 'ball_count or numbers_formatted does not match digit strings' }); } const correction = row.correction; if ( correction?.previous_numbers && (correction.previous_numbers.length !== row.ball_count || correction.previous_numbers_formatted !== correction.previous_numbers.join('-')) ) { context.addIssue({ code: 'custom', message: 'correction digits do not match ball_count or formatted value' }); } }); export type EvaluatedDraw = z.infer; function assertBoundedJson(root: unknown, lineNumber: number) { const pending = [{ value: root, depth: 0 }]; let nodes = 0; while (pending.length) { const { value, depth } = pending.pop()!; if (++nodes > 2_048 || depth > 8) throw new Error(`Line ${lineNumber}: JSON structure exceeds limits`); if (value !== null && typeof value === 'object') { const children = Object.values(value); if (children.length > 128) throw new Error(`Line ${lineNumber}: JSON structure exceeds limits`); for (const child of children) pending.push({ value: child, depth: depth + 1 }); } } } export function evaluateJsonl(input: string): { rows: EvaluatedDraw[]; duplicates: number } { if (Buffer.byteLength(input, 'utf8') > MAX_BYTES) throw new Error('Input exceeds 1 MiB limit'); const lines = input.trimEnd().split('\n'); if (lines.length === 0 || (lines.length === 1 && !lines[0])) throw new Error('Input is empty'); if (lines.length > MAX_ROWS) throw new Error('Input exceeds 1000-row limit'); const seen = new Map(); let duplicates = 0; for (const [index, line] of lines.entries()) { const lineNumber = index + 1; if (!line.trim() || Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) { throw new Error(`Line ${lineNumber}: blank or over 16 KiB`); } let raw: unknown; try { raw = JSON.parse(line); } catch { throw new Error(`Line ${lineNumber}: invalid JSON`); } assertBoundedJson(raw, lineNumber); if (JSON.stringify(raw) !== line) { throw new Error(`Line ${lineNumber}: JSON must be compact and round-trip unchanged (duplicate keys are not accepted)`); } const parsed = rowSchema.safeParse(raw); if (!parsed.success) { throw new Error( `Line ${lineNumber}: invalid public draw (${parsed.error.issues.map((issue) => issue.path.join('.') || issue.message).join(', ')})` ); } const row = parsed.data; const key = `${row.game_code}\0${row.draw_date}`; const prior = seen.get(key); if (prior) { if (!isDeepStrictEqual(prior.raw, raw)) { throw new Error( `Line ${lineNumber}: conflicting row for ${row.game_code} on ${row.draw_date}; review correction evidence manually` ); } duplicates += 1; } else { seen.set(key, { raw, row }); } } return { rows: [...seen.values()].map(({ row }) => row), duplicates }; } if (import.meta.main) { try { if (process.argv.length !== 3) throw new Error('Usage: bun import.ts '); const path = process.argv[2]; const file = await open(path, 'r'); let input: string; try { const info = await file.stat(); if (!info.isFile() || info.size > MAX_BYTES) throw new Error('Input must be a regular file at most 1 MiB'); // Cap the read itself, even if another process grows the file after stat. const bytes = Buffer.alloc(MAX_BYTES + 1); let length = 0; while (length < bytes.length) { const { bytesRead } = await file.read(bytes, length, bytes.length - length, null); if (!bytesRead) break; length += bytesRead; } if (length > MAX_BYTES) throw new Error('Input exceeds 1 MiB limit'); input = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, length)); } finally { await file.close(); } const { rows, duplicates } = evaluateJsonl(input); process.stdout.write(`${rows.map((row) => JSON.stringify(row)).join('\n')}\n`); process.stderr.write(`Validated ${rows.length} distinct rows; skipped ${duplicates} exact duplicates. No data published.\n`); } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : 'Import failed'}\n`); process.exitCode = 1; } }