#!/usr/bin/env node /** * judge-watermark (K6, §11/§2; F2 per-session) — анти-откатная монотонная зарубка. * Журнал/план/указатель шага лежат в runtime и ПОДПИСАНЫ, но СТАРАЯ подпись валидна * → подмена на раннюю валидную пару файлов = откат (спрятать прошлое / отмотать шаг). * verifyChain это НЕ ловит (печать цела, просто старая). Лечение: max(seq/шаг), * виденный судьёй, хранится в OS-keychain рядом с ключом судьи — ВНЕ откатываемого * runtime. current < виденного → откат → блок. Per-session: свежая сессия = генезис 0 * (это не обход — там отката нет). keychainGet/Set инъектируются (тесты). */ const SERVICE = 'router-mentor-judge-watermark'; function account(sessionId) { return `wm:${sessionId}`; } export function readWatermark({ sessionId, keychainGet }) { let raw = null; try { raw = keychainGet({ service: SERVICE, account: account(sessionId) }); } catch { raw = null; } const n = Number.parseInt(String(raw ?? ''), 10); return Number.isFinite(n) && n >= 0 ? n : 0; // genesis 0 при отсутствии/мусоре } /** Поднять зарубку ТОЛЬКО вверх (монотонно). */ export function bumpWatermark({ sessionId, value, keychainGet, keychainSet }) { const cur = readWatermark({ sessionId, keychainGet }); const next = Math.max(cur, Number.isFinite(value) ? value : cur); if (next !== cur) keychainSet({ service: SERVICE, account: account(sessionId), value: next }); return next; } /** current ниже виденного → откат → блок. */ export function checkNoRollback({ sessionId, current, keychainGet }) { const seen = readWatermark({ sessionId, keychainGet }); const cur = Number.isFinite(current) ? current : 0; const rolledBack = cur < seen; return { ok: !rolledBack, rolledBack, seen }; } export const _internals = { SERVICE };