fix: une liste qu'on ne peut pas ouvrir n'est pas une liste

listMyEntityDocs rendait la liste des documents même quand la lecture des clés
échouait. Vu de l'appelant, une liste dont les documents ne s'ouvrent pas est
INDISCERNABLE d'une liste dont ils s'ouvrent : rien ne signale la différence
jusqu'à une lecture ultérieure qui revient vide, la cause étant alors loin
derrière.

C'était un choix délibéré — ne pas transformer un appel publié en levée, la
liste étant déjà en main à ce moment-là. C'est précisément ce qui en faisait une
demi-vérité plutôt qu'un raccourci. Et c'était le dernier membre connu de la
famille qui a produit une panne chez une application cette semaine.

Les deux lectures sur lesquelles l'appel repose remontent désormais : la branche
Main dit quels documents sont là, la branche Store dit ce qui ouvre chacun. Un
tableau vide signifie donc que ce compte n'a rien créé dans cette portée, et
jamais que le store n'a pas été lu.

readUserStore avalait le même échec pour son propre compte ; son autre appelant,
ownsDocument, garde le comportement actuel par un catch explicite et documenté —
toutes ses réponses étant des refus, il échoue en fermeture, ce qui est la
règle qu'e32b6d0 avait posée.

C'est un changement de comportement d'un appel publié, donc le contrat le dit,
et docs/api-contract.md aussi.

Au passage, deux citations pourries corrigées en citant un SYMBOLE plutôt qu'une
ligne — types.rs:4251 désignait DialogRequest et non Link, index.d.ts:138
désignait const ng et non le type NG. Les douze autres références numériques du
voisinage ont été vérifiées : aucune n'avait bougé.
This commit is contained in:
Sylvain Duchesne
2026-08-17 08:59:34 +02:00
parent 43aadbeb45
commit 90712e0ad0
8 changed files with 228 additions and 44 deletions
@@ -200,7 +200,8 @@ export async function publishInboxAddress(doc: Nuri, inbox: Nuri): Promise<void>
* "I own nothing in this store" against "every document I own is invisible and nothing
* said so". {@link restoreOwnCaps} is the caller that cannot survive the confusion, since
* its whole contract is that the connection either did the restore or says it did not.
* A caller that genuinely prefers to carry on catches it itself, and one of them does.
* `listMyEntityDocs` used to be the exception; it stopped being one on 2026-08-17, because
* a listing handed back without its keys is the same confusion one layer up (see it).
*/
// @provenance readStoreCaps kind=aligned level=1 ref=engine/repo/src/types.rs:AddRepoV0 — the replay of the Store branch — what reloads a store's documents with their keys
export async function readStoreCaps(storeDoc: Nuri): Promise<ReadCap[]> {
@@ -340,6 +341,15 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
* register of the documents it created — across the three scopes, which is the only
* place that records authorship. Holding a cap is NOT ownership: a cap can be
* received, and a recipient must not be able to open an inbox on what it merely reads.
*
* The ONE caller that catches `readUserStore` rather than propagating it, deliberately.
* Every answer this function feeds is a REFUSAL — the write guard (`reach.mustNotAttempt`)
* and `openDocumentInbox` both only ever ask it for permission — so a store that did not
* answer closes the door, which is the safe side and writes nothing. That is the ruling
* made when this family was swept (commit `e32b6d0`): the sites that fail CLOSED were
* left, the ones that answer a caller with a fabricated value were not. The refusal names
* the wrong reason ("not yours" for "could not tell"), which is the price, and it is
* bounded — nobody acts on this answer except by being denied.
*/
// @provenance ownsDocument kind=aligned level=1 ref=engine/repo/src/types.rs:AddRepoV0 — authorship is what the Store branch records; holding a cap is NOT ownership, since a cap can be received
export async function ownsDocument(doc: Nuri): Promise<boolean> {
@@ -350,7 +360,11 @@ export async function ownsDocument(doc: Nuri): Promise<boolean> {
for (const scope of ["public", "protected", "private"] as const) {
const store = storeOf(record, scope);
if (!store) continue;
if ((await readUserStore(store)).includes(doc)) return true;
try {
if ((await readUserStore(store)).includes(doc)) return true;
} catch (error) {
console.error(accessLogPrefix() + " ownsDocument: store unreadable:", error);
}
}
return false;
}