UmbrelApps: community app store whatsnext
Electrum Gate, Evolu Relay en Atreides Assets, met de image-recepten in tools/ en de tests in tests/. Begonnen zonder voorgeschiedenis op 09-10-2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
85d85a1295
63 files changed
+13987
No files matched your search
@@ -0,0 +1,173 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// De relay van Evolu, met onze eigen toegangscontrole eromheen.
|
||||
//
|
||||
// Wat dit programma NIET is: een eigen relay. `createRelay` uit `@evolu/nodejs`
|
||||
// doet al het werk; het enige dat wij toevoegen zijn de twee terugroepfuncties
|
||||
// die het bedoelde uitbreidpunt vormen. Trezor doet in `createEvoluRelay.ts`
|
||||
// precies hetzelfde, alleen kijken hun functies in een Postgres.
|
||||
//
|
||||
// De opstartvolgorde en de `runMain`-aanroep zijn overgenomen uit
|
||||
// `apps/relay/src/index.ts` van Evolu zelf. Wijk daar niet van af zonder reden:
|
||||
// dat bestand is de referentie voor hoe deze relay hoort te starten.
|
||||
//
|
||||
// Zie het plan Umbrelapp, PLAN.md §4g, en Referenties/Upstream-evolu-relay.md §8.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import { createConsole, createConsoleFormatter } from '@evolu/common';
|
||||
import { installPolyfills } from '@evolu/common/polyfills';
|
||||
import { createRelay, createRelayDeps, runMain } from '@evolu/nodejs';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
|
||||
import { applyCommand, decideOwner } from './policy.js';
|
||||
import { readState, takeCommand, writeState } from './store.js';
|
||||
|
||||
installPolyfills();
|
||||
|
||||
// Zoals bovenstrooms: de database komt in een map `data` naast het programma,
|
||||
// zodat het volume van de container een voorspelbaar pad heeft. Onze allowlist
|
||||
// komt in diezelfde map te staan, naast de database en niet erin.
|
||||
mkdirSync('data', { recursive: true });
|
||||
process.chdir('data');
|
||||
|
||||
const dataDir = process.cwd();
|
||||
|
||||
const port = Number.parseInt(process.env.RELAY_PORT ?? '4000', 10);
|
||||
|
||||
// Per schrijfactie, niet per eigenaar. Dat is hoe `isOwnerWithinQuota` bij Evolu
|
||||
// bedoeld is: de relay geeft door hoeveel bytes déze actie nodig heeft. Trezor
|
||||
// telt er zelf een totaal bij op uit hun eigen tabel; wij hebben die tabel niet en
|
||||
// doen dat dus niet.
|
||||
//
|
||||
// De standaard is dezelfde 1 MB als de gepubliceerde image, en dat is een bewuste
|
||||
// keuze en geen overname: een enkele labelwijziging is klein, dus een schrijfactie
|
||||
// die hierboven uitkomt is eerder een fout of misbruik dan normaal gebruik.
|
||||
const maxWriteBytes = Number.parseInt(process.env.RELAY_MAX_WRITE_BYTES ?? '1048576', 10);
|
||||
|
||||
const console = createConsole({
|
||||
formatter: createConsoleFormatter()({ timestampFormat: 'relative' }),
|
||||
});
|
||||
|
||||
// ── De staat ──────────────────────────────────────────────────────────────────
|
||||
// In het geheugen gezaghebbend, op schijf bewaard. `isOwnerAllowed` zit in het
|
||||
// verbindingspad, dus daar wordt niet naar schijf geschreven; dat doet de
|
||||
// achtergrondlus hieronder.
|
||||
|
||||
const start = readState(dataDir, new Date().toISOString());
|
||||
let state = start.state;
|
||||
let dirty = start.status === 'new';
|
||||
|
||||
if (start.status === 'corrupt') {
|
||||
console.log(
|
||||
'[warn] owners.json was unreadable and has been moved aside. Everyone is on the waiting list ' +
|
||||
'again; allow the owners you recognise on the status page.',
|
||||
);
|
||||
}
|
||||
|
||||
const markDirty = () => {
|
||||
dirty = true;
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
if (!dirty) return;
|
||||
try {
|
||||
writeState(dataDir, state);
|
||||
dirty = false;
|
||||
} catch (error) {
|
||||
// Niet fataal: de relay blijft werken met de staat in het geheugen. Wel
|
||||
// melden, want na een herstart is de allowlist dan verouderd.
|
||||
console.log(`[warn] could not write owners.json: ${String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Opdrachten van de statuspagina ────────────────────────────────────────────
|
||||
// De pagina schrijft een bestand, dit proces past het toe. Geen Docker-socket en
|
||||
// geen tweede container die in dezelfde staat schrijft; dezelfde afspraak als bij
|
||||
// Electrum Gate.
|
||||
|
||||
const pollCommands = () => {
|
||||
const command = takeCommand(dataDir);
|
||||
if (command === null) return;
|
||||
|
||||
const result = applyCommand(state, command, new Date().toISOString());
|
||||
if (result.error !== null) {
|
||||
console.log(`[warn] command refused: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
if (result.changed) {
|
||||
state = result.state;
|
||||
markDirty();
|
||||
console.log(`[info] command applied: ${command.action}`);
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
pollCommands();
|
||||
flush();
|
||||
}, 2000).unref();
|
||||
|
||||
const stopCleanly = () => {
|
||||
flush();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', stopCleanly);
|
||||
process.on('SIGINT', stopCleanly);
|
||||
|
||||
// ── De relay ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Eén regel per eigenaar per keer dat dit proces draait, en niet per verbinding.
|
||||
// Zonder deze rem staat het log vol zodra een cliënt opnieuw verbindt; zónder
|
||||
// logging kostte het op 28-08-2026 juist een half uur om te zien dat een tweede
|
||||
// eigenaar aan de deur stond te kloppen. De bovengrens erop is er omdat een
|
||||
// weigering een id bevat dat de ander zelf verzint.
|
||||
const MAX_ANNOUNCED = 100;
|
||||
const announced = new Set();
|
||||
|
||||
const announceOnce = (key, message) => {
|
||||
if (announced.has(key)) return;
|
||||
if (announced.size < MAX_ANNOUNCED) announced.add(key);
|
||||
console.log(message);
|
||||
};
|
||||
|
||||
await runMain({ ...createRelayDeps(), console })(
|
||||
createRelay({
|
||||
port,
|
||||
|
||||
isOwnerAllowed: (ownerId) => {
|
||||
let decision;
|
||||
try {
|
||||
decision = decideOwner(state, ownerId, new Date().toISOString());
|
||||
} catch (error) {
|
||||
// Bij twijfel weigeren. Een fout in het beleid mag geen open deur worden.
|
||||
console.log(`[warn] policy failed, refusing owner: ${String(error)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = decision.state;
|
||||
if (decision.changed) markDirty();
|
||||
|
||||
const short = typeof ownerId === 'string' ? ownerId.slice(0, 12) : '(unusable)';
|
||||
if (decision.allowed) {
|
||||
announceOnce(`ok:${short}`, `[info] owner ${short} connected`);
|
||||
} else {
|
||||
announceOnce(
|
||||
`no:${short}`,
|
||||
`[info] owner ${short} is on the waiting list (${decision.reason}); allow it on the ` +
|
||||
'status page if it is yours',
|
||||
);
|
||||
}
|
||||
return decision.allowed;
|
||||
},
|
||||
|
||||
isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
||||
if (requiredBytes <= maxWriteBytes) return true;
|
||||
const short = typeof ownerId === 'string' ? ownerId.slice(0, 12) : '(unusable)';
|
||||
announceOnce(
|
||||
`quota:${short}`,
|
||||
`[warn] refused a write of ${requiredBytes} bytes from ${short}; the limit is ` +
|
||||
`${maxWriteBytes}. Raise RELAY_MAX_WRITE_BYTES if this is normal for your app.`,
|
||||
);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,239 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Beleid: wie mag er op deze relay schrijven, en wat gebeurt er met een
|
||||
// onbekende eigenaar.
|
||||
//
|
||||
// **Er is sinds 0.7.0 (09-09-2026) precies één weg naar binnen: de wachtlijst.**
|
||||
// Een onbekende eigenaar wordt geweigerd en onthouden; op de statuspagina staat
|
||||
// hij met zijn `OwnerId` erbij en daar laat je hem toe. De cliënt verbindt
|
||||
// vanzelf opnieuw en is dan binnen.
|
||||
//
|
||||
// Tot 0.6.0 was er daarnaast een getimede leerstand die de eerstvolgende
|
||||
// onbekende eigenaars toeliet. Die is eruit op voorstel van de gebruiker, en de
|
||||
// reden is dat het dubbelop was: allebei de wegen vragen iemand die bij de app
|
||||
// kan, en het venster is de zwakste van de twee omdat het iederéén toelaat die
|
||||
// er toevallig in verbindt. Dat weegt zwaarder nu de relay op een publiek
|
||||
// wss-adres kan staan. De oorspronkelijke reden voor de leerstand was dat je je
|
||||
// eigen `OwnerId` nergens kon aflezen, en die reden verviel toen de weigerlijst
|
||||
// dat id ging tonen.
|
||||
//
|
||||
// De velden `learning` en `learningUntil` worden nog wél gelézen, en dat is met
|
||||
// opzet: `STATE_VERSION` verhogen zou `normalizeState` een bestaand bestand
|
||||
// laten afwijzen, en dan schuift `store.js` de allowlist van een werkende
|
||||
// installatie opzij en gaat de deur dicht voor eigenaars die er al in stonden.
|
||||
// Ze worden gelezen, genegeerd en niet teruggeschreven.
|
||||
//
|
||||
// Alles hier is een pure functie: geen bestanden, geen netwerk, geen klok. De
|
||||
// aanroeper geeft de huidige staat en het huidige tijdstip mee en krijgt een
|
||||
// nieuwe staat terug. Dat is met opzet, want dit is de enige plek waar staat wie
|
||||
// er binnenkomt, en zulke logica hoort te testen te zijn zonder relay en zonder
|
||||
// Docker.
|
||||
//
|
||||
// De relay van Evolu roept `isOwnerAllowed(ownerId)` aan. Wij beantwoorden die
|
||||
// vraag hier; `src/index.js` doet niets anders dan lezen, doorgeven en opslaan.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const STATE_VERSION = 1;
|
||||
|
||||
// Hoeveel wachtenden we onthouden om op de pagina te tonen. Er zit een grens op
|
||||
// omdat een onbekende die blijft proberen anders het bestand vol schrijft: elke
|
||||
// poging is een eigenaar-id dat hij zelf verzint.
|
||||
//
|
||||
// Het veld heet op schijf nog `rejected` en dat blijft zo. Op de pagina heet het
|
||||
// sinds 0.7.0 de wachtlijst, want dat is wat het is: kandidaten die op een
|
||||
// beslissing wachten. De naam in het bestand hernoemen zou een migratie zijn, en
|
||||
// die is het niet waard voor een woord dat niemand ziet.
|
||||
export const MAX_REJECTED = 20;
|
||||
|
||||
// Een `OwnerId` is een publieke identificatie en geen geheim, maar hij komt van
|
||||
// buiten en belandt in een bestand. Daarom een bovengrens en een typecontrole, en
|
||||
// verder geen aannames over de vorm.
|
||||
export const MAX_OWNER_ID_LENGTH = 256;
|
||||
|
||||
export const createEmptyState = () => ({
|
||||
version: STATE_VERSION,
|
||||
owners: [],
|
||||
rejected: [],
|
||||
});
|
||||
|
||||
const isUsableOwnerId = (value) =>
|
||||
typeof value === 'string' && value.length > 0 && value.length <= MAX_OWNER_ID_LENGTH;
|
||||
|
||||
const findOwner = (state, ownerId) => state.owners.find((owner) => owner.id === ownerId);
|
||||
|
||||
/**
|
||||
* Leest een staat die van schijf komt. Geeft `null` terug als het niet klopt.
|
||||
*
|
||||
* Bewust streng: een half begrepen bestand is gevaarlijker dan geen bestand,
|
||||
* want dan denkt de app dat er een allowlist is terwijl die leeg is. De
|
||||
* aanroeper beslist wat er bij `null` gebeurt, en die keuze staat in `store.js`.
|
||||
*/
|
||||
export const normalizeState = (raw) => {
|
||||
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
||||
if (raw.version !== STATE_VERSION) return null;
|
||||
if (!Array.isArray(raw.owners) || !Array.isArray(raw.rejected)) return null;
|
||||
|
||||
// `learning` en `learningUntil` mogen er nog in staan en worden overgeslagen.
|
||||
// Een bestand van 0.6.0 of ouder heeft ze, en dat bestand hoort gewoon te
|
||||
// lezen; zie de kop van dit bestand voor waarom hier geen versieverhoging
|
||||
// tegenover staat.
|
||||
|
||||
const owners = [];
|
||||
for (const owner of raw.owners) {
|
||||
if (owner === null || typeof owner !== 'object') return null;
|
||||
if (!isUsableOwnerId(owner.id)) return null;
|
||||
if (typeof owner.allowed !== 'boolean') return null;
|
||||
owners.push({
|
||||
id: owner.id,
|
||||
allowed: owner.allowed,
|
||||
firstSeen: typeof owner.firstSeen === 'string' ? owner.firstSeen : null,
|
||||
lastSeen: typeof owner.lastSeen === 'string' ? owner.lastSeen : null,
|
||||
});
|
||||
}
|
||||
|
||||
const rejected = [];
|
||||
for (const entry of raw.rejected) {
|
||||
if (entry === null || typeof entry !== 'object') return null;
|
||||
if (!isUsableOwnerId(entry.id)) return null;
|
||||
rejected.push({
|
||||
id: entry.id,
|
||||
firstSeen: typeof entry.firstSeen === 'string' ? entry.firstSeen : null,
|
||||
lastSeen: typeof entry.lastSeen === 'string' ? entry.lastSeen : null,
|
||||
attempts: Number.isInteger(entry.attempts) && entry.attempts > 0 ? entry.attempts : 1,
|
||||
});
|
||||
}
|
||||
|
||||
return { version: STATE_VERSION, owners, rejected };
|
||||
};
|
||||
|
||||
const rememberWaiting = (rejected, ownerId, now) => {
|
||||
const existing = rejected.find((entry) => entry.id === ownerId);
|
||||
if (existing) {
|
||||
return rejected.map((entry) =>
|
||||
entry.id === ownerId ? { ...entry, lastSeen: now, attempts: entry.attempts + 1 } : entry,
|
||||
);
|
||||
}
|
||||
// Nieuwste vooraan, en afkappen op MAX_REJECTED. De oudste valt eraf; dat is
|
||||
// hier de juiste kant om te verliezen, want een aanvaller die blijft proberen
|
||||
// duwt dan zijn eigen eerdere pogingen weg en niet de allowlist.
|
||||
return [{ id: ownerId, firstSeen: now, lastSeen: now, attempts: 1 }, ...rejected].slice(
|
||||
0,
|
||||
MAX_REJECTED,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Beslist of deze eigenaar erin mag, en geeft de staat terug zoals hij daarna is.
|
||||
*
|
||||
* @returns {{allowed: boolean, state: object, reason: string, changed: boolean}}
|
||||
*/
|
||||
export const decideOwner = (state, ownerId, now) => {
|
||||
if (!isUsableOwnerId(ownerId)) {
|
||||
// Geen bruikbaar id: weigeren en niets onthouden. Onthouden zou hier juist
|
||||
// de weg openzetten om het bestand vol te schrijven met rommel.
|
||||
return { allowed: false, state, reason: 'invalid-owner-id', changed: false };
|
||||
}
|
||||
|
||||
const known = findOwner(state, ownerId);
|
||||
|
||||
if (known) {
|
||||
const seen = { ...known, lastSeen: now };
|
||||
const owners = state.owners.map((owner) => (owner.id === ownerId ? seen : owner));
|
||||
return {
|
||||
allowed: known.allowed,
|
||||
state: { ...state, owners },
|
||||
reason: known.allowed ? 'known-allowed' : 'known-blocked',
|
||||
changed: known.lastSeen !== now,
|
||||
};
|
||||
}
|
||||
|
||||
// Onbekend: weigeren en op de wachtlijst zetten. Dat is sinds 0.7.0 de enige
|
||||
// uitkomst voor een onbekende, en daarmee ook de enige weg naar binnen: op de
|
||||
// statuspagina staat hij met zijn `OwnerId` erbij en daar laat je hem toe.
|
||||
return {
|
||||
allowed: false,
|
||||
state: { ...state, rejected: rememberWaiting(state.rejected, ownerId, now) },
|
||||
reason: 'waiting',
|
||||
changed: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Voert een opdracht van de statuspagina uit.
|
||||
*
|
||||
* De pagina schrijft een opdrachtbestand en dit proces past hem toe; er is geen
|
||||
* Docker-socket en geen tweede container die in dezelfde staat schrijft. Dat is
|
||||
* dezelfde afspraak als bij Electrum Gate.
|
||||
*
|
||||
* @returns {{state: object, changed: boolean, error: string|null}}
|
||||
*/
|
||||
export const applyCommand = (state, command, now) => {
|
||||
const unchanged = (error) => ({ state, changed: false, error });
|
||||
|
||||
if (command === null || typeof command !== 'object') return unchanged('malformed-command');
|
||||
|
||||
switch (command.action) {
|
||||
case 'block': {
|
||||
if (!isUsableOwnerId(command.ownerId)) return unchanged('malformed-command');
|
||||
if (!findOwner(state, command.ownerId)) return unchanged('unknown-owner');
|
||||
const owners = state.owners.map((owner) =>
|
||||
owner.id === command.ownerId ? { ...owner, allowed: false, lastSeen: owner.lastSeen } : owner,
|
||||
);
|
||||
return { state: { ...state, owners }, changed: true, error: null };
|
||||
}
|
||||
|
||||
case 'allow': {
|
||||
if (!isUsableOwnerId(command.ownerId)) return unchanged('malformed-command');
|
||||
const known = findOwner(state, command.ownerId);
|
||||
// Ook een eigenaar die alleen als geweigerde poging bekend is, mag hiermee
|
||||
// alsnog toegelaten worden. Dat is de knop naast zo'n regel op de pagina.
|
||||
//
|
||||
// De geschiedenis van die pogingen gaat mee. Dat leek een detail tot het
|
||||
// op 28-08-2026 misging: de pagina meldde daarna "first seen" op het moment
|
||||
// van de klik, terwijl die eigenaar al twintig minuten aan de deur stond.
|
||||
// Juist bij een eigenaar die je alsnog toelaat wil je kunnen terugzien
|
||||
// sinds wanneer hij er is.
|
||||
const attempted = state.rejected.find((entry) => entry.id === command.ownerId);
|
||||
const owners = known
|
||||
? state.owners.map((owner) =>
|
||||
owner.id === command.ownerId ? { ...owner, allowed: true } : owner,
|
||||
)
|
||||
: [
|
||||
...state.owners,
|
||||
{
|
||||
id: command.ownerId,
|
||||
allowed: true,
|
||||
firstSeen: attempted && attempted.firstSeen ? attempted.firstSeen : now,
|
||||
lastSeen: attempted ? attempted.lastSeen : null,
|
||||
},
|
||||
];
|
||||
return {
|
||||
state: {
|
||||
...state,
|
||||
owners,
|
||||
rejected: state.rejected.filter((entry) => entry.id !== command.ownerId),
|
||||
},
|
||||
changed: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
case 'forget': {
|
||||
if (!isUsableOwnerId(command.ownerId)) return unchanged('malformed-command');
|
||||
const inOwners = Boolean(findOwner(state, command.ownerId));
|
||||
const inRejected = state.rejected.some((entry) => entry.id === command.ownerId);
|
||||
if (!inOwners && !inRejected) return unchanged('unknown-owner');
|
||||
return {
|
||||
state: {
|
||||
...state,
|
||||
owners: state.owners.filter((owner) => owner.id !== command.ownerId),
|
||||
rejected: state.rejected.filter((entry) => entry.id !== command.ownerId),
|
||||
},
|
||||
changed: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return unchanged('unknown-action');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 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);
|
||||
Reference in new issue
Block a user