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:
@@ -83,7 +83,7 @@ import { hasReadCap, isNuri } from "../model/nuri";
|
||||
import { mintCap } from "../emulated-verifier/caps";
|
||||
import { mustNotAttempt } from "../emulated-verifier/reach";
|
||||
import { accessLogPrefix, logStage, shortNuri } from "./access-log";
|
||||
import type { Nuri, ReadCap, Scope } from "../model/types";
|
||||
import type { InboxScope, Nuri, ReadCap, Scope } from "../model/types";
|
||||
|
||||
// --- sharedWalletShim model ----------------------------------------------
|
||||
|
||||
@@ -116,7 +116,7 @@ export const P = {
|
||||
docProtected: `${SHIM}:docProtected`,
|
||||
docPrivate: `${SHIM}:docPrivate`,
|
||||
contains: `${SHIM}:contains`, // scope-index → entity document NURI
|
||||
docInbox: `${SHIM}:docInbox`, // account → ITS OWN inbox document
|
||||
docInbox: `${SHIM}:docInbox`, // (user, inboxScope) → ITS inbox document
|
||||
link: `${SHIM}:link`, // user branch → a ReadCap received for an EXTERNAL document
|
||||
readCap: `${SHIM}:readCap`, // store branch → the ReadCap of a document IN this store
|
||||
inboxCap: `${SHIM}:inboxCap`, // user branch → an inbox this user may READ
|
||||
@@ -813,11 +813,29 @@ const inboxCache = new Map<string, Nuri>();
|
||||
* Concurrency-safe (see {@link inboxInFlight}), and a fork is reconciled the same
|
||||
* content-addressed way as everything else ({@link canonicalDoc}).
|
||||
*
|
||||
* At migration this becomes the identity's native inbox and the resolution moves
|
||||
* here — the consumer-facing act (deposit to an inbox, process my own) is unchanged.
|
||||
* ── TWO inboxes, because upstream a user has two ─────────────────────────
|
||||
* A *site* carries an inbox on its **public** store repo and another on its
|
||||
* **protected** one — the only two `AddInboxCap` commits in the engine
|
||||
* (`engine/verifier/src/site.rs:127-152`), `new_store_default` attaching one solely
|
||||
* `if !private` (`engine/verifier/src/verifier.rs:2994`). They are addressed
|
||||
* separately right down to the contact records, which pick their predicate from the
|
||||
* profile being reached: `ng:site_inbox` for a public profile, `ng:protected_inbox`
|
||||
* otherwise (`engine/verifier/src/inbox_processor.rs:787,823-824`).
|
||||
*
|
||||
* This function used to expose ONE, which was a cardinality this library invented.
|
||||
* Corrected 2026-08-03 by walking the cascade: neither the JS ORM nor the wasm binding
|
||||
* says anything about inboxes — `@ng-org/web` has no method containing "inbox" and the
|
||||
* session exposes none — so the engine's model is what decides, and it says two.
|
||||
*
|
||||
* **The private store has none**, hence {@link InboxScope} rather than `Scope`: asking
|
||||
* for a private inbox is not a lookup that returns nothing, it is a question the model
|
||||
* has no meaning for.
|
||||
*
|
||||
* At migration these become the site's native store inboxes and the resolution moves
|
||||
* there — the consumer-facing act (deposit to an inbox, process my own) is unchanged.
|
||||
*/
|
||||
export async function userInbox(id: string): Promise<Nuri> {
|
||||
const key = accountKey(id);
|
||||
export async function userInbox(id: string, scope: InboxScope): Promise<Nuri> {
|
||||
const key = `${accountKey(id)}\u0000${scope}`;
|
||||
const cached = inboxCache.get(key);
|
||||
if (cached) {
|
||||
fileOwnInbox(id, cached);
|
||||
@@ -831,12 +849,15 @@ export async function userInbox(id: string): Promise<Nuri> {
|
||||
const shimDoc = await resolveShimDoc();
|
||||
await ensureAccount(id); // the account must exist before it can own an inbox
|
||||
const subj = accountSubject(id);
|
||||
// One triple per (user, scope): the two inboxes are distinct documents, as the two
|
||||
// store repos that carry them are distinct upstream.
|
||||
const pred = `${P.docInbox}:${scope}`;
|
||||
try {
|
||||
// The doc-shim is machinery: this reads WHICH inbox a virtual user owns,
|
||||
// which is exactly the kind of question that cannot be confined to that user.
|
||||
const res = await physicalQuery(
|
||||
s.sessionId,
|
||||
`SELECT ?d WHERE { <${subj}> <${P.docInbox}> ?d }`,
|
||||
`SELECT ?d WHERE { <${subj}> <${pred}> ?d }`,
|
||||
undefined,
|
||||
shimDoc,
|
||||
"userInbox",
|
||||
@@ -856,7 +877,7 @@ export async function userInbox(id: string): Promise<Nuri> {
|
||||
try {
|
||||
await physicalUpdate(
|
||||
s.sessionId,
|
||||
`INSERT DATA { <${subj}> <${P.docInbox}> "${escapeLiteral(doc)}" }`,
|
||||
`INSERT DATA { <${subj}> <${pred}> "${escapeLiteral(doc)}" }`,
|
||||
shimDoc,
|
||||
"userInbox",
|
||||
);
|
||||
@@ -864,7 +885,7 @@ export async function userInbox(id: string): Promise<Nuri> {
|
||||
console.error(accessLogPrefix() + " userInbox persist failed:", error);
|
||||
}
|
||||
inboxCache.set(key, doc);
|
||||
logStage("userInbox(" + key + ") → " + shortNuri(doc));
|
||||
logStage("userInbox(" + key + "/" + scope + ") → " + shortNuri(doc));
|
||||
return doc;
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user