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,157 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 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 };
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user