docs: nommer par la fonction, et n'annoncer qu'un point d'entrée
Deux corrections indépendantes dans la doc vivante, les briefs et décisions
datés restant tels qu'écrits.
P1a et P1b ne disaient rien à personne. Six mois plus tard il aurait fallu lire
le code pour savoir de quoi on parle, et le coût de la recherche se repaie à
chaque lecture. Ils deviennent cap-surface — la forme des capacités, livrée le
2026-07-28 — et cap-enforcement — ce qui reste : le chiffrement par document et
les gardes d'écriture aujourd'hui décoratives. 28 occurrences.
Et api-contract.md se contredisait à quatre lignes d'intervalle : il annonçait
deux points d'entrée en tête, et en bas qu'il n'y en a qu'un depuis la fusion du
2026-08-07. Vérifié dans package.json avant d'écrire — exports mappe exactement
{".": "./src/index.ts"} et src/polyfill.ts n'existe pas.
Ce qui identifie un symbole polyfill-era ne change pas : le bloc marqué dans
src/index.ts et le test de vocabulaire, plus aucun chemin d'import.
This commit is contained in:
@@ -15,7 +15,7 @@ Every published symbol has exactly one of two fates, and knowing which is the wh
|
||||
|
||||
## Why the deletion list is not an import path any more
|
||||
|
||||
There were two entry points until 2026-08-07, `.` and `./polyfill`, and the second one carried a signal worth naming: *what you import from that path is exactly what you will delete*. That was a mechanical guarantee — the compiler produced the list.
|
||||
There were two entries until 2026-08-07, `.` and `./polyfill`, and the second one carried a signal worth naming: *what you import from that path is exactly what you will delete*. That was a mechanical guarantee — the compiler produced the list.
|
||||
|
||||
Merging them lost it. Nothing at an import line now distinguishes `configure`, which goes away, from `docs`, which is replaced. Three things carry it instead, and it is worth knowing that only the last two are enforced:
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
|
||||
|
||||
**Scope: the APP-FACING contract only.** Everything reachable from the two published entry points, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15.
|
||||
**Scope: the APP-FACING contract only.** Everything reachable from the published entry point, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15.
|
||||
|
||||
**Scope.** The real exported surface of `@ng-eventually/polyfill` (verified against the `export` statements in `packages/polyfill/src/index.ts` and `packages/polyfill/src/polyfill.ts` — `package.json` maps exactly two entry points, `.` and `./polyfill`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
**Scope.** The real exported surface of `@ng-eventually/polyfill` (verified against the `export` statements in `packages/polyfill/src/index.ts` — `package.json` maps exactly one entry point, `.`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
|
||||
**How to read the epistemic labels.** Every target-side claim carries one of:
|
||||
|
||||
@@ -141,7 +141,7 @@ export type { NG } from "@ng-org/web";
|
||||
The two overrides:
|
||||
|
||||
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
|
||||
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until P1b.
|
||||
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until cap-enforcement.
|
||||
|
||||
---
|
||||
|
||||
@@ -453,10 +453,10 @@ isInPublicStore(nuri: Nuri): boolean;
|
||||
open(nuri: Nuri, scope: Scope): ReadCap;
|
||||
isEnforcing(): boolean;
|
||||
onChange(listener: () => void): () => void;
|
||||
grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until P1b
|
||||
governsWrite(doc: Nuri): boolean; // decorative until P1b
|
||||
canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until P1b
|
||||
hasWritePolicy(): boolean; // decorative until P1b
|
||||
grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until cap-enforcement
|
||||
governsWrite(doc: Nuri): boolean; // decorative until cap-enforcement
|
||||
canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until cap-enforcement
|
||||
hasWritePolicy(): boolean; // decorative until cap-enforcement
|
||||
clear(): void;
|
||||
```
|
||||
|
||||
@@ -465,7 +465,7 @@ clear(): void;
|
||||
**LEVEL-1 SHAPE.** There is no capability API at level 2 or 3 (no cap method in `index.d.ts`, none in the ORM), and there is **nothing to introspect upstream**: reading is key possession. The model, VERIFIED:
|
||||
|
||||
- A ReadCap is the serialized `ObjectRef` — `format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). **The `r:` segment and its encoding are upstream's**, reported by NextGraph's developer and verified in that function: id and key are serialized together into ONE opaque segment, unlike the `:k:` object/file/commit forms where they are two. The lib's `ReadCap` template-literal type uses that segment, with the stand-in constant `OK` in place of the key material.
|
||||
- **"P1b swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`** — `NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field** — `NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, P1b moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11).
|
||||
- **"cap-enforcement swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`** — `NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field** — `NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, cap-enforcement moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11).
|
||||
- Caps live in two durable registers by origin: created documents → `AddRepo { read_cap }` on the store's Store branch (`engine/repo/src/types.rs:1890-1899`, committed by `doc_create` via `send_add_repo_to_store`, `engine/verifier/src/request_processor.rs:698`); received caps → `AddLink { read_cap }` on the private store's User branch (`types.rs:1939-1948`).
|
||||
- The one path that loads a repo from a cap is `pub(crate)` — `Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`) — unexposed to JS.
|
||||
|
||||
@@ -637,7 +637,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
- ~~**`docs.depositInto`**~~ **FIXED 2026-08-07.** It was published "only because `inbox.ts` lives in another module", with the note that a consumer must always go through `inbox.post`. That note is not a mechanism: an adversarial review drove through it — bare reference to a public document, rewrite its posted inbox address, divert its owner's deposits. It now lives in `emulated-verifier/register-write.ts`, which nothing exports.
|
||||
- **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers.
|
||||
- **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do.
|
||||
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer.
|
||||
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until cap-enforcement — the guard they feed is bypassed by every internal writer.
|
||||
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers.
|
||||
- ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream.
|
||||
- ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
|
||||
|
||||
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/polyfill/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.` → `src/index.ts` and `./polyfill` → `src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/polyfill/src/` that is NOT reachable from the published entry point (`package.json` maps exactly `.` → `src/index.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
|
||||
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07, and the polyfill-era block was then cut to ONE published call, `configure`. `configureStoreRegistry`, `setCurrentUser` and `connectedUser` became internal the same day — folded, replaced by the gate, and awaited inside it respectively; the machinery accessors and test resets are internal too. All four are inventoried below.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
|
||||
|
||||
@@ -38,7 +38,7 @@ export function mintCap(nuri: Nuri): ReadCap;
|
||||
`targetOf` strips a `:r:` cap segment to the naming form; `parseNuri` is the parsed pair; `mintCap` builds the cap-bearing form with the stand-in value `OK` (`STAND_IN_CAP`). Kept off the surface deliberately: nothing published turns a bare reference into a cap.
|
||||
|
||||
- `targetOf` / `parseNuri` — **LEVEL-1 SHAPE, model VERIFIED**: they transcribe upstream's one-type-with-optional-access NURI, on the **two** fields this library uses. Not a "1:1 mirror" of `NuriV0`, as this line claimed until 2026-08-10: that type has TEN fields — `identity, target, entire_store, objects, signature, branch, overlay, access, topic, locator` (`engine/net/src/app_protocol.rs:181-194`) — of which `parseNuri` carries `target` and the cap half of `access`. The other eight have no counterpart here (the missing `locator` is a stated gap, `docs/readcap-and-nuri-model.md` § 4sexies). The ReadCap encoding they discriminate on is `r:{base64url(serde_bare(ObjectRef))}` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`), distinct from the `:k:` object/commit forms (`object_nuri`/`commit_nuri`, `types.rs:510-514`). No JS surface parses NURIs at level 2 or 3 — the real SDK takes plain strings — so these helpers never surface in signatures and survive only as internals.
|
||||
- `mintCap` — **NO COUNTERPART as an operation, and that is the point**: upstream a ReadCap is produced by the engine when a repo is created, never derived from a bare reference by a caller. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constant `OK` pretends nothing (`STAND_IN_CAP`, `emulated-verifier/caps.ts`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — the minting points of the emulation. At P1b the constant becomes a real key; at migration the function is deleted (the engine mints).
|
||||
- `mintCap` — **NO COUNTERPART as an operation, and that is the point**: upstream a ReadCap is produced by the engine when a repo is created, never derived from a bare reference by a caller. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and cap-enforcement has not yet supplied real key material; the constant `OK` pretends nothing (`STAND_IN_CAP`, `emulated-verifier/caps.ts`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — the minting points of the emulation. With cap-enforcement the constant becomes a real key; at migration the function is deleted (the engine mints).
|
||||
|
||||
## 3. The reach boundary — `emulated-verifier/reach.ts`
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ has no clear target image, that is a drift signal (see
|
||||
## Checklist
|
||||
|
||||
### 1. Emulated ReadCaps → real capabilities
|
||||
The shape is already the target's (P1a): a `ReadCap` is the document's key, a
|
||||
The shape is already the target's (the cap-surface work): a `ReadCap` is the document's key, a
|
||||
each identity holds a set of caps, and there is no read-ACL anywhere. So
|
||||
this step swaps the *emulated* key for the real one, not the model:
|
||||
|
||||
@@ -41,7 +41,7 @@ this step swaps the *emulated* key for the real one, not the model:
|
||||
whose cap the wallet holds. Remove them.
|
||||
- The write guard (`surface/ng-proxy.ts` `sparql_update` override) is a separate axis and
|
||||
is decorative today (every internal writer bypasses the proxy); it belongs to the
|
||||
P1b batch, not here.
|
||||
cap-enforcement batch, not here.
|
||||
|
||||
The access unit is already the document (`@graph`), matching the native per-repo cap
|
||||
model, so this is a key-material step, not a reshape.
|
||||
@@ -103,7 +103,7 @@ the POLYFILL-ERA block of `@ng-eventually/polyfill` — is deleted. The lib itse
|
||||
|
||||
## The one break already taken: `declareConnections`
|
||||
|
||||
P1a broke the consumer once, deliberately and early, so that migration would not.
|
||||
The cap-surface work broke the consumer once, deliberately and early, so that migration would not.
|
||||
The old surface was an ACL held in memory, which forced the consumer to re-declare
|
||||
every grant on every session (`declareConnections`). That call **disappears**: with
|
||||
delivered caps the grant moves to the moment a connection is *accepted* — one
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Established 2026-07-20**, VERIFIED by direct reading of the `nextgraph-rs` Rust core (except for points marked INFERRED). The `file:line` references are dated — line numbers are volatile, navigate by symbol/regex.
|
||||
|
||||
Purpose: to give the ground truth of NextGraph's access-rights model, in order to align the polyfill's `emulated-verifier/caps.ts` emulation (an ACL until P1a — the inverse of the real model; realigned 2026-07-28, see §5). This is the basis for the item "align ReadCap/WriteCap with NextGraph".
|
||||
Purpose: to give the ground truth of NextGraph's access-rights model, in order to align the polyfill's `emulated-verifier/caps.ts` emulation (an ACL until the cap-surface work — the inverse of the real model; realigned 2026-07-28, see §5). This is the basis for the item "align ReadCap/WriteCap with NextGraph".
|
||||
|
||||
> ## How to use this document — verify, never infer
|
||||
>
|
||||
@@ -378,9 +378,9 @@ One thing a consumer must not conclude from the emulation: that a public documen
|
||||
|
||||
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
|
||||
|
||||
**Realigned 2026-07-28 (batch P1a).** `packages/polyfill/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 (the cap-surface batch).** `packages/polyfill/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.
|
||||
|
||||
| | Real NextGraph | caps.ts emulation (post-P1a) |
|
||||
| | Real NextGraph | caps.ts emulation (post-cap-surface) |
|
||||
|---|---|---|
|
||||
| Nature | possession of a **key** | possession of a **key** — recorded per identity, indexed by the cap-less NURI |
|
||||
| Grant | seal the key (crypto_box) to the inbox | `inbox.share(doc, toUser)` → an inbox deposit, absorbed inline on read |
|
||||
@@ -390,7 +390,7 @@ One thing a consumer must not conclude from the emulation: that a public documen
|
||||
| Ref. without rights | **cap-less NURI** (no `r:` segment) | same — `Nuri` names, `ReadCap` names and reads |
|
||||
| Public store | the broker serves the outer overlay; the ReadCap is **downloaded** from it | `public-store.ts` — the cap is exposed on the document and fetched through the machinery's door, then held like any other. Filed apart (`learnFromPublicStore`) so it grants reading and **not** writing |
|
||||
|
||||
**The divergence that REMAINS**: the stand-in cap value is the constant `OK` rather than a secret. The read paths that once consulted no cap at all are now confined to the connected virtual user (`emulated-verifier/reach.ts`, 2026-07-30) — `docs.sparqlQuery`/`sparqlUpdate` and `subscribeDoc` are guarded, the inbox is read only by its owner, and the shim's own machinery moved to unguarded primitives that are never exported. So what is left for **P1b** is per-document encryption: replacing one constant with a real key. Until then, nothing may be claimed "anonymous" or "private".
|
||||
**The divergence that REMAINS**: the stand-in cap value is the constant `OK` rather than a secret. The read paths that once consulted no cap at all are now confined to the connected virtual user (`emulated-verifier/reach.ts`, 2026-07-30) — `docs.sparqlQuery`/`sparqlUpdate` and `subscribeDoc` are guarded, the inbox is read only by its owner, and the shim's own machinery moved to unguarded primitives that are never exported. So what is left for **cap-enforcement** is per-document encryption: replacing one constant with a real key. Until then, nothing may be claimed "anonymous" or "private".
|
||||
|
||||
**App-facing**: `declareConnections` (on the consumer side), which re-declared "my connections read my protected entities" **every session**, was an artifact of the ephemeral ACL — **it disappears**. The grant moves to the moment a connection is accepted (`inbox.share` once, per document), which is a consumer **re-architecture**, not an API swap.
|
||||
|
||||
@@ -398,7 +398,7 @@ One thing a consumer must not conclude from the emulation: that a public documen
|
||||
|
||||
- "**protected scope = my network can read**" is **not** an ACL checked by the broker: it is "I have **sealed my read key** to each of my connections". The "scope = ACL" mental model is wrong at the NextGraph level.
|
||||
- **Anonymous references are possible**: putting a **cap-less NURI** in a third party's collection lets that third party **name/count** without **reading the identity**; the cap-bearing one is sealed separately to the authorized parties only. (Basis for a presence model of the form "self-owned participation + curated cap-less Set + cap sealed to the connections".)
|
||||
- **Alignment DONE for the surface (P1a, 2026-07-28)**: the emulated ACL is gone, replaced by per-identity cap possession + per-document delivery to an inbox; `declareConnections`-as-a-re-declared-ACL has disappeared. What remains for the real cap operations is swapping the stand-in key value (`OK`) for the real one and closing the bypasses (P1b) — a key-material step, not a reshape. See `migration-guide.md` §1.
|
||||
- **Alignment DONE for the surface (cap-surface, 2026-07-28)**: the emulated ACL is gone, replaced by per-identity cap possession + per-document delivery to an inbox; `declareConnections`-as-a-re-declared-ACL has disappeared. What remains for the real cap operations is swapping the stand-in key value (`OK`) for the real one and closing the bypasses (cap-enforcement) — a key-material step, not a reshape. See `migration-guide.md` §1.
|
||||
|
||||
## Caveats / gaps
|
||||
|
||||
|
||||
+5
-5
@@ -304,7 +304,7 @@ wallet, everything readable) the lib reproduces that with a read-filtered view:
|
||||
type would be a concept NextGraph does not have. The stand-in key value is the
|
||||
constant `OK` (see the module header): the only question the emulation answers is
|
||||
*do I hold this cap or not*, so the value says exactly that and pretends nothing
|
||||
more. P1b, not P1a, is the batch that turns the shape into a protection.
|
||||
more. Cap-enforcement, not cap-surface, is the batch that turns the shape into a protection.
|
||||
- **`emulated-verifier/read-filter.ts`** — `makeReadFilteredView` wraps the reactive set in a
|
||||
`Proxy`: iteration / `size` / `forEach` keep only items whose `@graph` the
|
||||
current holder holds; everything else (`add`, `delete`, `has`, `getById`…) forwards to
|
||||
@@ -340,7 +340,7 @@ Three ways a cap arrives, and there are no others:
|
||||
- **Creation.** `createEntityDoc(id, scope)` writes the cap on the store's emulated
|
||||
Store branch (`shim:readCap`) and the creator holds it. The consumer declares
|
||||
nothing, and the cap is minted exactly ONCE — the stored value is the held value,
|
||||
which is what keeps this correct when P1b makes the key real.
|
||||
which is what keeps this correct when cap-enforcement makes the key real.
|
||||
- **Re-listing.** `listMyEntityDocs(id, scope)` READS those records back. It does not
|
||||
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
|
||||
@@ -418,10 +418,10 @@ registry. And dynamically: the cap lands in Bob's inbox, his client processes it
|
||||
the read that was empty yields the content — the held-caps signal re-running it.
|
||||
|
||||
That is what real NextGraph does, and it holds **without a line of encryption** —
|
||||
which is what makes the P1a (shape) / P1b (enforcement) split honest rather than
|
||||
which is what makes the cap-surface (shape) / cap-enforcement (enforcement) split honest rather than
|
||||
cosmetic. Proven in `test/cross-user-access.test.ts`.
|
||||
|
||||
> **After P1a the shape is right and the isolation is still fake.** The stand-in key
|
||||
> **After the cap-surface batch the shape is right and the isolation is still fake.** The stand-in key
|
||||
> is a constant, and several read paths (`docs.sparqlQuery`/`sparqlUpdate`, the whole
|
||||
> inbox, `store-registry`, `subscribe`, `open-repo`) consult no cap at all — worse,
|
||||
> any wallet can reach any document. That is the subject of
|
||||
@@ -501,7 +501,7 @@ The asymmetry that matters:
|
||||
the whole reachability graph. A deposit grants the depositor nothing in return —
|
||||
upstream it is an anonymous sealed box.
|
||||
- **Reading an inbox is confined to its owner** (`isOwnInbox`, enforced in `read` /
|
||||
`readSynced`, hence in `watch`). Since P1a routes ReadCaps through deposits, an
|
||||
`readSynced`, hence in `watch`). Since the cap-surface batch routes ReadCaps through deposits, an
|
||||
unguarded read let anyone who knew an inbox NURI collect the caps addressed to its
|
||||
owner — defeating directed sharing. Anonymous owns no inbox and reads none.
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ The 25 current modules, with the two splits' offspring shown where a module divi
|
||||
| `index.ts` | `src/index.ts` (unchanged path) | The SDK-entry manifest. New rule made checkable: imports only from `surface/` and `model/`. |
|
||||
| `polyfill.ts` | `src/polyfill.ts` (path unchanged) + **new** `shared-wallet/bootstrap.ts` | Split: the entry keeps the re-exports; the config store (`configure`, `getConfig`, `registryDeps`, the current-user relay, the `CapRegistry` singleton wiring) moves to `shared-wallet/bootstrap.ts` — it is the injection machinery with NO COUNTERPART by design (`docs/api-contract.md` § 1). Side effect: removes the current entry↔internal import cycles (`polyfill.ts:16` ↔ `connect.ts:37`; `polyfill.ts:228` ↔ `inbox.ts:32`). |
|
||||
| `types.ts` | `model/types.ts`, minus `NgLike` / `UseShapeLike` → `shared-wallet/bootstrap.ts` | `Nuri`/`ReadCap`/`Scope` are level-1-verified vocabulary; `PrincipalId` stays with a note (target: the wallet user; polyfill: a relayed id). `NgLike`/`UseShapeLike` describe the *injection*, not the target — they belong to the bootstrap. See § 5 for the published-type wrinkle this creates. |
|
||||
| `nuri.ts` | `model/nuri.ts`, minus `mintCap` → `emulated-verifier/` | The guards and `targetOf`/`parseNuri` are the model transcription. `mintCap` is the emulation's minting point — upstream only the engine mints, at repo creation (level 1, `BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`) — and its presence in the model module contradicts the module's own header ("nothing on the surface turns a bare reference into a cap"). P1b swaps its constant; migration deletes it. |
|
||||
| `nuri.ts` | `model/nuri.ts`, minus `mintCap` → `emulated-verifier/` | The guards and `targetOf`/`parseNuri` are the model transcription. `mintCap` is the emulation's minting point — upstream only the engine mints, at repo creation (level 1, `BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`) — and its presence in the model module contradicts the module's own header ("nothing on the surface turns a bare reference into a cap"). Cap-enforcement swaps its constant; migration deletes it. |
|
||||
| `sparql.ts` | `surface/sparql.ts` | Published, generic injection-safety utilities with NO COUNTERPART and none expected (`docs/api-contract.md` § 11) — the one surface family that survives *any* migration unchanged. Placed with the surface because it is published and documented there; the folder note must carry this exception. |
|
||||
| `docs.ts` | `surface/docs.ts` — stays whole | Level-2 passthroughs whose in-body cap filing and reach guard *mirror the target's own composition* (§ 4 ruling). The mint and the guard become named imports from `emulated-verifier/` — the visible seam. |
|
||||
| `lifecycle.ts` | `surface/lifecycle.ts` | Pure forwarding to the injected level-2/3 calls (`docs/api-contract.md` § 2). |
|
||||
@@ -130,7 +130,7 @@ Full mixed list: `docs.ts`, `surface/read-model.ts`, `inbox.ts`, `subscribe.ts`,
|
||||
|
||||
## 5. Cost and risk
|
||||
|
||||
**What does not change: the published surface.** Both entries keep their `src/` paths *(they were merged into one on 2026-08-07)*; `package.json`'s `exports` map is untouched; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entries with identical contents. A consumer application importing the two entries sees nothing — with one deliberate exception below.
|
||||
**What does not change: the published surface.** Both entries keep their `src/` paths *(they were merged into one on 2026-08-07, and `package.json`'s `exports` map now maps only `.`)*; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entry with identical contents. A consumer application importing the entry sees nothing — with one deliberate exception below.
|
||||
|
||||
**Import churn — the inventory:**
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Without a minimum of crypto simulation, damaging shortcuts get taken (reading th
|
||||
|
||||
Concretely, **in the target**: a document's data is **stored encrypted** (per-doc symmetric encryption, however lightweight); the **ReadCap = the key**; without it, **decrypting/reading is impossible**. No ACL, no plaintext accessible "on the side". Obtaining read access = **holding the key**, exactly as in the target model.
|
||||
|
||||
> **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is **P1b**, and it is one function (`mintCap`, in `emulated-verifier/caps.ts` — this said `nuri.ts` until 2026-08-10). Until it lands, nothing this library does may be described as anonymous or private.
|
||||
> **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is the **cap-enforcement** work, and it is one function (`mintCap`, in `emulated-verifier/caps.ts` — this said `nuri.ts` until 2026-08-10). Until it lands, nothing this library does may be described as anonymous or private.
|
||||
|
||||
## Shape consequences (to respect everywhere)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user