spb/modelmap Public License
Internal cartography of local LLMs on Apple Silicon — registered, gated, negative-first. Public atlas at modelmap.io.
Python 66.3%
JavaScript 24.5%
CSS 8.1%
Shell 0.7%
1// ============================================================================2// Project : modelmap3// File : site/lib/comments.js4// Purpose : Visitor comments — persistent JSON store, validation, limits5// Author : Simon-Pierre Boucher6// Contact : contact@spboucher.ai7// Website : https://modelmap.io8// Created : 2026-08-129// Modified : 2026-08-1210// Platform : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr)11// License : All rights reserved (research code)12// ============================================================================13"use strict";1415const fs = require("fs");16const os = require("os");17const path = require("path");18const crypto = require("crypto");1920// Stored OUTSIDE the app directory so rsync --delete redeploys never wipe it.21const STORE = process.env.COMMENTS_FILE ||22 path.join(os.homedir(), ".modelmap-web", "comments.json");2324const MAX_NAME = 60;25const MAX_MESSAGE = 2000;26const MIN_MESSAGE = 3;27const MAX_TOTAL = 5000; // hard cap on stored comments28const RATE_LIMIT = 5; // posts per IP per window29const RATE_WINDOW_MS = 60 * 60 * 1000;3031const recentByIp = new Map(); // ipHash -> [timestamps]3233function ensureStore() {34 fs.mkdirSync(path.dirname(STORE), { recursive: true });35 if (!fs.existsSync(STORE)) fs.writeFileSync(STORE, "[]\n");36}3738function list() {39 try {40 ensureStore();41 const all = JSON.parse(fs.readFileSync(STORE, "utf8"));42 return Array.isArray(all) ? all.slice().reverse() : [];43 } catch {44 return [];45 }46}4748function clean(s, max) {49 return String(s || "")50 .replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, "")51 .replace(/\s+/g, (m) => (m.includes("\n") ? "\n" : " "))52 .trim()53 .slice(0, max);54}5556function ipHash(ip) {57 return crypto.createHash("sha256").update(String(ip || "")).digest("hex").slice(0, 16);58}5960function rateLimited(ip) {61 const key = ipHash(ip);62 const now = Date.now();63 const times = (recentByIp.get(key) || []).filter((t) => now - t < RATE_WINDOW_MS);64 if (times.length >= RATE_LIMIT) return true;65 times.push(now);66 recentByIp.set(key, times);67 return false;68}6970/**71 * Add a comment. Returns { ok, error? }.72 * honeypot: hidden "website" field — bots fill it, humans don't.73 */74function add({ name, message, honeypot, ip }) {75 if (honeypot) return { ok: true, dropped: true }; // pretend success, drop silently76 const n = clean(name, MAX_NAME) || "Anonymous";77 const m = clean(message, MAX_MESSAGE);78 if (m.length < MIN_MESSAGE) return { ok: false, error: "Message is too short." };79 if (rateLimited(ip)) return { ok: false, error: "Too many comments from this address — try again later." };80 ensureStore();81 let all;82 try {83 all = JSON.parse(fs.readFileSync(STORE, "utf8"));84 if (!Array.isArray(all)) all = [];85 } catch {86 all = [];87 }88 all.push({89 id: crypto.randomUUID(),90 name: n,91 message: m,92 created: new Date().toISOString(),93 });94 if (all.length > MAX_TOTAL) all = all.slice(all.length - MAX_TOTAL);95 fs.writeFileSync(STORE, JSON.stringify(all, null, 2) + "\n");96 return { ok: true };97}9899module.exports = { list, add, STORE };100