SPB Git

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%
3.1 KB · 100 lines javascript
Raw Blame History
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