docs: ce qu'un NURI transporte, et les quatre besoins d'un lien
Une discussion de conception a calé plusieurs fois sur une séparation supposée entre « un NURI » et « un lien ». Elle n'existe pas, et l'avoir crue est ce qui a fait livrer un `linkTo` faux sur trois points à la fois. `NuriV0` porte exactement ce qu'un lien porte — `target`, `overlay`, `access: Vec<NgAccessV0>`, `locator` — et `NgLinkV0` en est la forme structurée. La vraie question n'est donc pas « NURI ou lien » mais ce qu'on met dans `access` et `locator`. Nuance qui compte pour tout ce qui s'imprime : le TYPE a tous les emplacements, la GRAMMAIRE de chaîne ne les expose pas tous. `access` est une LISTE de formes d'accès (`ReadCap | Token | ExtRequest | Key | Inbox | Topic`), donc une liste VIDE est un état légitime et courant : la référence NOMME sa cible et n'accorde rien. D'où les quatre besoins, chacun avec ce que la référence doit porter : nommer (access vide), donner à lire (+ ReadCap, irréversible), faire trouver un document public (overlay/locator, aucune clé), joindre depuis ailleurs (locator, sans quoi un inconnu n'ouvre rien). Trois confusions nommées parce que chacune a été faite ici : « sans clé » ne veut pas dire « public » ; nommer n'est pas lire et c'est l'acte PAR DÉFAUT (ce que notre test central prouve déjà) ; rien n'est vérifié à l'accès, donc une référence ne peut pas être neutre en droits — ce qu'on y met EST l'autorisation, et un lien diffusé ne se révoque pas. Et la divergence la plus lourde, qui n'était consignée nulle part : publier signifie ICI distribuer une clé, alors qu'en amont un document public est lisible parce que son STORE l'est et que les brokers le servent. L'émulation est trop stricte, pas inversée — elle sous-accorde — mais un consommateur ne doit pas en conclure que publier est un acte de distribution de clé, ni attendre quoi que ce soit de per-lecteur sur un document public.
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
# Document links — what travels when an application shares a document
|
||||||
|
|
||||||
|
**Established 2026-08-06**, by direct reading of the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16), the installed `@ng-org/web@0.1.2-alpha.13` declarations, and this repo's docs. Layer numbers per `README.md` § *The three references* (1 = engine, 2 = wasm binding / Rust SDK, 3 = ORM). `file:line` references are dated — navigate by symbol when they drift. Epistemic labels follow `docs/api-contract.md`.
|
||||||
|
|
||||||
|
**The question.** Alice writes a note; she wants Bob to open it. Bob is not necessarily known to her, and the sharing may cross a channel NextGraph knows nothing about — an email, a printed QR code, a web page. Does the model contain a value that travels, and if so, what does an application legitimately hold and pass around?
|
||||||
|
|
||||||
|
**The verdict.** Yes — and it is not a side feature: circulating a link is the model's only distribution mechanism ("you cannot discover, you can only follow links" — PO, 2026-07-30, `docs/readcap-and-nuri-model.md` §4ter-bis). The engine declares a complete link family (`NgLink`) whose doc comments describe out-of-band circulation as the normal case, and even names a durable "PermaLink" governed by a permission. But **nothing constructs any of it yet**, the repo-level *string* form an application would put in an email does not exist at any layer, and the value differs by scope: a link to a protected document carries the key, a link to a public document carries none.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Does a real application need this?
|
||||||
|
|
||||||
|
Yes, and not as a convenience. The two candidate paths in the framing collapse into one gesture at the model level: *publishing is two acts — place the data, then circulate the link* (§4ter-bis), and the inbox is merely one channel for that circulation ("post it into inboxes, **or** put it somewhere already reachable by the people concerned"). Directed delivery (path 1) is the same Link payload sealed to a known recipient's inbox; the email/QR case (path 2) is the same payload over a channel the recipient's identity never touches. Since there is no discovery — no index, no registry, nothing to find — a document that never has a link circulated for it has **no audience at all**. An application that lets users share anything therefore handles links; the only question is what the value is.
|
||||||
|
|
||||||
|
## 2. What the model actually offers
|
||||||
|
|
||||||
|
### Exists and is exercised
|
||||||
|
|
||||||
|
- **A self-contained share-URL for objects, commits and files.** Grammar: `did:ng[:o:{repo}]:v:{overlay}(:[cj]:{id}:k:{key})+[:s:{id}:k:{key}]:l:{locator}` (`RE_OBJECTS`, `engine/net/src/types.rs:63-65`); parser `NuriV0::new_for_readcaps` (`engine/net/src/app_protocol.rs:493-548`); one caller, `ngcli get` (`bin/ngcli/src/main.rs:430-460`), which connects **anonymously** to the broker named in the URL's `:l:` locator, fetches blocks with `ExtObjectGetV0`, and decrypts locally with the keys carried in the URL. Level 1 + CLI, working end to end: a stranger holding only the string reads the content. It addresses *frozen* content (objects, files, a commit) — not a live document.
|
||||||
|
- **Shareable values at level 2 — for profiles and wallets, not documents.** `get_qrcode_for_profile(session_id, _public, size)` and its receiving half `import_contact_from_qrcode` (`index.d.ts:96,106`), payload `NgQRCodeProfileSharingV0 { inbox, profile, name, email }` (`engine/net/src/types.rs:5227-5233`); `wallet_export_get_qrcode` (`index.d.ts:332`; `sdk/rust/src/local_broker.rs:2074`, with textcode and rendezvous variants). Precedent that "a value crosses an out-of-band channel" is SDK-real — no document instance of it exists.
|
||||||
|
- **NURIs are URL-shaped and the app opens them.** The app layer routes `/did:ng:*` to a `NURI` component (`app/ui-common/src/App.svelte:66`); the component is absent from this clone (app mid-rebuild), and its historical version (git `077ba626`, old `ng-app/src/routes/NURI.svelte`) deep-links only documents the **connected user already holds**. An in-app precedent for "a document reference is a URL", not for stranger-opening.
|
||||||
|
|
||||||
|
### Exists, unexercised — the declared model
|
||||||
|
|
||||||
|
- **The link family.** `NgLinkV0 = Repo | PublicRepo | Branch | Object` (`engine/net/src/types.rs:5206-5217`). `RepoLinkV0 { id, read_cap, overlay, peers }` (`:5062-5079`), whose comment states the flow: *"In most cases, the link is shared and then the recipient opens it and subscribes soon afterward"* (`:5059`). `PublicRepoLinkV0 { repo, branch?, heads, snapshot?, public_store, peers }` — **no `read_cap` field** (`:5105-5127`). `ReadBranchLinkV0` (`:5142`), `ObjectLinkV0` (`:5176`). **Zero constructors and zero consumers in the entire workspace** (grep over `engine/`, `sdk/`, `bin/`, `app/`): the family is a declared model, not a feature.
|
||||||
|
- **The NURI type has exactly a link's slots — the string grammar does not.** `NuriV0 { target, overlay, access: Vec<NgAccessV0>, locator: Option<Locator> }` (`engine/net/src/app_protocol.rs:181-195`); `NgAccessV0::ReadCap(ReadCap)` (`:55`) has **zero consumers anywhere**. The `r:` segment (a repo-level ReadCap, `BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`) is *produced* for display (`engine/verifier/src/verifier.rs:278,320`) and **parsed by nothing**; `NuriV0::new_from` (`app_protocol.rs:643-737`) accepts only cap-less forms plus the file/commit `:k:` forms. The only parsed cap+locator grammar is the object form above.
|
||||||
|
- **The recipient's mechanics exist and work — short of the external case.** `Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`, `pub(crate)`) is exercised for the user's own stores and the `AddRepo` replay (`engine/verifier/src/commits/mod.rs:660`); the one call site that would open an **external** repo from a received cap is commented out (`engine/verifier/src/inbox_processor.rs:469-481`, social-query forwarder). The durable filing register for a received link exists: `AddLink { read_cap }` on the User branch, *"so that a user can share with all its device a new Link they received"*, external repos only (`engine/repo/src/types.rs:1934-1950`).
|
||||||
|
- **Serving the public store to strangers is broker-plumbed and client-disabled.** `PinRepoV0.expose_outer` exists through the broker's storage (`engine/net/src/types.rs:3123`; `engine/broker/src/server_storage/core/overlay.rs:103-127`), but the client hard-codes `expose_outer: false` in both `PinRepo` constructors (`engine/net/src/actors/client/pin_repo.rs:51,79`), and `ExtTopicSyncReq` — the anonymous branch-sync a `PublicRepoLink` needs — is declared and `unimplemented!()` (`engine/net/src/types.rs:4523,4533`). Only `ExtObjectGet` and `WalletGetExport` are servable ext requests today.
|
||||||
|
- **Durable links are named, not built.** `PermaCap` is a TODO (`engine/repo/src/types.rs:578`), and `PermissionV0::PermaShare` — *"can create and answer to PermaCap (PermaLink)"* — already reserves the governance of creating one (`:1761`).
|
||||||
|
|
||||||
|
### Does not exist
|
||||||
|
|
||||||
|
- No function at level 2 or 3 produces or accepts a document link (all 77 `index.d.ts` exports checked; `sdk/rust/src/local_broker.rs` has only the wallet QR/textcode/rendezvous trio). `doc_fetch_repo_subscribe` (`index.d.ts:64`, `sdk/js/lib-wasm/src/lib.rs:1900`) opens documents the session **already holds** — an unknown repo errors `RepoNotFound` (`engine/verifier/src/request_processor.rs:155,163`) — it is not a stranger-open.
|
||||||
|
- No string serialization of `NgLink`, and no repo-level cap-bearing NURI that any parser accepts. Per the design principle, none of these absences says anything about the future SDK; the declared types above say a great deal.
|
||||||
|
|
||||||
|
### What the receiving side actually needs — a reference alone is NOT enough
|
||||||
|
|
||||||
|
A bare `did:ng:o:{id}`, or even `o:{id}:v:{overlay}`, gives a stranger nothing: no locator → no broker to ask; no cap → nothing decrypts, and addressing itself presupposes the cap (the outer overlay is never registered, and there is no existence probe — `docs/readcap-and-nuri-model.md` §4bis, the refuted-hypothesis block). What a stranger needs to resolve **and join** is precisely `RepoLinkV0`'s field list — id + read_cap + overlay + peers — or, public, `PublicRepoLinkV0`'s find-and-join set. The `peers`/locator half is not decoration; it is the join, and it is why the polyfill's single-broker world makes locators invisible today.
|
||||||
|
|
||||||
|
## 3. Public vs protected — the distinction is in the model
|
||||||
|
|
||||||
|
The two link structures differ on exactly one thing: **the key**. `RepoLinkV0` carries `read_cap`; `PublicRepoLinkV0` carries none — *"The latest ReadCap of the branch … will be downloaded from the outerOverlay, if the peer brokers listed below allow it"*, and *"This link is durable, because the public site are served differently by brokers"* (`engine/net/src/types.rs:5100-5104`). Public-ness is therefore a property of the **store and of how brokers serve it** (outer-overlay exposure), not a capability that circulates. This matches the official docs' store table (docs-intent, transcribed in `docs/nextgraph-current-state.md:44-58`): Public read = *"Everyone, no capability"*, *"equivalent to your website … without the need for special permissions"*; Protected = *"share … but they will need a special link and permission"*.
|
||||||
|
|
||||||
|
Two nuances against over-reading:
|
||||||
|
|
||||||
|
- It is not that no key *exists*. Today every branch — public included — has its ReadCap and its content is encrypted; the public link merely does not **carry** the key, because any holder of the link may fetch the current one from the outer overlay. The stated direction of not encrypting public-store content at all is PO-confirmed but explicitly not to be depended on by any surface (§4ter).
|
||||||
|
- The durability asymmetry follows from what the value contains: a public link *names* and stays valid across cap refresh; a protected link *carries* and goes stale when the cap is rotated (§4 below).
|
||||||
|
|
||||||
|
What follows for a public document, and is easy to get wrong: **there is nothing per-reader**. No grant, no per-reader revocation, no audience list — the only lever is coarse (unpublish/re-key the store's serving). An application UI that enumerates or revokes "the readers of my public document" is built on nothing.
|
||||||
|
|
||||||
|
### The polyfill's public emulation, read against this
|
||||||
|
|
||||||
|
Verified in this repo: publication **mints a cap** (`publishRepoLink` → `mint`, `packages/client/src/emulated-verifier/caps.ts:192-196`), the `published` set is consulted by no read path (emitter-side guard only), and the possession filter gates public documents exactly like private ones. Against the target *under the emulation's own topology* (one broker, so join-reachability is trivially satisfied): upstream, whoever can name a public document reads it; the polyfill refuses the cap-less form for everyone. That is **over-strict, not inverted** — it under-grants and never over-grants, and "circulate the link" remains the valid currency at migration. But three deltas deserve to stay visible wherever the public emulation is documented:
|
||||||
|
|
||||||
|
1. A **cap-less reference to a public document** embedded in reachable content will resolve upstream (once public serving is wired) and does not resolve here.
|
||||||
|
2. **Per-reader semantics for public documents** must not be inferred from the emulation's cap-per-holder bookkeeping — upstream has none.
|
||||||
|
3. The `:r:` segment inside a link to a *public* document is emulation detail: upstream's public link carries no key material. Harmless as long as the value is opaque to the consumer — which is the contract to enforce.
|
||||||
|
|
||||||
|
The surface consequence: the *act* — obtain a link, circulate it — is the same for both scopes upstream (`NgLinkV0` is one enum over both), so one producing function covering both is target-shaped; what differs is the semantics attached to the **value** (durability, revocability, the absence of per-reader anything), and that belongs in documentation, not in a second function.
|
||||||
|
|
||||||
|
## 4. Durability — what an application may store
|
||||||
|
|
||||||
|
- **Protected links go stale by design.** *"Those capabilities are not durable: They can be refreshed … and previously shared Caps will become obsolete/revoked"* (`engine/net/src/types.rs:5055-5058`). Rotation re-delivers the new cap **to the inboxes of those who keep access, at the rotating party's choice** (PO DIRECTION, `docs/readcap-and-nuri-model.md` §3) — never through the stored link. So a protected link in an email opened months later may no longer open; an application must treat open-from-link as fallible and fall back to a person-channel re-request. The declared future answer for stored-unopened links is `PermaCap`/PermaLink — a TODO (`engine/repo/src/types.rs:578,1761`).
|
||||||
|
- **Public links are durable** by construction (`:5103`): they carry nothing that rotation invalidates.
|
||||||
|
- **The polyfill emulates no revocation** (readcap doc §5, P3), so nothing goes stale today; consumers must not read permanence into that.
|
||||||
|
|
||||||
|
## 5. Recommendation for the polyfill
|
||||||
|
|
||||||
|
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/client/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/client/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
|
||||||
|
|
||||||
|
1. **Label it LEVEL-1 SHAPE in `docs/api-contract.md`.** What supports it: the `NgLink` family and its stated sharing flow, the `PermaShare` permission, the exercised object-URL and profile-QR precedents, and the PO doctrine that circulation is the only distribution. The model's own stated flows are unusable without *some* produce-a-link affordance, which is as much confidence as an unbuilt feature allows. What cannot be promised: the SDK's name for it, sync vs async (upstream link-building needs overlay + peers from the session, so async is plausible — same adapter-sized delta class as `subscribeDoc`'s sync unsubscribe), and whether the value is a NURI string or a structured link. Therefore: **the returned value is opaque**; a consumer that stores it, transmits it, and hands it back unmodified learns nothing to unlearn; a consumer that parses it does.
|
||||||
|
2. **Fix the comment-vs-code mismatch in `linkTo`.** The docstring claims *"A protected document's key never comes out this way — it goes through `share`"*; the code returns any held cap, with no scope check. The **code** is the model-true side: `RepoLinkV0`-with-key IS the protected-document link, and sharing it out-of-band is the documented normal case (`:5059`). Align the comment: a protected link carries the key and is legitimate to circulate — with the §4 durability caveat, not a prohibition.
|
||||||
|
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/client/src/surface/inbox.ts:410`, `packages/client/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
|
||||||
|
4. **Do not add**: link options (expiry, audience, revoke-this-link), per-reader introspection for public documents, or any API that parses or inspects a link's insides — nothing upstream supports any of them, and each teaches a lever the model does not have.
|
||||||
|
|
||||||
|
## 6. The question for the NextGraph developer
|
||||||
|
|
||||||
|
What will an application hold and pass around to share **one document** out-of-band (email, QR) — a NURI string with `r:`/`l:` segments, a serialized `NgLink`, something else — and which SDK calls will produce it and open it? And for a *public* document, is the intended value the key-less `PublicRepoLink` form, i.e. should applications treat public sharing as never involving a per-reader capability?
|
||||||
@@ -108,6 +108,12 @@ stores.
|
|||||||
|
|
||||||
## Capability sharing / NURI
|
## Capability sharing / NURI
|
||||||
|
|
||||||
|
**What a NURI transports, and the four things one may want from a link, are in
|
||||||
|
[`readcap-and-nuri-model.md`](./readcap-and-nuri-model.md) §4sexies** — including why a
|
||||||
|
key-less reference is the ordinary case rather than a degenerate one, and why nothing is
|
||||||
|
checked at access time. The analysis of what exists vs what is merely declared is in
|
||||||
|
[`document-links.md`](./document-links.md).
|
||||||
|
|
||||||
Sharing transmits a **NURI** embedding the crypto capability (read and/or write).
|
Sharing transmits a **NURI** embedding the crypto capability (read and/or write).
|
||||||
No central ACL: holding the NURI *is* the right. *"adding permissions can be done
|
No central ACL: holding the NURI *is* the right. *"adding permissions can be done
|
||||||
offline"*; *"removing permissions … requires a SyncSignature"* (synchronous).
|
offline"*; *"removing permissions … requires a SyncSignature"* (synchronous).
|
||||||
|
|||||||
@@ -269,6 +269,62 @@ What remains true, and is a separate matter — the *delivery* path is unimpleme
|
|||||||
|
|
||||||
*Consequence for this library*: **both durable registers are now emulated** (2026-07-30) — `AddRepo` as a `shim:readCap` record on a distinct subject of the store document (`storeBranch`), `AddLink` as `shim:link` on another (`userBranch`) — and the in-memory `CapRegistry` is what it always was, level 3: the cache. Caps are READ back from those records, never recomputed. What stays an invention is representing branches as RDF subjects at all: upstream both branches carry `BranchCrdt::None` and hold service commits, not triples. What is faithful is that the key sits beside the document, and that the listing (`contains`, the Main branch) is separate from the keys.
|
*Consequence for this library*: **both durable registers are now emulated** (2026-07-30) — `AddRepo` as a `shim:readCap` record on a distinct subject of the store document (`storeBranch`), `AddLink` as `shim:link` on another (`userBranch`) — and the in-memory `CapRegistry` is what it always was, level 3: the cache. Caps are READ back from those records, never recomputed. What stays an invention is representing branches as RDF subjects at all: upstream both branches carry `BranchCrdt::None` and hold service commits, not triples. What is faithful is that the key sits beside the document, and that the listing (`contains`, the Main branch) is separate from the keys.
|
||||||
|
|
||||||
|
## 4sexies. What a NURI TRANSPORTS — and the four things one may want from a link
|
||||||
|
|
||||||
|
**VERIFIED 2026-08-06** by reading `nextgraph-rs`, after a design discussion kept stalling on an assumed split between "a NURI" and "a link". There is no such split, and getting that wrong is what made this library ship a `linkTo` that was wrong three ways at once.
|
||||||
|
|
||||||
|
### A NURI and a Link are one thing in two shapes
|
||||||
|
|
||||||
|
`NuriV0` (`engine/net/src/app_protocol.rs:181-194`) carries exactly what a link carries:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
NuriV0 { identity, target, entire_store, objects, signature,
|
||||||
|
branch, overlay, access: Vec<NgAccessV0>, topic, locator: Option<Locator> }
|
||||||
|
```
|
||||||
|
|
||||||
|
`NgLinkV0 = Repo | PublicRepo | Branch | Object` (`engine/net/src/types.rs:5206-5211`) is the structured form of the same information; the NURI is its URI form. So "should we hand out a NURI or a link?" is not a question — they are the same thing, and the real question is **what one puts in the `access` and `locator` slots**.
|
||||||
|
|
||||||
|
*Caveat, and it matters for anything printed on paper:* the **type** has all the slots, the **string grammar does not expose them all**. A self-contained share URL exists for objects, commits and files (`…:v:{overlay}(:[cj]:{id}:k:{key})+:l:{locator}`); the repo case is not exercised. See [`document-links.md`](./document-links.md).
|
||||||
|
|
||||||
|
### `access` is a LIST of access forms, not a key flag
|
||||||
|
|
||||||
|
```rust
|
||||||
|
NgAccessV0 = ReadCap(ReadCap) | Token(Digest) | ExtRequest(Vec<u8>)
|
||||||
|
| Key(BlockKey) | Inbox(PrivKey) | Topic(PrivKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
(`app_protocol.rs:54-62`.) An **empty** `access` is therefore a legitimate, meaningful state: the reference NAMES its target and grants nothing. That is not a degenerate link — see below, it is the common case.
|
||||||
|
|
||||||
|
### The four things one may want, and what each requires
|
||||||
|
|
||||||
|
| What you want | What the reference must carry |
|
||||||
|
|---|---|
|
||||||
|
| **Name** a document — cite it, reference it from another document, let someone ask you for it | `target`; `access` EMPTY |
|
||||||
|
| **Let someone read** a non-public document | `target` + `ReadCap`. Irreversible: whoever receives the message reads, and nothing checks anything later |
|
||||||
|
| **Let someone find** a public document | `target` + `overlay`/`locator`; `access` EMPTY — the broker serves it, no key travels |
|
||||||
|
| **Let someone JOIN from elsewhere** | `locator`. Without it a stranger with no broker in common opens nothing, key or no key |
|
||||||
|
|
||||||
|
### Three confusions worth naming, because each one was made here
|
||||||
|
|
||||||
|
**"No key" does not mean "public".** A key-less reference to a PROTECTED document withholds access deliberately — the recipient can name it, not read it. A `PublicRepoLinkV0` (`types.rs:5105-5127`) carries no `read_cap` for an unrelated reason: the content is served publicly, so there is nothing to hand over. Same shape, opposite situations.
|
||||||
|
|
||||||
|
**Naming is not reading, and it is the DEFAULT act.** This is what `test/cross-user-access.test.ts` proves: Alice publishes a public document that REFERENCES her protected one; Bob follows the public link, finds the reference, and can name the protected document while reading nothing of it. Publication is not recursive. Withholding the key is the ordinary, reversible gesture; including it is the exceptional, irreversible one.
|
||||||
|
|
||||||
|
**Nothing is checked at access time.** Reading IS possession — the engine verifies a permission on WRITE only (`verify_permission` → `PermissionDenied` in `Commit::verify`), never on read. So a reference cannot be "neutral about rights, resolved later": what you put in it IS the grant. There is no revoking a link you have circulated, only rotating the key (and `RepoLinkV0`'s own comment says shared caps go stale on refresh — `PermaShare` / PermaCap, `engine/repo/src/types.rs:1761`, is the reserved answer to that, unimplemented).
|
||||||
|
|
||||||
|
### Publishing here means distributing a key; upstream it does not
|
||||||
|
|
||||||
|
The sharpest divergence this discussion surfaced, and it is not in `linkTo` — it is one level down.
|
||||||
|
|
||||||
|
This library mints a cap on publication (`emulated-verifier/caps.ts` `publishRepoLink`) and its possession filter gates public documents like any other. So *publish* means, here, **distribute a key**. Upstream, a public document is readable because its STORE is public and brokers serve it accordingly (`expose_outer`); the link carries no key at all, and any holder fetches the current one from the outer overlay.
|
||||||
|
|
||||||
|
The emulation is therefore **over-strict, not inverted** — it under-grants. A cap-less reference to a public document resolves upstream and does not here. That matters in two ways: "circulate the link" stays the right gesture at migration (so the consumer act survives), but a consumer must not conclude that publishing is an act of key distribution, nor expect anything per-reader on a public document — upstream there is no grant, no revoke, no audience to build a UI on.
|
||||||
|
|
||||||
|
### Consequence for this library
|
||||||
|
|
||||||
|
`linkTo` (added 2026-08-05) fails all three: it always embeds the key — including where none is needed — never carries a locator, and offers no way to withhold. It must be rebuilt from the table above rather than patched. The recipient verb is missing entirely, which is why Bob's side of the scenario cannot be tested by the multi-actor rule.
|
||||||
|
|
||||||
|
|
||||||
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
|
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
|
||||||
|
|
||||||
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
|
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
|
||||||
|
|||||||
Reference in New Issue
Block a user