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:
HarmenandClaude Opus 5.5 committed 2026-10-09 10:40:25 +02:00
commit 85d85a1295
63 files changed
+13987

No files matched your search

+157
View File
@@ -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 };
},
};
};