"use strict"; const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); class MapGate { constructor({ mapsDir = "/etc/guanghu/navigation-maps", stateFile = "" } = {}) { this.mapsDir = mapsDir; this.stateFile = stateFile; this.acks = new Map(); this.load(); } read(target) { if (!/^[A-Z0-9-]+$/.test(target)) throw new Error("invalid_target"); const data = JSON.parse(fs.readFileSync(path.join(this.mapsDir, `${target}.json`), "utf8")); const canonical = JSON.stringify(data); return { data, hash: crypto.createHash("sha256").update(canonical).digest("hex") }; } ack(sessionToken, target, mapHash, now = Date.now() / 1000, ttl = 3600) { const current = this.read(target); if (!safeEqual(current.hash, mapHash)) return { ok: false, reason: "map_hash_mismatch" }; this.acks.set(hash(sessionToken), { target, mapHash, expiresAt: now + ttl }); this.persist(); return { ok: true, expiresAt: now + ttl }; } verify(sessionToken, target, mapHash, now = Date.now() / 1000) { const ack = this.acks.get(hash(sessionToken)); if (!ack) return { ok: false, reason: "map_not_acknowledged" }; if (now > ack.expiresAt) return { ok: false, reason: "map_ack_expired" }; if (ack.target !== target) return { ok: false, reason: "map_target_mismatch" }; if (!safeEqual(ack.mapHash, mapHash)) return { ok: false, reason: "map_changed" }; return { ok: true }; } load(now = Date.now() / 1000) { if (!this.stateFile || !fs.existsSync(this.stateFile)) return; try { for (const [key, value] of Object.entries(JSON.parse(fs.readFileSync(this.stateFile, "utf8")).acks || {})) if (value.expiresAt >= now) this.acks.set(key, value); } catch { this.acks.clear(); } } persist() { if (!this.stateFile) return; fs.writeFileSync(this.stateFile, JSON.stringify({ version: 1, acks: Object.fromEntries(this.acks) }), { mode: 0o600 }); } } function hash(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); } function safeEqual(left, right) { const a = Buffer.from(String(left)); const b = Buffer.from(String(right)); return a.length === b.length && crypto.timingSafeEqual(a, b); } module.exports = { MapGate };