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:
@@ -298,7 +298,7 @@ async function main(): Promise<void> {
|
||||
const t = Date.now();
|
||||
const r = await sdk<any>(frame, "documentInboxDeposit", "@owner-" + t, "@depositor-" + t);
|
||||
check(
|
||||
"the depositor RESOLVES the same inbox from the document, deposits into it, and the address stays out of the data",
|
||||
"the depositor holds only the BARE reference, resolves the same inbox from it, deposits, and the address stays out of the data",
|
||||
r.sameInbox === true &&
|
||||
r.openRefused === true &&
|
||||
JSON.stringify(r.deposits) === JSON.stringify([{ viaPostToDocument: true }, { joining: true }]) &&
|
||||
@@ -383,15 +383,15 @@ async function main(): Promise<void> {
|
||||
const linkOpensPublic =
|
||||
r.strangerWithLinkView.length === 1 && r.strangerWithLinkView.includes("public-item");
|
||||
check(
|
||||
"owner reads held docs only; stranger reads nothing; the repo link opens the published one",
|
||||
"the read-filtered view decides on possession alone: owner sees what he holds, a stranger nothing, and a filed cap opens it",
|
||||
ownerReadsHeld && ownerMissesUnheld && strangerReadsNothing && linkOpensPublic,
|
||||
`owner=${JSON.stringify(r.ownerView)} stranger=${JSON.stringify(r.strangerView)} withLink=${JSON.stringify(r.strangerWithLinkView)}`,
|
||||
`owner=${JSON.stringify(r.ownerView)} stranger=${JSON.stringify(r.strangerView)} withCap=${JSON.stringify(r.strangerWithLinkView)}`,
|
||||
);
|
||||
});
|
||||
await step("shareCap: a cap delivered to an inbox reveals the doc", async () => {
|
||||
const r = await sdk<any>(frame, "capsShareCap", "@friend-" + Date.now());
|
||||
check(
|
||||
"shareCap → inbox processed → the shared doc becomes readable, and the delivery is not surfaced",
|
||||
"share → inbox processed → the shared doc becomes readable, and the delivery is not surfaced",
|
||||
r.before === 0 && r.after === 1 && r.surfacedDeposits === 0,
|
||||
`before=${r.before} after=${r.after} surfaced=${r.surfacedDeposits}`,
|
||||
);
|
||||
|
||||
@@ -837,10 +837,11 @@ const identity = new IdentityStore(
|
||||
];
|
||||
setCurrentUser("owner-O");
|
||||
getCaps().open("did:ng:o:protdoc", "protected");
|
||||
const link = getCaps().recordInPublicStore("did:ng:o:pubdoc");
|
||||
const link = getCaps().open("did:ng:o:pubdoc", "public");
|
||||
const ownerView = [...(libUseShape(null, null) as Iterable<any>)].map((i) => i.v);
|
||||
// A stranger holds nothing — including the PUBLISHED document, until the repo
|
||||
// link reaches them (§5: whoever has the URL reads the content).
|
||||
// A stranger holds nothing, and this VIEW asks nobody: it is pure possession, with
|
||||
// no round-trip to spend (see `read-filter.ts`). That a public store would serve
|
||||
// the cap is proven on the read paths, not here.
|
||||
setCurrentUser("stranger");
|
||||
const strangerView = [...(libUseShape(null, null) as Iterable<any>)].map((i) => i.v);
|
||||
getCaps().learn(link);
|
||||
@@ -861,20 +862,19 @@ const identity = new IdentityStore(
|
||||
* owner opens it, a third party RESOLVES its address from the document itself and
|
||||
* deposits, the owner reads it back.
|
||||
*
|
||||
* The point of the step is the resolution: nothing hands `depositorId` the address.
|
||||
* It gets the document's link (which is what circulates in this model) and must find
|
||||
* where to deposit on its own — which is exactly what a consumer app has to do, and
|
||||
* what a unit test passing the NURI through a variable cannot prove.
|
||||
* The point of the step is the resolution: the depositor is handed the document's
|
||||
* BARE reference — the only thing an application circulates — and must find where to
|
||||
* deposit on its own. It reads the document at all because the document sits in a
|
||||
* public store, which serves its read cap to whoever asks (`public-store.ts`); no key
|
||||
* crosses the identity boundary, here or in any real application.
|
||||
*/
|
||||
async documentInboxDeposit(ownerId: string, depositorId: string) {
|
||||
registryInternals.resetRegistryCache();
|
||||
setCurrentUser(ownerId);
|
||||
const doc = await storeRegistry.createEntityDoc(ownerId, "public");
|
||||
const ownerInbox = await storeRegistry.openDocumentInbox(doc);
|
||||
const link = getCaps().capFor(doc)!; // out-of-band: the harness plays 'the owner sent it'
|
||||
|
||||
setCurrentUser(depositorId);
|
||||
getCaps().learn(link);
|
||||
const resolved = await documentInboxAddress(doc);
|
||||
// The one-call form an app actually uses: it names the DOCUMENT, never an inbox.
|
||||
await inbox.postToDocument(doc, { payload: { viaPostToDocument: true }, ts: 900 });
|
||||
|
||||
Reference in New Issue
Block a user