'use strict'; const path = require('node:path'); const zlib = require('node:zlib'); const { URL } = require('node:url'); const MAX_BODY_BYTES = 256 * 1024 * 1024; function isCompressible(contentType) { const t = String(contentType || ''); return /^text\//.test(t) || /application\/(json|javascript)/.test(t); } function send(res, status, body = '', contentType = 'text/plain; charset=utf-8', headers = {}) { let bytes = Buffer.isBuffer(body) ? body : Buffer.from(String(body), 'utf8'); const outHeaders = { 'Content-Type': contentType, 'Cache-Control': 'no-store', ...headers }; // Gzip text/JSON responses when the client accepts it (single-user app: sync gzip is fine). if (res._acceptGzip && isCompressible(contentType) && bytes.length > 1024) { bytes = zlib.gzipSync(bytes); outHeaders['Content-Encoding'] = 'gzip'; outHeaders['Vary'] = 'Accept-Encoding'; } outHeaders['Content-Length'] = bytes.length; res.writeHead(status, outHeaders); res.end(bytes); } function json(res, status, data, headers = {}) { send(res, status, JSON.stringify(data), 'application/json; charset=utf-8', headers); } async function readBody(req) { const chunks = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > MAX_BODY_BYTES) throw new Error('request body too large'); chunks.push(chunk); } return Buffer.concat(chunks); } async function readJson(req) { const body = await readBody(req); if (!body.length) return {}; return JSON.parse(body.toString('utf8')); } function safeName(name, fallback = 'file') { return String(name || fallback) .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') .replace(/\s+/g, ' ') .trim() .slice(0, 180) || fallback; } function mimeByExt(file) { const ext = path.extname(file).toLowerCase(); if (ext === '.html') return 'text/html; charset=utf-8'; if (ext === '.js' || ext === '.mjs') return 'text/javascript; charset=utf-8'; if (ext === '.css') return 'text/css; charset=utf-8'; if (ext === '.json') return 'application/json; charset=utf-8'; if (ext === '.png') return 'image/png'; if (ext === '.webp') return 'image/webp'; if (ext === '.gif') return 'image/gif'; if (ext === '.svg') return 'image/svg+xml'; if (ext === '.woff2') return 'font/woff2'; if (ext === '.woff') return 'font/woff'; if (ext === '.ttf') return 'font/ttf'; if (ext === '.otf') return 'font/otf'; return 'image/jpeg'; } function extFromContentType(type, sourceUrl) { const contentType = String(type || '').toLowerCase(); if (contentType.includes('png')) return '.png'; if (contentType.includes('webp')) return '.webp'; if (contentType.includes('gif')) return '.gif'; if (contentType.includes('jpeg') || contentType.includes('jpg')) return '.jpg'; try { return path.extname(new URL(sourceUrl).pathname) || '.jpg'; } catch { return '.jpg'; } } module.exports = { send, json, readBody, readJson, safeName, mimeByExt, extFromContentType, MAX_BODY_BYTES };