docs: état courant NextGraph enrichi + modèle cible aligné + retrait du lot PW
MODÈLE CIBLE (readcap-and-nuri-model) — trois ajouts, deux corrections :
- Store public : lisible par l'URL, et NON récursif — un contenu public peut
référencer du contenu privé sans y donner accès. C'est la non-récursivité qui
porte la valeur (objet public pointant vers de l'identité privée).
- Le trousseau : la branche de store, où chaque création commite AddRepo{read_cap}
— avec l'avertissement explicite que ce n'est PAS le mécanisme de partage.
Confondre l'index privé et le geste de partage mène à « on partage le store »,
ce qui livrerait tout son contenu présent et futur.
- Rotation de clé : re-livraison par inbox, traitée automatiquement à la
connexion. Écrit comme DIRECTION, en signalant que le commentaire amont dont ça
partait décrit l'état courant.
- Levée de la confusion did/NURI en tête de la section grammaire : `did🆖` est
un préfixe de schéma présent partout, pas un marqueur de « sans cap ». C'est un
seul objet, avec ou sans la clé dedans.
- Livraison de cap par inbox signalée comme MANQUE (forme bonne, chemin absent).
ÉTAT COURANT (nextgraph-current-state) — 218 lignes ajoutées, structure intacte :
livraison de cap par inbox non implémentée ; vérification de signature d'auteur
jamais appelée au runtime (members map vide, //TODO) ; aucune sonde d'existence
au niveau SDK ; expose_outer codé en dur à false, absent du SDK ; protocole Ext
sans aucun contrôle. Plus trois constats d'exploitation : heal cold-start,
fork de compte sur provision concurrente, et l'abort du flush outbox sur
TopicNotFound. La mort du socket est seulement référencée (déjà couverte).
CORRECTION D'UN FAIT QUE J'AVAIS ÉNONCÉ FAUX : le digest d'auteur n'est PAS clé
sous le secret de lecture — il est clé par l'overlay outer, public. C'est le
CONTENU du commit qui est chiffré. La conclusion « vérifier suppose de pouvoir
lire » tient, le mécanisme diffère.
Lot PW (WriteCap = membership) RETIRÉ de la liste des phases : il restait planifié
alors que le brief déclare plus haut qu'il n'y a pas de membership. Il était en
outre justifié par un besoin de dédup par signature que le consommateur n'a pas —
sa dédup s'appuie sur l'overlay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -364,11 +364,17 @@ rend le découpage P1a/P1b honnête plutôt que cosmétique.
|
||||
le vrai contenu de P2 est **durabilité + cap-less + re-partage par le détenteur**,
|
||||
pas « inverser l'ACL » — sans crypto, inverser ne produit aucun delta observable.*
|
||||
- **P3** — révocation par re-key (invalidation + re-livraison, non-rétroactive).
|
||||
- **PW** — **WriteCap = membership** : primitif distinct (add/remove member,
|
||||
members map lisible par un membre, **vérification d'auteur de commit** → digest
|
||||
par-store), et découpage en stores **exprimable** par le consommateur.
|
||||
*Indépendant de P1–P3 ; **bloquant pour la dédup Festipod**, donc à ordonnancer
|
||||
tôt si c'est ce besoin-là qui presse.*
|
||||
- ~~**PW** — WriteCap = membership~~ **RETIRÉ (2026-07-27)**. Ce lot reposait sur
|
||||
une notion d'appartenance qui **n'existe pas** dans le modèle (tout est clés et
|
||||
URLs) ; voir la section barrée plus haut. Il était en outre justifié par un
|
||||
besoin de **dédup par vérification de signature** que le consommateur n'a
|
||||
finalement pas : sa dédup s'appuie sur l'overlay, store-scopé.
|
||||
*Pour mémoire, deux faits vérifiés au passage, à ne pas re-découvrir* : la
|
||||
vérification de signature d'auteur **n'est pas appelée au runtime** ; et le
|
||||
digest d'auteur n'est **pas** clé sous le secret de lecture — il est clé par
|
||||
l'overlay **outer**, public *(c'est le **contenu** du commit qui est chiffré,
|
||||
d'où le fait que vérifier suppose quand même de pouvoir lire)*. Détail dans
|
||||
`nextgraph-current-state.md`.
|
||||
- **P4** — adapter l'API consommateur + `migration-guide.md`. *La revue adverse
|
||||
requalifie ce lot : ce n'est pas un swap d'API mais une **re-architecture
|
||||
consommateur** (le grant se déplace vers l'acceptation de connexion et devient
|
||||
|
||||
@@ -137,6 +137,32 @@ A related exposed primitive: `social_query_start` (a federated query via inbox u
|
||||
`degree` hops) exists but is limited to contacts — it does not cover an anonymous
|
||||
notification to a non-connected host.
|
||||
|
||||
### Delivering a ReadCap through the inbox — the field exists, the path does NOT — VERIFIED
|
||||
|
||||
The `ContactDetails` inbox message carries `read_cap: Option<ReadCap>`, commented
|
||||
*"optional readcap on the profile, if user wants to share the content of profile"*
|
||||
(`engine/net/src/types.rs`). Nothing behind that field is implemented:
|
||||
|
||||
- **Building it panics.** `InboxPost::new_contact_details(…, with_readcap: bool, …)`
|
||||
(`engine/net/src/types.rs`) fills `read_cap` with `unimplemented!()` when
|
||||
`with_readcap` is true, and `None` otherwise. Asking for a cap in the message is a
|
||||
panic, not a feature.
|
||||
- **Nobody asks for one.** Its ONLY caller is the `QrCodeProfileImport` path in
|
||||
`engine/verifier/src/request_processor.rs`
|
||||
(`post_to_inbox(InboxPost::new_contact_details(…))`), which passes `with_readcap =
|
||||
false`. No message ever carries a cap.
|
||||
- **The receiver discards it.** The `InboxMsgContent::ContactDetails(details)` arm of
|
||||
`engine/verifier/src/inbox_processor.rs` reads `details.profile`, `details.name` and
|
||||
`details.email` to build a `social:contact` document — it **never reads
|
||||
`details.read_cap`**. Even a hand-crafted message carrying a cap would be dropped.
|
||||
|
||||
**Consequence for this lib:** there is no native channel to HAND a key to somebody. The
|
||||
inbox transports an identity/profile pointer, not a read capability. Combined with
|
||||
§ *The inbox is not usable from the JS SDK* (no `InboxPost` arm in the request processor
|
||||
at all), cap delivery must be emulated end to end: the polyfill's emulated inbox and its
|
||||
`CapRegistry` are not a shortcut around an existing mechanism, they stand in for a
|
||||
mechanism that does not exist.
|
||||
|
||||
## The query capability — ONE local store, named graphs, union queries
|
||||
|
||||
The single fact that makes read-time *listing* possible on the shared wallet, and
|
||||
@@ -471,6 +497,117 @@ redirect afterwards. This lib's identity store sidesteps all of it — the ident
|
||||
id is set at wallet-import time and relayed to the lib, without a separate login;
|
||||
see the identity store in [`simulation.md`](./simulation.md).
|
||||
|
||||
## Authorship, existence, outer overlay, `Ext` (section added 2026-07-27)
|
||||
|
||||
Four capability facts about the current core, verified in `nextgraph-rs`. They bear on
|
||||
what can be BUILT on top (can we deliver a key? can we tell whether a document exists?
|
||||
can we attribute a write?) — they are not a security assessment. Each carries its
|
||||
epistemic status; do not upgrade an INFERRED item without new evidence.
|
||||
|
||||
### Author-signature verification is never called at runtime — VERIFIED
|
||||
|
||||
`Commit::verify` (`engine/repo/src/commit.rs`) chains `verify_sig` → `verify_perm` →
|
||||
`verify_full_object_refs_of_branch_at_commit`. Its only callers in the whole tree are
|
||||
inside `#[cfg(test)] mod test` blocks (`engine/repo/src/commit.rs`,
|
||||
`engine/repo/src/branch.rs`); `verify_sig` and `verify_perm` have no other caller. The
|
||||
verifier's commit path calls a **different** `verify`:
|
||||
`CommitBodyV0::<Body>::verify(commit, self, branch_id, repo_id, store)` in
|
||||
`engine/verifier/src/verifier.rs` — the `CommitVerifier` trait, which APPLIES a body
|
||||
(mutating verifier state); it is not a signature check.
|
||||
|
||||
Even if it were called it could not succeed. `verify_sig` resolves the author through
|
||||
`Repo::member_pubkey` → `Repo.members`, and every `Repo` the verifier builds at runtime
|
||||
sets `members: HashMap::new()` — `engine/verifier/src/user_storage/repo.rs` (with a
|
||||
literal `//TODO: members`) and `engine/verifier/src/commits/mod.rs`. Only
|
||||
`Repo::new_with_member` ever populates a member, and it is called only from tests. An
|
||||
empty table makes `member_pubkey` return `NotFound` →
|
||||
`CommitVerifyError::PermissionDenied`.
|
||||
|
||||
Reading authorship at all presupposes the read cap (VERIFIED): the author field is not a
|
||||
UserId but `CommitContent::author_digest(user, overlay)`, a BLAKE3 keyed hash, and the
|
||||
commit content sits in blocks ChaCha20-encrypted under `Object::convergence_key(store)`
|
||||
(`engine/repo/src/object.rs`), whose key material is the store id **plus the
|
||||
store-overlay-branch ReadCapSecret**. No read cap → the author field is not even
|
||||
visible. *Nuance, VERIFIED:* the digest's own hashing key derives from
|
||||
`overlay_id_for_read_purpose`, which for Public/Protected/Private/Group stores is
|
||||
`OverlayId::outer(store_id)` — public. What is secret is the commit content, not the
|
||||
hash key.
|
||||
|
||||
**Consequence for this lib:** "who wrote this triple" is unanswerable today — neither
|
||||
cryptographically (nothing verifies) nor by identity (the digest is opaque without a
|
||||
member table). Any authorship or provenance the polyfill needs must be carried in the
|
||||
DATA it writes and re-read from there; an "authored by X" claim in the emulation has no
|
||||
core check behind it.
|
||||
|
||||
### No existence probe at SDK level — addressing presupposes the cap — VERIFIED
|
||||
|
||||
`AppRequestCommandV0` (`engine/net/src/app_protocol.rs`) contains no existence command:
|
||||
`Fetch`, `Pin`, `UnPin`, `Delete`, `Create`, `FileGet`, `FilePut`, `Header`, `InboxPost`,
|
||||
`SocialQueryStart`, `SocialQueryCancel`, `QrCodeProfile`, `QrCodeProfileImport`,
|
||||
`OrmStartGraph`, `OrmStartDiscrete`, `OrmGraphUpdate`, `OrmDiscreteUpdate`, `OrmStop`.
|
||||
Nothing answers *"does document D exist?"*.
|
||||
|
||||
The single probe in the tree is internal and cannot answer it either:
|
||||
`Verifier::has_blocks` (`engine/verifier/src/verifier.rs`) sends
|
||||
`BlocksExist { blocks, overlay }`. It is `pub(crate)` (never reaches JS); it takes
|
||||
**`BlockId`s** — content addresses you only hold if you already read the object; it takes
|
||||
a **`&Repo` already loaded**; and it targets
|
||||
`repo.store.overlay_for_read_on_client_protocol()` = the **inner** overlay
|
||||
(`Store::inner_overlay` → `overlay_id_for_write_purpose(store_overlay_branch_readcap.key)`,
|
||||
`engine/repo/src/store.rs`), derived from the read-cap secret.
|
||||
|
||||
**Consequence for this lib:** you cannot prove — nor disprove — the existence of a
|
||||
document whose key you do not hold. **Addressing presupposes the cap.** Every "is it
|
||||
there?" question therefore collapses into "can I read it?", which is why absence is only
|
||||
ever established behind a sync barrier (see § *Findable-without-lookup vs subscribable*)
|
||||
and never by probing.
|
||||
|
||||
### `expose_outer` is hard-coded to `false` — VERIFIED
|
||||
|
||||
Both constructors of `PinRepo` — `PinRepo::for_branch` and `PinRepo::from_repo`
|
||||
(`engine/net/src/actors/client/pin_repo.rs`) — set `expose_outer: false`, and they are
|
||||
the only two `PinRepoV0` constructions in the tree. No parameter carries the flag up:
|
||||
`expose_outer` appears nowhere under `sdk/`. The broker side is fully wired
|
||||
(`RepoInfo.expose_outer: HashSet<UserId>` in `engine/broker/src/server_broker.rs`, the
|
||||
`if expose_outer` branch in `rocksdb_server_storage.rs`, the outer-overlay registration
|
||||
in `server_storage/core/overlay.rs`), and the `PinRepo` responder even validates the flag
|
||||
(refusing `expose_outer` from a peer that publishes no topic) — but no client ever sets
|
||||
it.
|
||||
|
||||
**Consequence for this lib:** a store's **outer** overlay is never registered broker-side,
|
||||
so there is no anonymous / capability-free read surface to build on. Everything is reached
|
||||
through the inner overlay, i.e. through a read cap — the same cap-first addressing as
|
||||
above. The "public store readable by everyone without permission" promise in the official
|
||||
docs has no client-side switch today.
|
||||
|
||||
### The `Ext` protocol serves blocks with no control — VERIFIED
|
||||
|
||||
The `ExtObjectGetV0` responder (`engine/net/src/actors/ext/get.rs`) builds
|
||||
`Store::new_from_overlay_id(&req.overlay, …)` from the OverlayId the **requester
|
||||
declares**, then returns `Object::load_without_header(obj_id, None, &store)` blocks for
|
||||
each requested id. No authentication, no verification that the requester belongs to that
|
||||
overlay. The guards that were planned exist but are dead:
|
||||
|
||||
- `Authorization::ExtMessage` is matched in `Broker::authorize`
|
||||
(`engine/net/src/broker.rs`) and returns `AccessDenied` — but **no caller ever passes
|
||||
it**; the only `authorize` call sites pass `Discover`, `Admin` or `Client`. The
|
||||
server-side `StartProtocol::Ext` arm in `engine/net/src/connection.rs` goes straight to
|
||||
`StepReply::Responder`, never through `authorize`.
|
||||
- the config flag whose comment reads *"are ExtRequest allowed on the server? this
|
||||
requires the core to be ON."* — `allow_read` in `engine/net/src/types.rs` — is declared
|
||||
and defaulted to `false`, and **read nowhere**.
|
||||
- `ExtRequestContentV0::get_actor` handles `WalletGetExport` and `ExtObjectGet` and falls
|
||||
through to `_ => unimplemented!()` for `ExtTopicSyncReq` — a **panic reachable from an
|
||||
anonymous peer**. (The commented-out `// Self::ExtTopicSyncReq(a) => a.get_actor(),` on
|
||||
that arm and the `// TODO inbox requests` in the enum are *direction hints*, labelled as
|
||||
such — not current behaviour.)
|
||||
|
||||
**Consequence for this lib:** `Ext` is not a usable read path in either direction. Blocks
|
||||
come back **encrypted**, and naming them requires ObjectIds you only have once you can
|
||||
already read — so it grants no capability we could build on, and confirms the shape of
|
||||
everything above: confidentiality lives entirely in the keys, and holding no key means
|
||||
holding no partial access, just none.
|
||||
|
||||
## Known open issues (section added 2026-07-18)
|
||||
|
||||
Live limitations observed against the current core/SDK, each with its epistemic
|
||||
@@ -517,3 +654,84 @@ cross-browser reactive update works). Verdict pending a live instrumented run.
|
||||
Full write-up (suspect link, instrumentation, planned polyfill-side fix):
|
||||
[`../packages/client/docs/sdk-reference.md`](../packages/client/docs/sdk-reference.md)
|
||||
§ *Current emulation status*.
|
||||
|
||||
### Cold-start anchored read returns 0 rows instead of an error — symptom VERIFIED, mechanism INFERRED, healed polyfill-side
|
||||
|
||||
On a FRESH session over the SAME persistent wallet (reconnect, new page, re-login), an
|
||||
anchored `sparql_query` against a document written in an earlier session comes back with
|
||||
**0 rows and no error** — persisted documents read as empty. Observed on every anchored
|
||||
reader of the polyfill and healed identically in each (`ensureRepoOpen` before the read,
|
||||
`packages/client/src/open-repo.ts`): the discovery index (`discovery.ts` `readIndex`),
|
||||
the per-scope index (`store-registry.ts` `readScopeIndex`), the by-need doc batch
|
||||
(`read-model.ts` `readUnion`), and the store-root pointer read (`store-registry.ts`
|
||||
`resolvePointer`). The heal is `doc_subscribe(nuri)` → await the first `State` (the sync
|
||||
barrier) → THEN the anchored read, and it is verified to return the data.
|
||||
|
||||
The circularity that made it self-inflicted (VERIFIED by the fix working): `doc_subscribe`
|
||||
WOULD open the repo, but the reactive layer only subscribes AFTER a listing produced
|
||||
NURIs, and the listing is itself an anchored read of a not-yet-open index repo → 0 rows →
|
||||
nothing to subscribe → nothing ever opens.
|
||||
|
||||
**Mechanism INFERRED, not established.** `resolve_target_for_sparql(Repo(id))`
|
||||
(`engine/verifier/src/request_processor.rs`) does
|
||||
`self.repos.get(repo_id).ok_or(RepoNotFound)`, so a repo genuinely absent from
|
||||
`self.repos` should ERROR, not return 0 rows. The most plausible reading of the silent 0
|
||||
is that the repo IS in `self.repos` (loaded from local user storage at bootstrap) while
|
||||
its named graph in `graph_dataset` is not yet populated — commits not applied/synced yet
|
||||
— so the query legitimately matches nothing. Not traced end to end; the tension with the
|
||||
`RepoNotFound` path described in § *A repo is only queryable once OPENED/synced into the
|
||||
store* is unresolved.
|
||||
|
||||
**Consequence for this lib:** a cold anchored read is NOT authoritative on its own — 0
|
||||
rows does not mean absent. This is what imposes the open-then-read discipline on every
|
||||
cold reader, and it is why the account trust root had to move behind a first-`State`
|
||||
barrier (see § *The pointer → doc-shim indirection*).
|
||||
|
||||
### Account fork on concurrent provision — symptom VERIFIED, guarded polyfill-side, residue persists in wallets
|
||||
|
||||
On a fresh page, several independent callers hit `ensureAccount(A)` near-simultaneously
|
||||
(the public and protected `watchShape`, container subscriptions, the app's owned-events
|
||||
effect). When the account is genuinely new, each caller sees 0 and each provisions its
|
||||
own set of three scope documents — an **in-session account fork**. The persisted residue
|
||||
is a single account subject carrying MULTIPLE values for one scope predicate (observed:
|
||||
five `shim:docPublic`), after which a writer and a later reader can resolve DIFFERENT
|
||||
scope docs and the reader's anchored read returns 0.
|
||||
|
||||
Two polyfill-side guards, both in `packages/client/src/store-registry.ts`: `ensureInFlight`
|
||||
(a bounded promise map keyed by account, so concurrent `ensureAccount` calls share ONE
|
||||
resolve-or-provision) prevents new forks; `canonicalDoc` (pick the lexicographically
|
||||
smallest NURI among all distinct values for a scope predicate — NURIs are
|
||||
content-addressed, so the order is total and session-independent) makes resolution
|
||||
deterministic on wallets that already carry fork residue. The earlier account-level
|
||||
`provisionRetry` / `resolveAccountReliably` loop is gone, replaced by the doc-shim
|
||||
barrier.
|
||||
|
||||
**Consequence for this lib:** the underlying enabler is core-side — there is no atomic
|
||||
create-if-absent, and no existence probe to settle "does this account already exist?"
|
||||
(see § *No existence probe at SDK level*), so provisioning is a read-then-create race the
|
||||
polyfill has to serialize itself. The guards are mitigation, not a fix: a wallet already
|
||||
corrupted stays corrupted, and only `canonicalDoc` keeps it readable.
|
||||
|
||||
### Outbox replay aborts on an unknown topic (`REPLAY TOPIC NOT FOUND`) — VERIFIED in core, already documented as an incident
|
||||
|
||||
`Verifier::send_outbox` (`engine/verifier/src/verifier.rs`) walks the queued events and,
|
||||
for each, looks up `self.topics.get(&(overlay, topic_id))`. On a miss it logs
|
||||
`REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>` and sets `need_replay`, calls
|
||||
`load_from_credentials_and_outbox(&events_to_replay)`, then in the send loop does
|
||||
`self.topics.get(…).ok_or(NgError::TopicNotFound)?` — the `?` **aborts the whole outbox
|
||||
flush**, so the remaining queued events are not sent. There is no per-event isolation and
|
||||
no signal to the caller.
|
||||
|
||||
Already covered — **not duplicated here**: this is the core-side mechanism behind the
|
||||
symptom described in § *Write loss on socket death (`SerializationError`)* above, whose
|
||||
full post-mortem (logs, causal chain, the unarbitrated (i)/(ii) reserve) is
|
||||
[`incidents/2026-07-14-write-loss-on-disconnect.md`](./incidents/2026-07-14-write-loss-on-disconnect.md).
|
||||
The spontaneous socket death (`SOCKET IS CLOSED Some(Left(SerializationError))`) is
|
||||
likewise covered there and in that section — the only fact added here is the abort
|
||||
semantics of the replay path itself (VERIFIED by reading `send_outbox`).
|
||||
|
||||
**Consequence for this lib:** a queued write can be dropped without any observable error,
|
||||
and one unknown topic can take the rest of the queue with it. The polyfill's own
|
||||
`outbox-log.ts` records write intents but cannot replay them into the core, and no
|
||||
write-durability confirmation exists to await — so "the write returned" is not "the write
|
||||
is durable".
|
||||
|
||||
@@ -45,6 +45,12 @@ sa clé privée.
|
||||
Le « ciblage wallet » vit donc dans **l'enveloppe de scellage**, pas dans le cap :
|
||||
le cap reste `{id, clé}`, possession-based.
|
||||
|
||||
> **État courant (2026-07-27) — le chemin est un MANQUE, pas un désaccord.** Le
|
||||
> champ `ContactDetails.read_cap` existe, mais la construction du message est
|
||||
> `unimplemented!()` (son unique appelant passe « sans read_cap ») et le récepteur
|
||||
> **jette** le cap qu'il recevrait. La *forme* est donc la bonne ; l'implémentation
|
||||
> n'est pas là. Le polyfill l'émule en attendant — fiche dans le bug-inbox.
|
||||
|
||||
## 3. Révocation = re-key (grossier, non-rétroactif)
|
||||
|
||||
On ne « reprend » pas une clé livrée. Révoquer = **re-chiffrer** avec une nouvelle
|
||||
@@ -60,8 +66,32 @@ clé et ne la re-sceller qu'aux autorisés restants.
|
||||
**antérieures** au refresh).
|
||||
- Livraison **durable** d'un cap = `PermaCap` — encore **TODO** (`repo/types.rs:578`).
|
||||
|
||||
### DIRECTION — la rotation ne fait PAS perdre l'accès (confirmé PO, 2026-07-27)
|
||||
|
||||
**Ne pas lire le commentaire ci-dessus comme l'intention.** « *if they don't
|
||||
subscribe, they lose access after the refresh* » décrit **l'état courant**, pas la
|
||||
cible. Ce que NextGraph vise :
|
||||
|
||||
> Quand une clé tourne, la nouvelle est **envoyée dans l'inbox** des utilisateurs
|
||||
> qui conservent le droit d'accès. Cette inbox est **traitée automatiquement** dès
|
||||
> qu'un client de l'utilisateur se connecte.
|
||||
|
||||
Donc l'accès n'est **pas perdu**, il est **différé** jusqu'à la prochaine connexion
|
||||
— cohérent avec le local-first. Conséquences de forme : **aucune obligation
|
||||
d'abonnement** à exposer au consommateur ; une re-livraison emprunte **le même
|
||||
canal** que la livraison initiale, donc le mécanisme de partage couvre les deux
|
||||
sans cas particulier. La **révocation** reste « cesser de re-livrer », non
|
||||
rétroactive.
|
||||
|
||||
## 4. Grammaire NURI : cap-less vs cap-porteur (le segment `:k:`)
|
||||
|
||||
**Lever la confusion d'abord** : `did:ng:` n'est **pas** un marqueur de « sans
|
||||
cap », c'est le **préfixe de schéma d'URI** — présent partout (inbox `did:ng:d:…`,
|
||||
branche `did:ng:b:…`, overlay `did:ng:v:…`, document `did:ng:o:…`). Un NURI **est**
|
||||
un `did:ng:…`. Il n'y a donc pas « le did » d'un côté et « le NURI » de l'autre :
|
||||
c'est **un seul objet**, avec ou sans la clé dedans — un seul type en amont,
|
||||
`NuriV0 { target, access }`, où un NURI cap-less a simplement `access` vide.
|
||||
|
||||
Le discriminant est le segment **`:k:{clé}`** : présent = cap-porteur ; **absent =
|
||||
cap-less** (nomme/localise **sans** donner le droit de lire). C'est de **première
|
||||
classe** dans le type : `NuriV0.target` (des ids) et `access`/`objects` (le cap)
|
||||
@@ -156,6 +186,45 @@ Leçon transposable : vérifier qu'une garde d'accès **laisse passer** ne prouv
|
||||
qu'une opération est atteignable — encore faut-il pouvoir **nommer** ce qu'on
|
||||
demande.
|
||||
|
||||
## 4ter. Le store public : lisible par l'URL, et NON récursif
|
||||
|
||||
Principe cible (confirmé PO, 2026-07-27) :
|
||||
|
||||
> **Un élément du store public est public : qui a l'URL lit le contenu.**
|
||||
> Mais **pas récursivement** — un contenu public peut *référencer* du contenu
|
||||
> privé, et la référence ne donne **pas** accès au référencé.
|
||||
|
||||
C'est un **second mécanisme**, à côté de la possession de clé (§1) — pas une
|
||||
entorse. Et c'est la **non-récursivité** qui porte la valeur : elle autorise un
|
||||
objet public qui **pointe** vers de l'identité privée, sans la divulguer. C'est
|
||||
exactement le motif dont un modèle de présence anonyme a besoin.
|
||||
|
||||
*Détail d'implémentation, à ne PAS faire porter par la forme* : NextGraph s'oriente
|
||||
vers un **non-chiffrement** du contenu du store public (les données restant
|
||||
**signées**). Une surface ne doit pas en dépendre. Et si le store public ne se
|
||||
comporte pas comme ce principe le décrit, c'est **le polyfill** qui s'adapte, pas
|
||||
le consommateur.
|
||||
|
||||
## 4quater. Le trousseau : d'où le propriétaire tire les caps de SES documents
|
||||
|
||||
À chaque création de document, un `AddRepo { read_cap }` est commité sur une
|
||||
**branche du store** — le store étant lui-même un repo, doté de branches **typées**
|
||||
(le mot « branche » n'a rien de git : c'est un compartiment à rôle défini). Cette
|
||||
branche liste **les documents du store, chacun avec sa clé de lecture**.
|
||||
|
||||
Elle **est** donc le **trousseau du propriétaire** : le mécanisme par lequel il
|
||||
retrouve les caps de ses propres documents. En amont, le trousseau, c'est le
|
||||
**wallet**.
|
||||
|
||||
**Ce n'est PAS le mécanisme de partage.** Confusion facile et coûteuse : en déduire
|
||||
« on partage au niveau du store » est faux — livrer un cap de store donnerait accès
|
||||
à **tout** son contenu, présent et futur. **L'unité de partage est le document**
|
||||
(§2). Le trousseau est un index privé, pas un geste de partage.
|
||||
|
||||
*(VÉRIFIÉ pour le mécanisme `AddRepo { read_cap }` ; le **nom exact** des branches
|
||||
et l'énumération de leurs types n'ont pas été re-tracés — à confirmer si ce point
|
||||
devient porteur.)*
|
||||
|
||||
## 5. Ce que le polyfill émule (caps.ts) — et où ça diverge
|
||||
|
||||
`packages/client/src/caps.ts` modélise `readers: Map<Nuri, Set<PrincipalId>>` +
|
||||
|
||||
Reference in New Issue
Block a user