feat: un document en store public sert son ReadCap, une référence nue suffit
Le modèle amont est explicite dans `PublicRepoLinkV0` : le lien ne porte AUCUN `read_cap`, et son commentaire dit pourquoi — *"The latest ReadCap of the branch will be downloaded from the outerOverlay, if the peer brokers listed below allow it […] the public site are served differently by brokers"* (engine/net/src/types.rs:5098). La clé n'est pas remise par un émetteur : elle est donnée par le réseau à qui la demande, parce que le broker a épinglé l'overlay externe (`expose_outer`). La bibliothèque refusait jusqu'ici la forme sans cap quel que soit le store. Sûr dans le bon sens, mais une application ne pouvait pas exprimer « fais circuler, la référence suffit » — le seul acte que le modèle rend gratuit — et son unique contournement était de distribuer la clé, ce qui détruit la confidentialité composable. `emulated-verifier/public-store.ts` émule le mécanisme SANS toucher à la garde. La possession reste l'unique critère : un document public est lisible non par exception mais parce que son cap est *obtenable*. Chaque porte de lecture demande d'abord (`readUnion`, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`), puis le chemin ordinaire s'applique. Lire n'est pas écrire. Ce que le store sert est un droit de LECTURE : `learnFromPublicStore` le classe à part et `assertMayWrite` refuse l'écriture dessus. Sans cela une référence nue achetait une écriture, ce qu'aucun store amont n'accorde. Autres conséquences : - `recordInPublicStore` (marquer + frapper) devient `markInPublicStore` (marquer). Frapper un second cap à côté de celui qu'on vient de télécharger donnerait deux clés différentes le jour où la constante devient un secret. - `hasCap` quitte la porte polyfill : il se lisait « ai-je le droit de lire ceci ? » et un document public y répondait `false` jusqu'à ce qu'on demande son cap. Aucun appelant hors des tests. - Les tests cross-user ne font plus traverser de cap par une variable JS : Bob n'obtient que la référence nue, comme une vraie application. Écarts documentés plutôt que masqués : le pari sur un modèle DÉCLARÉ (`expose_outer` est câblé à `false` côté client et `ExtTopicSyncReq` est `unimplemented!()`), la découverte limitée à ce qu'on sait déjà nommer, `useShape` qui n'a pas d'await à dépenser, et l'absence de `locator`. 179 tests unitaires, e2e 42/42 contre le broker en ligne.
This commit is contained in:
+13
-11
@@ -345,7 +345,7 @@ Three ways a cap arrives, and there are no others:
|
||||
recompute anything: that is the whole reason for storing them, and it is what lets
|
||||
a **fresh session** read its own documents again with nothing re-declared — the
|
||||
durability the old in-memory ACL faked and lost every reload.
|
||||
- **Delivery.** `shareCap(cap, toInbox)` deposits one document's cap into one
|
||||
- **Delivery.** `inbox.share(doc, toUser)` deposits one document's cap into one
|
||||
recipient's inbox; `inbox.read` applies it inline, exactly as the recipient's own
|
||||
verifier applies queued messages upstream. **Receiving needs no operation** — a
|
||||
consumer already watching its inbox gets them, and the resulting change
|
||||
@@ -364,17 +364,19 @@ another name.
|
||||
- **`setCurrentUser(id)` (`polyfill.ts`)** — the SDK's "current identity" call.
|
||||
It selects *whose* caps are consulted, lazily, so the delivered subset always
|
||||
reflects the identity in effect at read time.
|
||||
- **`shareCap(cap, toInbox)`** — the one sharing act the lib exposes. Recipients
|
||||
- **`inbox.share(doc, toUser)`** — the one sharing act the lib exposes. Recipients
|
||||
are addressed as **inboxes**, which `inbox.post(targetInbox)` already does here;
|
||||
there is no `PrincipalId` in this surface, because that notion exists nowhere
|
||||
upstream. Reaching several recipients means calling it once per inbox, which is
|
||||
what the real model does too (each delivery is sealed to one recipient).
|
||||
- **`getCaps().publishRepoLink(doc)`** — upstream `RepoLinkV0`: a shareable link
|
||||
**whoever receives it** can open. Put the *link* in what you make discoverable, not
|
||||
the bare NURI, or no reader can open it. Publication is **not recursive**: a public
|
||||
document may reference private ones, and the reference grants nothing on what it
|
||||
references — which is what lets a public object point at a private identity without
|
||||
disclosing it.
|
||||
- **A document created in the `public` scope** needs no sharing act at all. The store
|
||||
serves its ReadCap to whoever asks (`emulated-verifier/public-store.ts`, emulating
|
||||
*"the latest ReadCap will be downloaded from the outerOverlay"* — `PublicRepoLinkV0`,
|
||||
`engine/net/src/types.rs:5098`), so what an application circulates is the **bare
|
||||
reference**, exactly as it will after migration. Never recursive: a public document
|
||||
may reference private ones, and the reference grants nothing on what it references —
|
||||
which is what lets a public object point at a private identity without disclosing it.
|
||||
And never a write right: what the store serves is a read cap.
|
||||
|
||||
Upstream, directed delivery is a **gap, not a disagreement**: `ContactDetails.read_cap`
|
||||
exists, but the message construction is `unimplemented!()`, its only caller passes
|
||||
@@ -442,8 +444,8 @@ natively at migration); the read side is what makes isolation observably active.
|
||||
|
||||
Isolation is enforced by the per-document ReadCap (`emulated-verifier/caps.ts` + `emulated-verifier/read-filter.ts`)
|
||||
alone: the access unit is the document (`@graph` = repo), and the only acts are
|
||||
possession-shaped (`createEntityDoc` files a cap, `shareCap` delivers one,
|
||||
`publishRepoLink` emits an openable link). Because the consumer application writes
|
||||
possession-shaped (`createEntityDoc` files a cap, `inbox.share` delivers one, a public
|
||||
store serves one to whoever asks). Because the consumer application writes
|
||||
one document per entity, the per-document cap discriminates at entity granularity —
|
||||
the target's behaviour.
|
||||
|
||||
@@ -512,7 +514,7 @@ deposit — **whose JS name and signature are not known**, since none is exposed
|
||||
announced — and the read side is served by the recipient's own verifier unsealing
|
||||
queued messages inline.
|
||||
The inbox + watcher is the one deposit/read mechanism a consumer reuses for its own
|
||||
purposes — a registration/deposit, a cap delivery (`shareCap`), a link handed to
|
||||
purposes — a registration/deposit, a cap delivery (`inbox.share`), a link handed to
|
||||
someone — same `post` API, same watcher.
|
||||
|
||||
## The virtual user boundary (`emulated-verifier/reach.ts` + `shared-wallet/physical.ts`)
|
||||
|
||||
Reference in New Issue
Block a user