// ═══════════════════════════════════════════════════════════════════════════════ // De staat op schijf: de allowlist en het opdrachtbestand van de statuspagina. // // Dit is de enige plek met bestanden erin. Het beleid zelf staat in `policy.js` // en weet hier niets van. // // Waar het staat: onder `${APP_DATA_DIR}/data/`, náást de database van de relay // en niet erin. De relay bezit zijn eigen SQLite en daar schrijven wij niet in. // ═══════════════════════════════════════════════════════════════════════════════ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { createEmptyState, normalizeState } from './policy.js'; export const stateFileName = 'owners.json'; export const commandFileName = 'command.json'; /** * Leest de allowlist. * * Drie uitkomsten, en de derde is de interessante: * - `new`: er is nog niets, dus een verse staat met de leerstand aan; * - `ok`: gelezen en begrepen; * - `corrupt`: er stond iets wat we niet begrijpen. Dan schuiven we het bestand * opzij en beginnen we met de leerstand **uit**. * * Die laatste keuze is bewust en volgt de regel die deze store al kent van * Electrum Gate: bij twijfel weigert de app. Een onleesbaar bestand betekent dat * we niet weten wie er toegelaten was; met de leerstand aan zou de eerstvolgende * die verbindt stilzwijgend de nieuwe eigenaar worden, en dat hoeft je eigen * apparaat niet te zijn. Dan liever een app die niemand toelaat en dat op de * pagina toont. */ export const readState = (dataDir, now) => { const file = join(dataDir, stateFileName); if (!existsSync(file)) return { state: createEmptyState(), status: 'new' }; let parsed = null; try { parsed = JSON.parse(readFileSync(file, 'utf8')); } catch { parsed = null; } const state = parsed === null ? null : normalizeState(parsed); if (state !== null) return { state, status: 'ok' }; // Opzij schuiven en niet weggooien: als dit ooit gebeurt, wil je kunnen zien // wat erin stond. const aside = `${file}.corrupt-${now.replace(/[:.]/g, '-')}`; try { renameSync(file, aside); } catch { // Lukt het opzij schuiven niet, dan gaan we alsnog dicht. De melding hieronder // is dan het enige dat de gebruiker heeft, en dat is nog steeds beter dan // stilzwijgend iemand toelaten. } return { state: { ...createEmptyState(), learning: false }, status: 'corrupt' }; }; /** * Schrijft de allowlist. Eerst naar een tijdelijk bestand en dan hernoemen, zodat * een onderbroken schrijfactie geen half bestand achterlaat: hernoemen binnen * dezelfde map is atomair. */ export const writeState = (dataDir, state) => { mkdirSync(dataDir, { recursive: true }); const file = join(dataDir, stateFileName); const temporary = `${file}.tmp`; writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, 'utf8'); renameSync(temporary, file); }; /** * Haalt een opdracht van de statuspagina op en verwijdert het bestand meteen. * * Verwijderen vóór het uitvoeren is met opzet: een opdracht die het proces laat * struikelen mag niet bij elke ronde opnieuw worden geprobeerd. */ export const takeCommand = (dataDir) => { const file = join(dataDir, commandFileName); if (!existsSync(file)) return null; let raw = null; try { raw = readFileSync(file, 'utf8'); } catch { raw = null; } try { unlinkSync(file); } catch { // Niets aan te doen; de volgende ronde probeert het opnieuw. } if (raw === null) return null; try { return JSON.parse(raw); } catch { return null; } }; /** De map waarin de staat hoort, afgeleid van het pad van de relay-database. */ export const dataDirectoryFor = (relayDatabaseFile) => dirname(relayDatabaseFile);