docs+refactor(client): fidelity pass — id identity, drop connections, no faux-login, accurate NextGraph framing
Align the polyfill's surface and docs with the verified NextGraph reality and
remove application-level concepts:
- Identity is an ID, not a username: AccountRecord.id, shim predicate shim:id,
normalizeId; accounts core becomes IdentityStore (set/clear/get) — the faux
login/logout framing is gone (identity is set at wallet-import time).
- Relationship/connection is an application concept, not a platform primitive
(NextGraph has no bilateral-connection primitive: grantee is unpersisted
scaffolding, cap-send is unimplemented). Remove connections.ts; caps exposes
only a directed grantRead(doc, granteeId) + a read-only protectedDocsOf(owner).
Delete the now-dead isolation.ts social-visibility axis.
- Inbox docs: NextGraph has no separate curator — the recipient's own verifier
unseals and applies each queued sealed message inline (process_inbox);
inbox_post_link is a proposed/future API. Stop attributing the emulated
curator to the platform.
- Read isolation reframed around the outcome: no cap -> empty union read;
targeted read of an unheld repo -> RepoNotFound; cap introspection
(canRead/governsRead) is emulation-only with no NextGraph API behind it.
- read-model.md corrected: the listing path is per-doc ANCHORED default-graph
queries, never the anchorless GRAPH ?g union (that is O(wallet)); the probe
section no longer claims the opposite.
- README recap table restructured (target | current NextGraph status | current
emulation); INDEX_ACCOUNT documented as reservedAccount("index") in the
sentinel namespace; de-domained generic-layer comments; softened tone.
Consumer application (Festipod) rewired separately to own the relationship
concept and feed the lib an id. Lib gates: bun test 83 pass / 0 fail, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,14 +73,18 @@ grant the repos it contains — **you need each repo's own ReadCap**. The option
|
||||
users/quorum (write/permissions), **not** read-cap possession. (Repos of a
|
||||
`private_store` inherit implicitly.)
|
||||
|
||||
> **Consequence for this lib's emulation (see [`simulation.md`](./simulation.md)):**
|
||||
> the read access UNIT is the **repo = each item's `@graph`** — a per-DOCUMENT
|
||||
> Consequence for this lib's emulation (see [`simulation.md`](./simulation.md)):
|
||||
> the read access unit is the repo = each item's `@graph` — a per-document
|
||||
> filter, never per-store and never per-item. This is exactly what
|
||||
> `caps.ts` (`CapRegistry`) and `read-filter.ts` model: no store-level
|
||||
> inheritance, purely per-document caps. In a mono-store layout (all items in one
|
||||
> repo) the filter is therefore all-or-nothing on that document — which *is* the
|
||||
> native behaviour, and why fine-grained isolation requires one document per
|
||||
> entity.
|
||||
> entity. Read isolation is cryptographic in the target: with no cap for a repo, a
|
||||
> union / reactive read returns empty (the repo is never decrypted), while a
|
||||
> targeted read of an unheld repo returns `RepoNotFound`. There is no
|
||||
> cap-introspection API — the polyfill's `canRead` / `governsRead` are
|
||||
> emulation-only, with no NextGraph API behind them.
|
||||
|
||||
### Store ↔ document confusion (recurring)
|
||||
|
||||
@@ -98,36 +102,40 @@ offline"*; *"removing permissions … requires a SyncSignature"* (synchronous).
|
||||
|
||||
## Inbox
|
||||
|
||||
**Every document has a native inbox.** A non-editor can **deposit a link (DID
|
||||
cap)** into it without being invited as an editor; the owner **moderates**. NURI:
|
||||
Every document has a native inbox. A non-editor can deposit a link (DID
|
||||
cap) into it without being invited as an editor; the owner moderates. NURI:
|
||||
`did:ng:d:<inbox_id>`. Content: the `InboxMsgContent` enum (`ContactDetails`,
|
||||
`DialogRequest`, **`Link`**, `Patch`, `ServiceRequest`, `ExtRequest`,
|
||||
`RemoteQuery`, `SocialQuery`…). Messages are **sealed** (`crypto_box::seal`) to
|
||||
the inbox pubkey → only the owner decrypts. The `from` field is **optional** → an
|
||||
**anonymous** sender is possible. This is the "identified if known, anonymous
|
||||
`DialogRequest`, `Link`, `Patch`, `ServiceRequest`, `ExtRequest`,
|
||||
`RemoteQuery`, `SocialQuery`…). Messages are sealed (`crypto_box::seal`) to
|
||||
the inbox pubkey, so only the owner decrypts. The `from` field is optional, so an
|
||||
anonymous sender is possible. This is the "identified if known, anonymous
|
||||
otherwise" behaviour native to the protocol.
|
||||
|
||||
### The inbox is NOT usable from the JS SDK
|
||||
The recipient's own verifier unseals each queued message and applies it inline when
|
||||
it processes its inbox — there is no separate curator or materialization service.
|
||||
|
||||
### The inbox is not usable from the JS SDK
|
||||
|
||||
- `app_request(request)` is exposed, and `AppRequestCommandV0::InboxPost` +
|
||||
`AppRequest::inbox_post()` exist. **BUT** the verifier's `request_processor`
|
||||
has **no `InboxPost` arm** (arms actually handled: `OrmStart(Discrete)`,
|
||||
`AppRequest::inbox_post()` exist, but the verifier's `request_processor`
|
||||
has no `InboxPost` arm (arms actually handled: `OrmStart(Discrete)`,
|
||||
`Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`,
|
||||
`QrCodeProfile(Import)`, `Header`, `Create`, `FilePut`). Sending an `InboxPost`
|
||||
triggers nothing.
|
||||
- Building an `InboxPost` requires crypto sealing on the Rust side; **no wasm
|
||||
helper** exposes it.
|
||||
- Inbox deposit is only triggered **internally** by `QrCodeProfileImport`
|
||||
- Building an `InboxPost` requires crypto sealing on the Rust side; no wasm
|
||||
helper exposes it. A high-level `inbox_post_link` is a proposed/future API, not
|
||||
yet present.
|
||||
- Inbox deposit is only triggered internally by `QrCodeProfileImport`
|
||||
(`post_to_inbox(new_contact_details)`) and `social_query_start` (contact
|
||||
propagation via inbox).
|
||||
|
||||
**Consequence:** there is no clean way to "drop a Link" into an arbitrary
|
||||
document's inbox from the JS SDK today. This lib emulates the inbox instead of
|
||||
patching the broker — see [`simulation.md`](./simulation.md) (emulated inbox +
|
||||
curator) and [`fork-inbox-fallback.md`](./fork-inbox-fallback.md) (the Rust-patch
|
||||
path NOT taken). A related exposed primitive: `social_query_start` (a federated
|
||||
query via inbox up to `degree` hops) exists but is limited to **contacts** — it
|
||||
does not cover an anonymous notification to a non-connected host.
|
||||
patching the broker — see [`simulation.md`](./simulation.md) (emulated inbox) and
|
||||
[`fork-inbox-fallback.md`](./fork-inbox-fallback.md) (the Rust-patch path not taken).
|
||||
A related exposed primitive: `social_query_start` (a federated query via inbox up to
|
||||
`degree` hops) exists but is limited to contacts — it does not cover an anonymous
|
||||
notification to a non-connected host.
|
||||
|
||||
## The query capability — ONE local store, named graphs, union queries
|
||||
|
||||
@@ -190,36 +198,36 @@ from JS today a repo becomes queryable ONLY by being `doc_create`d in this sessi
|
||||
(own docs) or synced by an internal path — never on demand by NURI+ReadCap.
|
||||
|
||||
**Consequence for this lib's mono-wallet polyfill:** every account's documents are
|
||||
`doc_create`d in the ONE shared wallet within the SAME session, so they are ALL
|
||||
already in `self.repos`. `read-model.ts` reads the **bounded, by-need** set of docs
|
||||
with ONE **anchored** `sparql_query` per doc (`SELECT ?s ?p ?o WHERE { ?s ?p ?o }`,
|
||||
`doc_create`d in the one shared wallet within the same session, so they are all
|
||||
already in `self.repos`. `read-model.ts` reads the bounded, by-need set of docs
|
||||
with one anchored `sparql_query` per doc (`SELECT ?s ?p ?o WHERE { ?s ?p ?o }`,
|
||||
anchor = the doc NURI): the anchor resolves that same-session repo directly (no
|
||||
separate open needed) and restricts the query to its graph → O(1) per doc,
|
||||
separate open needed) and restricts the query to its graph, so it is O(1) per doc,
|
||||
independent of the store's size. An absent repo throws `RepoNotFound` on its own
|
||||
read and is skipped, never aborting the batch.
|
||||
|
||||
**Do NOT anchorless-union-scan on the read path.** An anchorless
|
||||
`SELECT … WHERE { GRAPH ?g { ?s ?p ?o } }` spans EVERY named graph in the store —
|
||||
O(wallet size). On a **shared / bloated** wallet that accumulates docs across runs
|
||||
that was O(wallet) and timed out (~90s observed on `readUnion` / probe reads). The
|
||||
per-doc anchored read makes a non-empty wallet irrelevant. At the real multi-store
|
||||
The read path avoids an anchorless union-scan. An anchorless
|
||||
`SELECT … WHERE { GRAPH ?g { ?s ?p ?o } }` spans every named graph in the store —
|
||||
O(wallet size). On a shared wallet that accumulates docs across runs that cost grows
|
||||
with the whole wallet, which is why the read path is per-doc anchored: the anchored
|
||||
read makes a non-empty wallet irrelevant. At the real multi-store
|
||||
migration this is unchanged (the anchored read is native); only bringing a repo into
|
||||
the session changes: opening a real per-user store repo by cap becomes a native
|
||||
broker sync (the `OpenRepo` TODO at `verifier.rs:1423`). Opening still requires the
|
||||
repo's **NURI + ReadCap** — there is **no store-level read inheritance** (see
|
||||
repo's NURI + ReadCap — there is no store-level read inheritance (see
|
||||
§ Capability / ReadCap granularity).
|
||||
|
||||
### The union is READ-ONLY — writes must target one document
|
||||
### The union is read-only — writes must target one document
|
||||
|
||||
`resolve_target_for_sparql(update=true)` returns `InvalidTarget` for `UserSite` /
|
||||
`None` (`request_processor.rs:275-282`). So `sparql_update` cannot write "to the
|
||||
union": every write must name **one** document's `@graph` — exactly what the
|
||||
union": every write must name one document's `@graph` — exactly what the
|
||||
polyfill's `docs.sparqlUpdate` already does.
|
||||
|
||||
### No reactive SPARQL — `sparql_query` is one-shot
|
||||
|
||||
`sparql_query` is non-streamed: it computes a `QueryResults` and returns once
|
||||
(`lib-wasm/src/lib.rs:352-405` / `553-610`). There is **no** "subscribe to a union
|
||||
(`lib-wasm/src/lib.rs:352-405` / `553-610`). There is no "subscribe to a union
|
||||
query". The only reactive primitives are the streamed ones: `orm_start_graph`,
|
||||
`orm_start_discrete`, `doc_subscribe`, `app_request_stream`.
|
||||
|
||||
@@ -228,18 +236,18 @@ query". The only reactive primitives are the streamed ones: `orm_start_graph`,
|
||||
The reactive ORM is structurally unfit for a fan-out of per-entity / not-yet-synced
|
||||
graphs, and this is *why* subscribing such a fan-out hangs:
|
||||
|
||||
- `OrmStartGraph` first loops over EVERY graph in the requested scope and calls
|
||||
- `OrmStartGraph` first loops over every graph in the requested scope and calls
|
||||
`open_for_target(&nuri.target, /*publisher*/ true)` on each
|
||||
(`request_processor.rs:53-66`), and `orm/graph/initialize.rs` does the same
|
||||
fan-out again for the graphs the ORM discovers (~`125-128`).
|
||||
- `open_for_target` → `resolve_target` → `self.repos.get(repo_id).ok_or(RepoNotFound)`
|
||||
(`request_processor.rs:286-294` calling `resolve_target` at `:147`, the
|
||||
`RepoNotFound` at `:155/:163`).
|
||||
- A **freshly-created per-entity doc**, or any **not-yet-synced other-account doc**,
|
||||
is absent from `self.repos` → `RepoNotFound` propagates through the `?` and
|
||||
**aborts the whole `orm_start_graph`**. The subscription never emits its initial →
|
||||
the ORM `readyPromise` never resolves → the multi-second hang observed (≈75s)
|
||||
when subscribing a fan-out of per-entity graphs.
|
||||
- A freshly-created per-entity doc, or any not-yet-synced other-account doc,
|
||||
is absent from `self.repos`, so `RepoNotFound` propagates through the `?` and
|
||||
aborts the whole `orm_start_graph`. The subscription never emits its initial, so
|
||||
the ORM `readyPromise` never resolves and the subscription hangs when a fan-out of
|
||||
per-entity graphs is passed in.
|
||||
|
||||
**Consequence:** passing per-entity / unsynced graphs to the reactive ORM is broken.
|
||||
Listing must go through a one-shot union `sparql_query` instead — see
|
||||
@@ -347,12 +355,12 @@ the idea of "a service with its own wallet sharing global data".
|
||||
that user.
|
||||
|
||||
**Consequence for a "global document" (e.g. a discovery index):** the only path
|
||||
glimpsed is a **singleton app** whose global document is administered by the
|
||||
developer-user — **but this is not implemented and not guaranteed** (simpler
|
||||
paths may exist; to explore later). The **incorrect** model to avoid: "a
|
||||
dedicated service with its own wallet sharing a freely-readable index" — that
|
||||
does not exist in NextGraph (a service is mono-user, no global data). This is why
|
||||
a global-index curator package is **deferred** in this lib (see the top-level
|
||||
glimpsed is a singleton app whose global document is administered by the
|
||||
developer-user — though this is not implemented and not guaranteed (simpler
|
||||
paths may exist; to explore later). The model that does exist is this
|
||||
singleton-app one; a dedicated service with its own wallet sharing a
|
||||
freely-readable index is not a NextGraph shape (a service is mono-user, no global
|
||||
data). This is why a global-index package is deferred in this lib (see the top-level
|
||||
README).
|
||||
|
||||
## Third-party wallet auto-import constraint
|
||||
@@ -382,14 +390,15 @@ real way to eliminate the cross-origin round-trip is to self-host/fork the ng-ap
|
||||
|
||||
## Login is not programmable
|
||||
|
||||
NextGraph login is a **web redirect** to the broker page (`nextgraph.net`). There
|
||||
NextGraph login is a web redirect to the broker page (`nextgraph.net`). There
|
||||
is no way to open a wallet silently — at least one broker-redirect pass per device
|
||||
is required. Session persistence: the wallet is remembered iframe-side
|
||||
(`localStorage` long-term + `sessionStorage` for the active session); on reload,
|
||||
`init()` recovers the session **without re-triggering the redirect** while the
|
||||
`init()` recovers the session without re-triggering the redirect while the
|
||||
broker session exists (`sdk/js/web/src/index.ts`, `sdk/js/api-web/main.ts`). A
|
||||
full browser restart (losing `sessionStorage`) can re-trigger the gate. A real
|
||||
logout IS exposed (`ng.session_stop()`, `ng.user_disconnect()`,
|
||||
`ng.wallet_close()` in `sdk/js/lib-wasm/src/lib.rs`) but **forces a new
|
||||
redirect** afterwards. This lib's faux login sidesteps all of it — see the faux
|
||||
login in [`simulation.md`](./simulation.md).
|
||||
logout is exposed (`ng.session_stop()`, `ng.user_disconnect()`,
|
||||
`ng.wallet_close()` in `sdk/js/lib-wasm/src/lib.rs`) but forces a new
|
||||
redirect afterwards. This lib's identity store sidesteps all of it — the identity
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user