fix: un échec de lecture ne fabrique plus d'état durable et faux

userInbox avalait deux échecs, et chacun laissait des dégâts sur le disque.

À la lecture : la requête « quelle inbox ce compte possède-t-il » levait, on
journalisait, et on créait une inbox de plus. Deux associations pour un même
couple utilisateur/portée — vérifié en conditions, sdoc5 et sdoc6 coexistants.

À l'écriture : l'INSERT levait, on retournait quand même la référence en la
mettant en cache. Le propriétaire tenait sdoc5 pendant qu'un déposant résolvait
sdoc6. Il lit une boîte où nul n'écrit, ils écrivent dans une boîte que nul ne
lit.

Et un troisième que je n'avais pas vu : recordInbox avalait son propre INSERT
puis marquait son index en mémoire — l'inbox était associée mais refusée à la
session suivante, donc tous les dépôts rebondissaient, définitivement.

Le balayage demandé a trouvé la famille entière : onze sites de cette forme, un
catch qui journalise puis une exécution qui continue comme si la chose cherchée
était absente. Les huit autres corrigés vont d'une seconde racine de registre
créée sur budget épuisé, à un document public qui ne sert plus jamais sa clé.

La règle appliquée partout : seule une absence VÉRIFIÉE autorise à créer, et une
référence n'est remise à personne avant que son association ait atterri.

Les sites laissés échouent en fermeture — un refus, une liste vide — sans rien
écrire. Ils sont listés, pas oubliés.
This commit is contained in:
Sylvain Duchesne
2026-08-13 13:21:54 +02:00
parent 0f1e493e93
commit e32b6d04fc
6 changed files with 796 additions and 11 deletions
@@ -122,6 +122,13 @@ export function resetPublicStoreFetches(): void {
*
* Replacement, not addition, like every Header-branch register: one document has one
* current cap, and two would leave a fetcher picking between them.
*
* **Propagates a failed write.** This runs exactly once, at creation, and nothing exposes
* the cap afterwards — so a swallowed failure leaves a document that IS in a public store
* and serves nothing: every third party's {@link fetchReadCap} answers "no cap", the read
* guard refuses, and the document is unreadable by anyone but its creator, for good. Its
* caller `createEntityDoc` already refuses to hand back a reference whose bookkeeping did
* not land, for the same reason and in the same words.
*/
export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
const s = await session();
@@ -143,6 +150,7 @@ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
);
} catch (error) {
console.error(accessLogPrefix() + " exposeReadCap failed:", error);
throw error;
}
}