feat(inbox): un utilisateur a DEUX inboxes, comme en amont

Tranché par la cascade plutôt qu'en attendant une réponse : le niveau 3 (ORM) ne
dit rien des inbox, le niveau 2 non plus — `@ng-org/web` n'expose aucune méthode
contenant « inbox » et la session n'en publie aucune. C'est donc le modèle du
moteur qui décide, et il dit DEUX : un site porte une inbox sur son repo de store
public et une autre sur son protégé (`engine/verifier/src/site.rs:127-152`), les
seuls `AddInboxCap` du moteur, `new_store_default` n'en posant une que
`if !private`. Elles sont adressées séparément jusque dans les enregistrements de
contact, qui choisissent leur prédicat selon le profil visé — `ng:site_inbox` pour
un profil public, `ng:protected_inbox` sinon
(`engine/verifier/src/inbox_processor.rs:787,823-824`).

`userInbox(id)` en exposait une : une cardinalité que cette bibliothèque avait
inventée, et que le nom `walletInbox` avait contribué à masquer. Elle prend
désormais le scope, et le store PRIVÉ n'en a pas — d'où `InboxScope` plutôt que
`Scope` : demander l'inbox privée n'est pas une recherche qui ne rend rien, c'est
une question sans référent dans le modèle, et le type l'interdit.

`myInboxes` énumère les deux, `isOwnInbox` reconnaît les deux. Le shim garde un
triple par (user, scope).

160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
This commit is contained in:
Sylvain Duchesne
2026-08-04 16:19:49 +02:00
parent b62bfe1e63
commit 3257afe8c0
16 changed files with 103 additions and 45 deletions
@@ -262,7 +262,7 @@ test("Bob: reads the public document, sees the reference, and cannot read throug
test("Charlie: same public document, same reference — and he reads through it", async () => {
inject();
const { protDoc, pubDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
const CHARLIE_INBOX = await userInbox("charlie");
const CHARLIE_INBOX = await userInbox("charlie", "protected");
// Alice decides Charlie may read that ONE document, and delivers its cap to his
// inbox. She names no principal to the registry; she addresses an inbox.
@@ -282,7 +282,7 @@ test("Charlie: same public document, same reference — and he reads through it"
test("the ONLY difference between Bob and Charlie is each of them holds", async () => {
inject();
const { protDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
const CHARLIE_INBOX = await userInbox("charlie");
const CHARLIE_INBOX = await userInbox("charlie", "protected");
setCurrentUser("alice");
await shareCap(protCap, CHARLIE_INBOX);
@@ -305,7 +305,7 @@ test("the ONLY difference between Bob and Charlie is each of them holds", async
test("dynamic: a cap delivered to Bob's inbox makes the refused document readable, and signals it", async () => {
inject();
const { pubDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
const BOB_INBOX = await userInbox("bob");
const BOB_INBOX = await userInbox("bob", "protected");
setCurrentUser("bob");
getCaps().learn(pubLink);
@@ -361,7 +361,7 @@ test("a bare reference to the PUBLIC document is not enough either — the link
test("a Link is APPLIED durably: the cap survives with the inbox emptied", async () => {
const ng = inject();
const { protDoc, protCap } = await aliceSetsUpHerDocuments();
const bobInbox = await userInbox("bob");
const bobInbox = await userInbox("bob", "protected");
setCurrentUser("alice");
await shareCap(protCap, bobInbox);
@@ -406,7 +406,7 @@ test("a document has its own inbox: anyone deposits, only the owner reads", asyn
setCurrentUser("alice");
const doc = await createEntityDoc("alice", "public");
const aliceInbox = await openDocumentInbox(doc);
expect(aliceInbox).not.toBe(await userInbox("alice"));
expect(aliceInbox).not.toBe(await userInbox("alice", "protected"));
const link = capFor(doc)!; // the repo link alice circulates — links DO travel
// Bob RESOLVES the address himself, from the document. The only thing he is handed
@@ -497,7 +497,7 @@ test("connecting drains BOTH levels: the user's inbox and its documents'", async
const protDoc = await createEntityDoc("alice", "protected");
const pubDoc = await createEntityDoc("alice", "public");
const docInbox = await openDocumentInbox(pubDoc);
const aliceInbox = await userInbox("alice");
const aliceInbox = await userInbox("alice", "protected");
// Two deposits, one at each level, both made by someone else.
setCurrentUser("carol");
@@ -519,8 +519,8 @@ test("connecting drains BOTH levels: the user's inbox and its documents'", async
test("a third party resolves another user's inbox (the wallet level)", async () => {
inject();
setCurrentUser("alice");
const aliceView = await userInbox("alice");
const aliceView = await userInbox("alice", "protected");
setCurrentUser("bob");
const bobView = await userInbox("alice");
const bobView = await userInbox("alice", "protected");
expect(bobView).toBe(aliceView);
});