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>
158 lines
6.3 KiB
JavaScript
158 lines
6.3 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Het register: welke eigenaars bestaan er, en hoe kom je erbij terug.
|
|
//
|
|
// Eén bestand, `owners.json`, met per eigenaar een naam, zijn mnemonic en zijn
|
|
// `OwnerId`. De mnemonic is het enige dat werkelijk bewaard hoeft te worden; het
|
|
// `OwnerId` staat erbij omdat je het nodig hebt om een eigenaar op de
|
|
// statuspagina van de app terug te vinden, en omdat je het dan kunt opzoeken
|
|
// zonder Evolu te starten.
|
|
//
|
|
// **Dit is sleutelmateriaal.** Het heeft buiten deze proefopstelling geen
|
|
// waarde, maar het bestand staat in .gitignore en het hoort daar te blijven; zie
|
|
// het plan Testclient, PLAN.md §4a.
|
|
//
|
|
// Alles hier is een gewone functie op een pad. Geen Evolu, geen netwerk: dat is
|
|
// wat dit het enige deel van de cliënt maakt dat zonder relay en zonder
|
|
// fixtures te toetsen is, en het is dezelfde scheiding die policy.js aan de
|
|
// relay-kant heeft.
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
/**
|
|
* Een naam is een herkenpunt voor jezelf en gaat nooit de deur uit.
|
|
*
|
|
* Toch begrensd, want hij belandt in een JSON-bestand en straks op een pagina.
|
|
* Letters, cijfers, streepje en liggend streepje; dat is genoeg om "trezor-mac"
|
|
* of "proef_2" te kunnen schrijven en het sluit een naam met een schuine streep
|
|
* erin uit, die er als pad uit zou zien.
|
|
*/
|
|
const NAAM_PATROON = /^[A-Za-z0-9_-]{1,40}$/;
|
|
|
|
export const isBruikbareNaam = (naam) => typeof naam === 'string' && NAAM_PATROON.test(naam);
|
|
|
|
const LEEG = { version: 1, owners: [] };
|
|
|
|
/**
|
|
* Leest het bestand. Geeft `null` bij iets wat niet klopt.
|
|
*
|
|
* Bewust streng, en om dezelfde reden als in policy.js: een half begrepen
|
|
* bestand is gevaarlijker dan geen bestand. Hier zou dat betekenen dat een
|
|
* eigenaar stil verdwijnt terwijl zijn mnemonic wél op schijf staat, en dan is
|
|
* de proef die eroverheen liep niet meer te herhalen.
|
|
*/
|
|
export const normaliseer = (ruw) => {
|
|
if (ruw === null || typeof ruw !== 'object' || Array.isArray(ruw)) return null;
|
|
if (ruw.version !== 1) return null;
|
|
if (!Array.isArray(ruw.owners)) return null;
|
|
|
|
const owners = [];
|
|
for (const eigenaar of ruw.owners) {
|
|
if (eigenaar === null || typeof eigenaar !== 'object') return null;
|
|
if (!isBruikbareNaam(eigenaar.naam)) return null;
|
|
if (typeof eigenaar.mnemonic !== 'string' || eigenaar.mnemonic.length === 0) return null;
|
|
if (typeof eigenaar.ownerId !== 'string' || eigenaar.ownerId.length === 0) return null;
|
|
if (owners.some((eerder) => eerder.naam === eigenaar.naam)) return null;
|
|
owners.push({
|
|
naam: eigenaar.naam,
|
|
mnemonic: eigenaar.mnemonic,
|
|
ownerId: eigenaar.ownerId,
|
|
aangemaakt: typeof eigenaar.aangemaakt === 'string' ? eigenaar.aangemaakt : null,
|
|
});
|
|
}
|
|
|
|
return { version: 1, owners };
|
|
};
|
|
|
|
/**
|
|
* Opent het register in `directory`, en maakt de map aan als hij er niet is.
|
|
*
|
|
* Een ontbrekend bestand is de begintoestand en geen fout. Een onleesbaar bestand
|
|
* wél: dat wordt gemeld en niet stil vervangen, want vervangen is hier hetzelfde
|
|
* als weggooien.
|
|
*/
|
|
export const openRegister = (directory) => {
|
|
mkdirSync(directory, { recursive: true });
|
|
const pad = join(directory, 'owners.json');
|
|
|
|
let staat;
|
|
try {
|
|
staat = normaliseer(JSON.parse(readFileSync(pad, 'utf8')));
|
|
} catch (oorzaak) {
|
|
if (oorzaak.code === 'ENOENT') {
|
|
staat = { ...LEEG, owners: [] };
|
|
} else {
|
|
throw new Error(`${pad} is niet te lezen: ${oorzaak.message}`);
|
|
}
|
|
}
|
|
|
|
if (staat === null) {
|
|
throw new Error(
|
|
`${pad} klopt niet. Er is niets vervangen; kijk er zelf naar, want hier staan mnemonics in.`,
|
|
);
|
|
}
|
|
|
|
// Schrijven gaat via een tijdelijk bestand en een hernoeming. Een half
|
|
// geschreven register kost mnemonics, en een hernoeming binnen dezelfde map is
|
|
// op elk besturingssysteem dat wij raken één handeling.
|
|
const bewaar = () => {
|
|
const tijdelijk = `${pad}.nieuw`;
|
|
writeFileSync(tijdelijk, `${JSON.stringify(staat, null, 2)}\n`, 'utf8');
|
|
renameSync(tijdelijk, pad);
|
|
};
|
|
|
|
return {
|
|
pad,
|
|
|
|
/** Alle eigenaars, in de volgorde waarin ze zijn aangemaakt. */
|
|
lijst: () => staat.owners.map((eigenaar) => ({ ...eigenaar })),
|
|
|
|
/** Eén eigenaar, of `null`. */
|
|
zoek: (naam) => {
|
|
const gevonden = staat.owners.find((eigenaar) => eigenaar.naam === naam);
|
|
return gevonden ? { ...gevonden } : null;
|
|
},
|
|
|
|
/**
|
|
* Zet een eigenaar in het register.
|
|
*
|
|
* Gooit bij een naam die niet kan of al bestaat. Overschrijven zou de
|
|
* mnemonic van de vorige eigenaar weggooien, en dat is precies het soort
|
|
* stille schade waar het commentaar bovenaan dit bestand over gaat.
|
|
*/
|
|
voegToe: ({ naam, mnemonic, ownerId, aangemaakt }) => {
|
|
if (!isBruikbareNaam(naam)) {
|
|
throw new Error(`"${naam}" kan niet als naam. Letters, cijfers, - en _, hoogstens 40 tekens.`);
|
|
}
|
|
if (staat.owners.some((eigenaar) => eigenaar.naam === naam)) {
|
|
throw new Error(`Er is al een eigenaar "${naam}".`);
|
|
}
|
|
const nieuw = {
|
|
naam,
|
|
mnemonic,
|
|
ownerId,
|
|
aangemaakt: aangemaakt ?? new Date().toISOString(),
|
|
};
|
|
staat = { ...staat, owners: [...staat.owners, nieuw] };
|
|
bewaar();
|
|
return { ...nieuw };
|
|
},
|
|
|
|
/**
|
|
* Haalt een eigenaar uit het register. Gooit als hij er niet in staat.
|
|
*
|
|
* Let op wat dit níet doet: de database van die eigenaar blijft staan, en op
|
|
* de relay verandert er helemaal niets. Vergeten is hier vergeten, geen
|
|
* wissen; zie OPEN.md punt 3 en 4 van het plan.
|
|
*/
|
|
vergeet: (naam) => {
|
|
const gevonden = staat.owners.find((eigenaar) => eigenaar.naam === naam);
|
|
if (!gevonden) throw new Error(`Geen eigenaar "${naam}".`);
|
|
staat = { ...staat, owners: staat.owners.filter((eigenaar) => eigenaar.naam !== naam) };
|
|
bewaar();
|
|
return { ...gevonden };
|
|
},
|
|
};
|
|
};
|