import { createReadStream, existsSync, mkdirSync, statSync } from "node:fs"; import { appendFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { createServer } from "node:http"; import { extname, join, normalize } from "node:path"; const packagedRoot = join(process.cwd(), "public"); const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out"); const port = Number(process.env.APP_PORT || 3200); const dataDirectory = process.env.DATA_DIR || join(process.cwd(), "data"); const usageLogPath = join(dataDirectory, "usage.jsonl"); mkdirSync(dataDirectory, { recursive: true }); function formatChinaTimestamp(date) { return new Date(date.getTime() + 8 * 60 * 60 * 1000) .toISOString() .replace("Z", "+08:00"); } const mimeTypes = { ".css": "text/css; charset=utf-8", ".html": "text/html; charset=utf-8", ".ico": "image/x-icon", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".rsc": "text/x-component", ".svg": "image/svg+xml", ".webp": "image/webp", }; function sendJson(response, status, value) { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); response.end(JSON.stringify(value)); } function writeUsageLog(request, response) { let body = ""; request.setEncoding("utf8"); request.on("data", (chunk) => { body += chunk; if (body.length > 16_384) request.destroy(); }); request.on("end", async () => { try { const input = JSON.parse(body || "{}"); if (!['page_view', 'conversion'].includes(input.event)) { sendJson(response, 400, { error: "Invalid event" }); return; } const now = new Date(); const entry = { timestamp: now.toISOString(), timestamp_cn: formatChinaTimestamp(now), event: input.event, client_hash: request.headers["x-forwarded-for"] || request.socket.remoteAddress ? createHash("sha256").update(String(request.headers["x-forwarded-for"] || request.socket.remoteAddress)).digest("hex").slice(0, 16) : undefined, ...(input.event === "conversion" ? { width: Math.max(8, Math.min(256, Number(input.width) || 0)), height: Math.max(8, Math.min(256, Number(input.height) || 0)), sampling_strategy: input.sampling_strategy === "dominant" ? "dominant" : "smooth", color_limit: Math.max(2, Math.min(64, Number(input.color_limit) || 0)), actual_colors: Math.max(0, Math.min(221, Number(input.actual_colors) || 0)), bead_count: Math.max(0, Math.min(65_536, Number(input.bead_count) || 0)), transparent_cells: Math.max(0, Math.min(65_536, Number(input.transparent_cells) || 0)), } : {}), }; await appendFile(usageLogPath, `${JSON.stringify(entry)}\n`, "utf8"); sendJson(response, 202, { ok: true }); } catch { sendJson(response, 400, { error: "Invalid request" }); } }); } createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname); if (pathname === "/api/usage" && request.method === "POST") { writeUsageLog(request, response); return; } if (pathname.startsWith("/api/")) { sendJson(response, 404, { error: "Not found" }); return; } const relativePath = normalize(pathname).replace(/^([/\\])+/, ""); let filePath = join(root, relativePath || "index.html"); if (!filePath.startsWith(root) || !existsSync(filePath)) filePath = join(root, "index.html"); if (existsSync(filePath) && statSync(filePath).isDirectory()) filePath = join(filePath, "index.html"); if (!existsSync(filePath)) { response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); response.end("Not found"); return; } response.writeHead(200, { "Content-Type": mimeTypes[extname(filePath)] || "application/octet-stream", "Cache-Control": filePath.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable", }); if (request.method === "HEAD") response.end(); else createReadStream(filePath).pipe(response); }).listen(port, "0.0.0.0", () => { console.log(`Pixel bead pattern web listening on ${port}`); });