De pagina van Evolu Relay op de lijst van de gebruiker
Negen punten uit echt gebruik met 0.4.0, alle negen gedaan. Twee ervan waren onderzoeksvragen en die staan onderaan. Evolu Relay 0.5.0 - Een tijdvenster van twee minuten voor nieuwe eigenaars, met een stopknop. De teller zit in het relay-proces en niet in de pagina: een teller in een tabblad dat je sluit, sluit de deur niet. policy.js kreeg learningUntil, isLearningOpen en expireLearning. - decideOwner kijkt naar isLearningOpen en niet naar het veld learning. De lus die een verlopen venster opruimt loopt elke twee seconden, en in dat gat zou een onbekende alsnog binnenkomen. - Zonder STATE_VERSION te verhogen, met een toets die dat verdedigt: een verhoging zou de allowlist van de draaiende installatie laten afwijzen en de deur sluiten voor eigenaars die er al in stonden. - Labels op een eigenaar-id, in een eigen labels.json met de agent als enige schrijver. Een label zegt niets over toegang, dus de relay hoeft het niet te weten; het is daardoor meteen opgeslagen en werkt ook als de relay omligt. - Geblokkeerde en geweigerde eigenaars in een kader, met een badge die zegt welke van de twee het is. De badge staat buiten het hover-blok, anders is dat onderscheid onzichtbaar tenzij je over de regel gaat. - Maatvoering gelijk aan Electrum Gate: 1760px, hetzelfde raster, icoon van 64 pixels, dezelfde kop, versienummer erachter. Uitleg uit de kaders, knoppen pas bij hover, geen voetregel. Electrum Gate 0.0.24 - Menu-item "About this app", in beide apps. - De statuswidget zei "Answering" met "answered in 7 ms, from inside the app" en zegt nu "Running" met de meting eronder. De nuance dat de controle van container naar container loopt is verplaatst naar een eigen kopje in die dialoog, waar er ruimte voor is; vier woorden waren te weinig. Toetsen en gereedschap - tests/test_relay_agent.py (nieuw, 65 toetsen) en tests/test_paginas_parsen.mjs (nieuw). Muteertests gedraaid op de beslissende regels. - Een dollarteken-toets in test_appstore_vorm.py. Het commentaar in drie bestanden beweerde al dat die test bestond; nu is dat waar. - Een toets dat er geen werkbestanden in een app-map staan. umbreld kopieert de hele map naar het apparaat en in de back-up. - tools/voorbeeldpagina.mjs maakt van een *.template een pagina die je in een browser kunt openen. Dat vond meteen twee echte opmaakfouten. De twee onderzoeksvragen - Een geweigerde eigenaar komt niet in de database: isOwnerAllowed zit in de WebSocket-upgrade, dus het is een 401 en een gesloten socket. Het gewenste gevolg treedt wel op, via de client: die is local-first en levert bij toelating de hele geschiedenis. Blokkeren werkt daarentegen pas bij de volgende verbinding, en dat staat als open punt. - De blobs zijn niet met een xpub te ontcijferen; een OwnerId komt daar niet uit. Met de SLIP-21-node van het apparaat kan het wel, maar die geeft volledige zeggenschap, dus dat hoort niet in een relay. Als plan-punt opgenomen bij de tool in HomeGit/Trezor. Nog niet uitgerold: de image 0.5.0 moet gebouwd en geduwd worden. De digest staat daarom niet in de compose, want een oude digest onder een nieuwe tag levert stil de oude relay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,14 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import {
|
||||
MAX_LEARNING_SECONDS,
|
||||
MAX_OWNER_ID_LENGTH,
|
||||
MAX_REJECTED,
|
||||
applyCommand,
|
||||
createEmptyState,
|
||||
decideOwner,
|
||||
expireLearning,
|
||||
isLearningOpen,
|
||||
normalizeState,
|
||||
} from '../tools/evolu-relay/src/policy.js';
|
||||
|
||||
@@ -35,8 +38,12 @@ const toets = (omschrijving, voorwaarde) => {
|
||||
|
||||
const NU = '2026-08-28T12:00:00.000Z';
|
||||
const LATER = '2026-08-28T13:00:00.000Z';
|
||||
// Binnen en buiten een venster van twee minuten dat op NU begint.
|
||||
const BINNEN = '2026-08-28T12:01:00.000Z';
|
||||
const NA = '2026-08-28T12:02:30.000Z';
|
||||
const EIGEN = 'owner-van-de-gebruiker';
|
||||
const VREEMD = 'owner-van-een-ander';
|
||||
const TWEE_MINUTEN = 120;
|
||||
|
||||
// ── De leerstand ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -164,6 +171,160 @@ const VREEMD = 'owner-van-een-ander';
|
||||
);
|
||||
}
|
||||
|
||||
// ── Het tijdvenster voor nieuwe eigenaars ─────────────────────────────────────
|
||||
//
|
||||
// De pagina zet de deur twee minuten open in plaats van een schakelaar om te
|
||||
// zetten die je moet onthouden. De teller hoort in dít bestand en niet in de
|
||||
// browser: een teller in een tabblad dat je sluit, sluit de deur niet.
|
||||
|
||||
{
|
||||
const vers = createEmptyState();
|
||||
toets('een verse staat heeft geen tijdslot', vers.learningUntil === null);
|
||||
toets('en staat dus open zonder dat er een klok loopt', isLearningOpen(vers, LATER) === true);
|
||||
|
||||
const open = applyCommand(
|
||||
vers,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
);
|
||||
toets('openzetten met seconden is een wijziging', open.changed === true);
|
||||
toets('en zet een tijdslot', open.state.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||
toets('binnen het venster staat de deur open', isLearningOpen(open.state, BINNEN) === true);
|
||||
toets('erna niet meer', isLearningOpen(open.state, NA) === false);
|
||||
}
|
||||
|
||||
// De guard die er het meest toe doet, en dit is de reden dat `decideOwner` naar
|
||||
// `isLearningOpen` kijkt en niet naar het veld `learning`: tussen het aflopen van
|
||||
// het venster en de ronde die het bestand opruimt zitten een paar seconden. In dat
|
||||
// gat mag er niemand binnenkomen.
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
toets('in het bestand staat de leerstand nog aan', open.learning === true);
|
||||
|
||||
const binnen = decideOwner(open, EIGEN, BINNEN);
|
||||
toets('binnen het venster wordt een onbekende geleerd', binnen.allowed === true);
|
||||
|
||||
const erna = decideOwner(open, VREEMD, NA);
|
||||
toets(
|
||||
'na het venster wordt hij geweigerd, ook al staat learning nog op true',
|
||||
erna.allowed === false,
|
||||
);
|
||||
toets('met de reden dat er niet geleerd wordt', erna.reason === 'not-learning');
|
||||
toets('en de poging wordt onthouden voor de pagina', erna.state.rejected.length === 1);
|
||||
|
||||
// En wie al binnen was, blijft binnen. Het venster gaat over onbekenden.
|
||||
const bekend = decideOwner(binnen.state, EIGEN, LATER);
|
||||
toets('een geleerde eigenaar mag er na het venster nog steeds in', bekend.allowed === true);
|
||||
}
|
||||
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
// Opnieuw indrukken verlengt. Zonder deze uitzondering zou `applyCommand`
|
||||
// 'changed: false' teruggeven omdat learning al true was, en dan liep het
|
||||
// venster af terwijl de gebruiker net verlengde.
|
||||
const opnieuw = applyCommand(
|
||||
open,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
BINNEN,
|
||||
);
|
||||
toets('opnieuw openzetten is een wijziging', opnieuw.changed === true);
|
||||
toets('en zet de klok terug', opnieuw.state.learningUntil === '2026-08-28T12:03:00.000Z');
|
||||
toets('dus is de deur na het oude venster nog open', isLearningOpen(opnieuw.state, NA) === true);
|
||||
|
||||
// Vroegtijdig sluiten haalt het tijdslot mee weg. Zou het blijven staan, dan
|
||||
// erfde een volgende 'open zonder seconden' een venster dat niet gevraagd is.
|
||||
const dicht = applyCommand(open, { action: 'set-learning', value: false }, BINNEN);
|
||||
toets('vroegtijdig sluiten kan', dicht.state.learning === false);
|
||||
toets('en haalt het tijdslot weg', dicht.state.learningUntil === null);
|
||||
toets('de deur is dan dicht', isLearningOpen(dicht.state, BINNEN) === false);
|
||||
|
||||
const zonderSlot = applyCommand(dicht.state, { action: 'set-learning', value: true }, BINNEN);
|
||||
toets('openzetten zonder seconden blijft bestaan', zonderSlot.state.learning === true);
|
||||
toets('en heeft dan geen tijdslot', zonderSlot.state.learningUntil === null);
|
||||
}
|
||||
|
||||
// Wat de agent hoort tegen te houden, houdt dit bestand ook tegen. Twee zeven, en
|
||||
// dat is met opzet: dit is de kant die van buiten komt.
|
||||
{
|
||||
const vers = createEmptyState();
|
||||
const rommel = [
|
||||
{ action: 'set-learning', value: true, seconds: 0 },
|
||||
{ action: 'set-learning', value: true, seconds: -60 },
|
||||
{ action: 'set-learning', value: true, seconds: 1.5 },
|
||||
{ action: 'set-learning', value: true, seconds: '120' },
|
||||
{ action: 'set-learning', value: true, seconds: true },
|
||||
{ action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS + 1 },
|
||||
];
|
||||
for (const opdracht of rommel) {
|
||||
const uitkomst = applyCommand(vers, opdracht, NU);
|
||||
toets(
|
||||
`een onbruikbaar aantal seconden wordt geweigerd: ${JSON.stringify(opdracht.seconds)}`,
|
||||
uitkomst.error === 'malformed-command',
|
||||
);
|
||||
toets('en verandert niets', uitkomst.changed === false);
|
||||
}
|
||||
|
||||
toets(
|
||||
'de bovengrens zelf mag wel',
|
||||
applyCommand(vers, { action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS }, NU)
|
||||
.error === null,
|
||||
);
|
||||
|
||||
// Een onleesbaar `now` mag geen venster zonder einde opleveren. Dat is de reden
|
||||
// dat `addSeconds` null teruggeeft in plaats van iets te verzinnen.
|
||||
const kapotteKlok = applyCommand(
|
||||
vers,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
'geen tijdstip',
|
||||
);
|
||||
toets('met een onleesbare klok wordt de opdracht geweigerd', kapotteKlok.error === 'malformed-command');
|
||||
toets('en blijft de staat zoals hij was', kapotteKlok.changed === false);
|
||||
}
|
||||
|
||||
// Het opruimen. Correctheid hangt hier niet aan, want `isLearningOpen` weigert al;
|
||||
// wat dit oplevert is dat het bestand en de pagina hetzelfde zeggen als de klok.
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
const nogNiet = expireLearning(open, BINNEN);
|
||||
toets('binnen het venster wordt er niets opgeruimd', nogNiet.changed === false);
|
||||
toets('en de staat blijft dezelfde', nogNiet.state === open);
|
||||
|
||||
const opgeruimd = expireLearning(open, NA);
|
||||
toets('na het venster gaat de leerstand uit', opgeruimd.state.learning === false);
|
||||
toets('en het tijdslot weg', opgeruimd.state.learningUntil === null);
|
||||
toets('en dat is een wijziging', opgeruimd.changed === true);
|
||||
|
||||
toets(
|
||||
'nog een keer opruimen doet niets',
|
||||
expireLearning(opgeruimd.state, LATER).changed === false,
|
||||
);
|
||||
toets(
|
||||
'een leerstand zonder tijdslot wordt niet opgeruimd',
|
||||
expireLearning(createEmptyState(), LATER).changed === false,
|
||||
);
|
||||
|
||||
// Een onleesbaar tijdstip in het bestand is dicht, niet open. Dezelfde regel als
|
||||
// bij een onleesbare owners.json in store.js: bij twijfel weigert de app.
|
||||
const kapot = { ...createEmptyState(), learningUntil: 'ergens volgende week' };
|
||||
toets('een onleesbaar tijdslot leest als dicht', isLearningOpen(kapot, NU) === false);
|
||||
toets('en een onleesbare klok ook', isLearningOpen(open, 'geen tijdstip') === false);
|
||||
}
|
||||
|
||||
// ── Lezen van schijf ──────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
@@ -185,6 +346,28 @@ const VREEMD = 'owner-van-een-ander';
|
||||
for (const invoer of rommel) {
|
||||
toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null);
|
||||
}
|
||||
|
||||
// Het tijdslot is toegevoegd zonder STATE_VERSION te verhogen, en dat is de
|
||||
// toets die dat verdedigt: een bestand van een draaiende installatie mag niet
|
||||
// ineens onleesbaar worden. Zou dat gebeuren, dan schuift store.js de allowlist
|
||||
// opzij en gaat de deur dicht voor eigenaars die er al in stonden.
|
||||
const oud = { version: 1, learning: true, owners: [{ id: EIGEN, allowed: true }], rejected: [] };
|
||||
const gelezen = normalizeState(oud);
|
||||
toets('een bestand van vóór het tijdslot leest nog', gelezen !== null);
|
||||
toets('met de leerstand die erin stond', gelezen.learning === true);
|
||||
toets('en zonder tijdslot', gelezen.learningUntil === null);
|
||||
toets('en de eigenaar er nog in', gelezen.owners[0].id === EIGEN);
|
||||
|
||||
const metSlot = normalizeState({ ...oud, learningUntil: '2026-08-28T12:02:00.000Z' });
|
||||
toets('een geldig tijdslot blijft staan', metSlot.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||
|
||||
for (const onzin of ['morgen', '', 42, {}, null]) {
|
||||
toets(
|
||||
`een onleesbaar tijdslot wordt null en geen weigering: ${JSON.stringify(onzin)}`,
|
||||
normalizeState({ ...oud, learningUntil: onzin }) !== null
|
||||
&& normalizeState({ ...oud, learningUntil: onzin }).learningUntil === null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
Reference in New Issue
Block a user