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:
Sylvain Duchesne
2026-07-06 14:02:16 +02:00
parent d39b12885a
commit 63ecfeeff8
31 changed files with 1059 additions and 1396 deletions
+38 -33
View File
@@ -1,25 +1,29 @@
/**
* Capability emulation — generic, no domain rules. Models NextGraph **ReadCaps**
* (and write caps) as faithfully as a data layer can.
* Capability emulation — generic, with no domain rules. It models NextGraph
* ReadCaps (and write caps) as a data layer can.
*
* In NextGraph a ReadCap is possession of a *document's* (repo's) read key: the
* broker only ever delivers documents the wallet holds a cap for. The access
* UNIT is therefore the **document = repo**, identified here by its NURI — the
* `@graph` an item lives in — **never the item**. (Verified in nextgraph-rs:
* a store is just a container repo; holding a store's cap does NOT grant the
* repos it references — each document needs its own cap. So this registry is
* purely per-document, with NO store-level inheritance.)
* In NextGraph a ReadCap is possession of a document's (repo's) read key: the
* broker only delivers documents the wallet holds a cap for. The access unit is
* therefore the document = repo, identified here by its NURI — the `@graph` an
* item lives in, rather than the item. (A store is just a container repo, and
* holding a store's cap does not grant the repos it references — each document
* carries its own cap — so this registry is purely per-document, with no
* store-level inheritance.)
*
* At migration this whole layer disappears: the broker/verifier enforces the
* real caps and `useShape` already returns only authorized documents.
* Sharing here is DIRECTED: a grant issues one grantee the read cap of one
* document (`grantRead(doc, granteeId)`). Whether two identities are "connected"
* — and therefore whether such a grant should be issued — is an application
* concept the consumer owns; this layer only records the resulting per-document
* grants. At migration this whole layer disappears: the broker/verifier enforces
* the real caps and `useShape` returns only authorized documents.
*/
import type { Nuri, PrincipalId, Scope } from "./types";
/**
* Who holds the read/write cap of each document. The consumer populates it via
* cap operations (create-public, grant-to-a-connection…) exactly as it will in
* the target; this layer enforces possession generically — it knows no policy.
* cap operations (make-public, directed grant…) exactly as it will in the
* target; this layer enforces possession generically, with no policy of its own.
*/
export class CapRegistry {
/** doc NURI → principals holding its READ cap. */
@@ -29,14 +33,14 @@ export class CapRegistry {
/** doc NURIs readable by everyone (public_store repos — no cap needed). */
private publicDocs = new Set<Nuri>();
/** doc NURI → its declared (scope, owner), as recorded at {@link open}. Lets
* a later, connection-aware sharing act (see {@link grantReadToConnections})
* re-derive which documents are `protected` and who owns them, without the
* consumer re-supplying that per-document — it already declared it at open. */
* the consumer re-derive which documents are `protected` and who owns them
* (see {@link protectedDocsOf}) so it can issue directed grants, without
* re-supplying that per-document — it already declared it at open. */
private policy = new Map<Nuri, { scope: Scope; owner: PrincipalId }>();
/** Grant `principal` the READ cap of document `doc`. */
grantRead(doc: Nuri, principal: PrincipalId): void {
add(this.readers, doc, principal);
/** Grant `grantee` the READ cap of document `doc` — a directed grant. */
grantRead(doc: Nuri, grantee: PrincipalId): void {
add(this.readers, doc, grantee);
}
/** Grant `principal` the WRITE cap of document `doc`. */
@@ -62,23 +66,24 @@ export class CapRegistry {
}
/**
* Extend the read caps of every `protected` document so its owner's direct
* connections may read it — the sharing act "protected = owner + connections".
* `neighborsOf(owner)` yields the principals connected to `owner` (the consumer
* supplies its social graph; this layer invents no relationship). Public docs
* are already world-readable; private docs are untouched (owner only). Additive
* and idempotent: re-running after the connection graph changes only ever adds
* read caps for the current connections.
* The `protected` documents owned by `owner`, as recorded at {@link open}. The
* consumer uses this to issue directed read grants: it decides who may read an
* owner's protected documents (its own relationship concept) and calls
* {@link grantRead} on each of these documents for each such reader. Public
* documents are already world-readable and private documents stay owner-only,
* so only the protected ones are surfaced here.
*
* This is the per-document ReadCap image of a native cap operation: in the
* target, sharing a protected repo with a connection issues that connection the
* repo's ReadCap. Here it grants the emulated read cap on the same unit.
* This mirrors a native cap operation: in the target, sharing a protected repo
* with another identity issues that identity the repo's ReadCap. Here the
* consumer selects the documents via this accessor and grants the emulated read
* cap on the same unit.
*/
grantReadToConnections(neighborsOf: (owner: PrincipalId) => Iterable<PrincipalId>): void {
for (const [doc, { scope, owner }] of this.policy) {
if (scope !== "protected") continue;
for (const connection of neighborsOf(owner)) this.grantRead(doc, connection);
protectedDocsOf(owner: PrincipalId): Nuri[] {
const out: Nuri[] = [];
for (const [doc, { scope, owner: o }] of this.policy) {
if (scope === "protected" && o === owner) out.push(doc);
}
return out;
}
/** Is `doc` under any READ-cap policy? (Undeclared docs are not enforced.) */