da6ef4b8b82bf92cf6ae95a0a716ab5f4f439883
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
54f8389e9e |
refactor(api): l'app nomme une personne ou un document, jamais une adresse d'inbox
L'app d'exemple a servi de juge, et elle a immédiatement montré ce que l'inventaire ne montrait pas : pour partager une note elle résolvait l'inbox du destinataire, pour lire ses messages elle résolvait l'adresse de la sienne. Deux gestes qu'aucune application n'aura à faire une fois la chose native — donc deux gestes qu'elle ne doit pas apprendre. - `shareCap(cap, toUser)` remplace `shareCap(cap, toInbox)`. Partager est un acte envers quelqu'un ; où est son inbox regarde la bibliothèque. - `inbox.readForDocument(doc)` : le propriétaire lit ses messages en nommant la note, comme le déposant la nomme pour en laisser un. - `storeRegistry.userInbox` et `documentInboxAddress` sortent de la surface publiée. Ils restent joignables en interne, où le shim en a besoin. Sortent aussi de `/polyfill`, chacun parce qu'une app qui code contre apprend ce qu'il faudra désapprendre : - `getCaps` / `CapRegistry` — la salle des machines. La question du consommateur est `capFor(doc)` : est-ce que je le détiens ? Le registre n'a ni successeur ni forme inerte ; ce qui s'appuie dessus sera à réécrire, pas à laisser en place. - `getCurrentUser` — une app sait qui elle a connecté ; le redemander à la bibliothèque est une commodité du wallet partagé. - `virtualUsers` / `IdentityStore` — se souvenir d'une identité entre deux sessions est aussi le travail de l'app en amont. L'écran d'accès persiste ce dont IL a besoin ; rien d'autre n'a à être exposé. Reste sur `/polyfill` ce qu'une app appelle vraiment : `configure` et `setCurrentUser`. Le reste y est du test ou de l'injection interne. 170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la bibliothèque, l'exemple et le harnais. |
||
|
|
3257afe8c0 |
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. |
||
|
|
b62bfe1e63 |
refactor: ouvrir une inbox est un acte de registre, et deux invariants rendus explicites
`openDocumentInbox` passe du shim aux registres de branche. Ce qu'il fait est de
la comptabilité du verifier : vérifier la propriété, enregistrer la moitié
lecture sur la branche User, publier l'adresse. Seule la création du document
support relève du shim, et elle est appelée, pas hébergée. En amont l'acte
équivalent est générer une paire de clés et commiter `AddInboxCap`.
Deux risques de migration signalés par le contrat interne, transformés en
invariants vérifiés plutôt que supposés :
- **Le couple `(document, inbox)`** est un littéral RDF séparé par une espace là
où l'amont a une structure typée (`AddInboxCapV0 { repo_id, overlay, priv_key }`).
L'espace est sûr parce qu'un NURI n'en contient pas — alphabet base64url et
segments `:` — mais c'était une propriété implicite. `encodeInboxCap` la
vérifie désormais : un découpage erroné classerait une inbox sous un document
tronqué et perdrait les dépôts sans erreur, la classe de panne que ce chemin a
déjà payée une fois.
- **Le namespace réservé** garantit qu'aucun identifiant utilisateur ne peut s'y
loger — sauf que `normalizeId` est injecté par le consommateur et que le
défaut de la bibliothèque ne fait que trimmer. Une collision ne serait pas
cosmétique : un utilisateur se retrouverait sur un compte d'infrastructure, à
lire et écrire des documents qui ne sont pas les siens. Vérifié à la
normalisation, avec un test qui simule un `normalizeId` hostile.
160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
|
||
|
|
107f9d1633 |
refactor(vocabulary): les noms publiés parlent la langue de la cible, et un test le tient
La correction de nomenclature du 2026-07-30 — en amont un *wallet* n'est qu'un trousseau, ce qui possède des stores est un **user** (un *site*) — s'était faite à la main. `walletInbox` y a échappé et a vécu des semaines, en faisant des dégâts : le nom rendait « une inbox par wallet » évident, masquant qu'un user en a **deux** en amont (repos de store public et protected, les deux seuls `AddInboxCap` du moteur). Une discipline appliquée à la main en oublie un ; un test non. D'où `test/vocabulary.test.ts` : tout nom publié est bâti sur des mots que la CIBLE emploie — vérifiés dans `nextgraph-rs` — ou porte un marqueur disant POURQUOI il n'existe qu'ici (`virtual`, `physical`, `shim`, `emulated`, `polyfill`), ce qui dit aussi quand il disparaît. Un échec n'est pas « renommer pour faire passer le test », c'est une question : la cible a-t-elle un mot pour ça ? la chose n'existe-t-elle qu'ici ? le mot est-il vraiment de la glue ? Ce que le test a trouvé, et les réponses : - `walletInbox` → `userInbox`, avec l'écart de cardinalité écrit noir sur blanc plutôt que caché par le nom. - `accounts` / `AccountRecord` / `AccountStorage` → `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage`, module `accounts.ts` → `virtual-users.ts`. « account » n'est pas de la cible : c'est notre mot pour l'utilisateur virtuel, et le marqueur le dit désormais. - `readModel` → la fonction `readUnion`, exposée directement. « model » n'était ni de la cible ni de la glue, et le namespace ne tenait qu'une fonction. - Le reste était du vocabulaire légitime à déclarer (`subject`, `base`, `schema`, `connected`, le modèle réactif de l'ORM). Corrigé au passage, sur signalement du contrat interne : l'en-tête d'`open-repo` justifiait son correctif par un mécanisme que le source contredit. Un repo absent de `self.repos` lève bien `RepoNotFound` (`engine/verifier/src/request_processor.rs:264,269`). Les 0 lignes observées viennent d'ailleurs — `Verifier::load` repeuple `self.repos` depuis le stockage sur un profil persistant (`verifier.rs:535-560`), et notre propre `readDoc` attrape toute erreur et rend `[]`. Le correctif est bon, le diagnostic écrit à côté ne l'était pas. 159 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker. |
||
|
|
e01a8dbab1 |
refactor(layout): séparer les registres de branche du shim
`store-registry.ts` portait deux destins dans un seul fichier de 1379 lignes. `emulated-verifier/branch-registers.ts` prend les compartiments durables — registre de caps de branche Store (`AddRepo`), registre de Links de branche User (`AddLink`), enregistrements d'inbox (`AddInboxCap`), adresses de branche Header — chacun nommant son mécanisme natif. Ils émulent la comptabilité du VERIFIER et survivent conceptuellement : à la migration le natif les reprend, seule notre représentation RDF disparaît. `shared-wallet/account-registry.ts` garde le shim proprement dit — indirection pointeur → doc-shim, résolution et provisionnement des comptes, cache. Aucun pendant amont, s'évapore en entier. Les imports croisés entre les deux sont délibérés et visibles : un registre a besoin du shim pour savoir À QUI il est, le shim classe la structure d'un user au moment où il le résout. Tout l'usage est en corps de fonction, donc le cycle de modules est inerte à l'évaluation. 157 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker. |
||
|
|
88914f50ae |
refactor(layout): ranger les modules par destin à la migration
Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement (`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est partagé — trois destins sans rapport. Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que l'`exports` du paquet et le code du consommateur ne bougent pas : - `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur, pas d'I/O. Survit comme connaissance. - `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé. - `emulated-verifier/` — les doublures de ce que le verifier fait nativement : possession, dépôt des caps, frontière, non-livraison, traitement des inbox, registres de branche, ouverture de repo. **C'est le dossier où diverger du modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de, jamais est — cette bibliothèque ne réside dans aucune couche de la cible, elle les référence. - `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de rester invisible depuis `surface/`. S'évapore, remplacé par rien. `store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la main ce que la frontière de dossier fait structurellement — c'est la meilleure preuve interne du bien-fondé de ce rangement. Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les scissions des modules à cheval suivent. 157 tests unitaires, typecheck src/test/e2e vert. |
||
|
|
5a7009bd75 |
fix(inbox): une inbox appartient à un document, jamais à plusieurs
Retour sur l'adresse par défaut livrée en
|
||
|
|
8a382f29f8 |
feat(inbox): l'inbox d'un document est adressable par tout détenteur
Répond au brief 2026-08-03 remonté depuis le consommateur. `documentInbox(doc)` répondait « quelle inbox est-ce que MOI je connais pour ce document » et en créait une quand la réponse était « aucune » : un tiers n'atteignait jamais l'inbox du propriétaire, il en obtenait une à lui, que personne ne lit, et son dépôt disparaissait sans erreur. C'est l'acte central du consommateur — s'inscrire à l'événement d'un autre — qui était silencieusement perdu. Lire une inbox et savoir où y déposer sont deux actes opposés, avec des publics opposés. Ils sont désormais deux fonctions : - `openDocumentInbox(doc)` — le PROPRIÉTAIRE ouvre une inbox dédiée. Refuse sur la PROPRIÉTÉ (lue depuis les branches Store), pas sur la possession du cap : un cap se reçoit, et un destinataire ne doit pas pouvoir rediriger vers lui les dépôts destinés au propriétaire. - `documentInboxAddress(doc)` — n'importe quel détenteur trouve où déposer. Ne crée jamais rien. L'adresse est publiée dès la CRÉATION, sur la branche Header émulée du document — un sujet réservé à l'intérieur du document, donc lisible par qui détient le document. Publier seulement le jour où le propriétaire ouvre une inbox dédiée laisserait une fenêtre pendant laquelle un tiers lit le document, ne trouve aucune adresse, et ne peut pas joindre le propriétaire du tout. Sur le coût mesuré par le brief (9m37 → 21m30) : il venait de la création d'un DOCUMENT supplémentaire par document. L'adresse publiée pointe vers l'inbox propre du propriétaire, qui existe déjà et s'amortit sur tous ses documents ; la création grandit d'un triple, pas d'un document. Le dépôt porte le document concerné, donc le propriétaire matérialise toujours par document. La forme « dérivable » du brief n'était pas disponible : notre inbox est un document, et un NURI dérivé nommerait un repo que `doc_create` n'a jamais créé. Le tout reflète la séparation d'amont : un déposant scelle avec la clé PUBLIQUE de l'inbox et n'a besoin de rien d'autre, seul le propriétaire détient la moitié privée — une adresse est donc publique par nature. `src/machinery.ts` : l'espace de noms `urn:ng-eventually:` que la bibliothèque se réserve, et le prédicat que le chemin de lecture utilise. La branche Header est le premier compartiment logé dans un document que le consommateur lit ; `read-model` écarte désormais tout sujet de cet espace, par SUJET et non par prédicat — ce qui couvre toutes les branches émulées, présentes et futures. Question ouverte du brief, tranchée : « une inbox de document adressable par tout détenteur » est une invention de cette bibliothèque, pas de l'amont — aucun document n'y a d'inbox, ni le store privé. Ce qui EST vérifié, c'est la forme qui rend l'anticipation défendable : `AddInboxCapV0` est clé par `repo_id`. Tests : le test qui validait « n'importe qui dépose » passait le NURI d'inbox au déposant par une variable du test — chemin qu'aucune app n'a. Réécrit avec les deux acteurs cloisonnés : le déposant reçoit le lien du document, qui est la seule chose qui circule dans ce modèle, et doit trouver l'adresse lui-même. Le fake `ng` gagne le SELECT de la branche Header et le `DELETE WHERE` (sans quoi un remplacement devenait une accumulation, précisément le bug qu'il évite). 157 tests unitaires, e2e 40/40 contre le broker en ligne. |
||
|
|
9d3e2d2bfe |
Fix documentation defects found by an adversarial review
Fifteen findings, all verified before acting. The ones that mattered: - Corrections added without updating what they corrected. §5's table still said a cap-less NURI is one "without :k:", two hundred lines after §4 established the discriminant is `r:`. Same shape of defect in the P1a report, which kept the sentence "it is the owner's keyring, upstream the keyring is the wallet" — the exact sentence §4quater declares wrong, and the one that produced a global in-memory keyring. - A wrong source citation: RootCapRefresh/BranchCapRefresh live in verifier/src/commits/mod.rs, not repo/src/commit.rs, and are no-op stubs. - Documentation describing deleted code: isolation.ts, discovery.readIndex, the global index, and an acceptance test that was dropped with discovery. - The P1a implementation report had aged into being wrong in four places (caps not persisted, inbox processing not started, plain string types, the :k: segment). It is dated, so it now carries a header saying what later lots overtook, rather than being rewritten. - vision.md stated "a document's data is stored encrypted" in the present tense. That is the target; here the cap value is the constant OK and nothing is encrypted. Said plainly now. - Prose left mangled by an earlier mechanical find-and-replace, in four places I had claimed were repaired. Also: reach.ts and connect.ts had no home in the permanent docs — the boundary and the connection sequence are now described in simulation.md, not only in a brief. |
||
|
|
ae9c32e271 |
Align the cap emulation on NextGraph's model, and confine it to a virtual user
Two batches, verified against nextgraph-rs throughout. P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>), the exact inversion of key possession. It is now possession: `capFor(nuri)` is the only question, there is no principal parameter anywhere, and nothing turns a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link deposit; receiving needs no operation. `Nuri` and `ReadCap` are template literal types, so passing a bare reference where a cap belongs is a compile error, with runtime guards behind it for JavaScript callers. The virtual user boundary. Every access function is now confined to the connected user, through two rules on one criterion (possession), implemented in two places so a lapse in either is caught by the other: authorization at the passage points, and "do not even attempt" at the callers. The polyfill's own machinery moved to physical.ts — unguarded, never exported — which replaced an exemption list: the machinery no longer gets waved through the guard, it calls something the guard never saw. Removed, as emulating capabilities the target does not have: - discovery.ts and its global index. There is no discovery in NextGraph; you follow links. It also pooled user data across wallets. - the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts, loadShim), which was cross-user enumeration by construction. - resolveInboxAnchor, a single inbox common to every user. Caps are now stored where NextGraph stores them, and read back rather than recomputed: AddRepo on the store's Store branch for documents a user creates, AddLink on its User branch for caps received. Inboxes belong to someone — the user's own, plus one per document — and connecting a user drains them all; that is the library's job, not the app's. Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO have a register (AddLink), contrary to what this repo's notes claimed; and "wallet" upstream means keyring — what owns three stores is a user, so the vocabulary follows. The cap value is the constant OK: the only question the emulation answers is whether a cap is held. P1b replaces that one constant with a real key. After this the shape is right and the isolation is still fake. Nothing here may be described as anonymous or private. |