feat(client): real per-document isolation + bilateral connections + deposit guards
The ReadCap filter now enforces on per-entity documents (consumers create one doc per entity, so each has a declared policy — private→owner, protected→owner+ connections, public→all). Isolation is genuinely active, not dormant. - connections.ts (new): a BILATERAL connection registry — a link grants protected read only when BOTH sides have asserted it (each assertion bound to its author). A unilateral/self-declared connection grants nothing (closes the confused-deputy hole). declareConnections is authenticated to the current identity. - inbox.post: `from` is bound to the current identity — a spoofed `from` throws. - discovery.submitToIndex: PUBLIC-ONLY — a governed non-public doc is refused (no protected/private leak into the world-readable index). - docs/simulation.md: documents this as application-level emulated isolation on a shared wallet (not crypto); at NextGraph maturity → real caps, consumer unchanged. 89 tests pass (+10 covering: active protected isolation via bilateral connect, unilateral grants nothing, from-spoof rejected, non-public submit refused). tsc rc=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
resetStoreRegistry,
|
||||
resetConfig,
|
||||
setCurrentUser,
|
||||
getCaps,
|
||||
resetCaps,
|
||||
} from "../src/polyfill";
|
||||
import { resetRegistryCache, ensureAccount } from "../src/store-registry";
|
||||
import type { RegistrySession } from "../src/store-registry";
|
||||
@@ -20,6 +22,7 @@ afterAll(() => {
|
||||
resetConfig();
|
||||
resetStoreRegistry();
|
||||
setCurrentUser(null);
|
||||
resetCaps();
|
||||
});
|
||||
|
||||
test("throws a clear error when configureStoreRegistry() was not called", async () => {
|
||||
@@ -171,6 +174,7 @@ test("submitToIndex creates the @index special account on first sight (3 docs)",
|
||||
});
|
||||
|
||||
test("submit → read round-trips the reference as an index entry", async () => {
|
||||
setCurrentUser("alice"); // `from` is bound to the current identity
|
||||
const ref = { nuri: "did:ng:o:event1", title: "Concert au parc" };
|
||||
await submitToIndex(ref, { from: "alice", ts: 100 });
|
||||
const entries = await readIndex();
|
||||
@@ -197,8 +201,9 @@ test("a reference submitted by A is discovered by a NON-connected reader via the
|
||||
|
||||
test("readIndex deduplicates identical references (materialization moderation point)", async () => {
|
||||
const ref = { nuri: "did:ng:o:dup", title: "Twice" };
|
||||
await submitToIndex(ref, { from: "alice", ts: 100 });
|
||||
await submitToIndex(ref, { from: "bob", ts: 200 }); // duplicate reference
|
||||
// Anonymous submissions (dedup keys on the ref, not the submitter).
|
||||
await submitToIndex(ref, { from: null, ts: 100 });
|
||||
await submitToIndex(ref, { from: null, ts: 200 }); // duplicate reference
|
||||
const entries = await readIndex();
|
||||
expect(entries).toHaveLength(1); // surfaced once
|
||||
});
|
||||
@@ -209,6 +214,30 @@ test("from: null makes an anonymous submission", async () => {
|
||||
expect(entries[0]!.from).toBeNull();
|
||||
});
|
||||
|
||||
// (d) PUBLIC-ONLY: a protected/private document must NOT be submittable to the
|
||||
// world-readable discovery index; a public (or ungoverned) document is fine.
|
||||
test("(d) submitToIndex refuses a PROTECTED/PRIVATE document (public-only)", async () => {
|
||||
resetCaps();
|
||||
// A PROTECTED and a PRIVATE governed document, and a PUBLIC one.
|
||||
getCaps().open("did:ng:o:prot", "protected", "alice");
|
||||
getCaps().open("did:ng:o:priv", "private", "alice");
|
||||
getCaps().open("did:ng:o:pub", "public", "alice");
|
||||
|
||||
// Submitting the protected doc's NURI is REJECTED.
|
||||
await expect(
|
||||
submitToIndex({ nuri: "did:ng:o:prot" }, { from: null, doc: "did:ng:o:prot" }),
|
||||
).rejects.toThrow(/PUBLIC|public-only|protected\/private/i);
|
||||
// Private too.
|
||||
await expect(
|
||||
submitToIndex({ nuri: "did:ng:o:priv" }, { from: null, doc: "did:ng:o:priv" }),
|
||||
).rejects.toThrow(/PUBLIC|public-only|protected\/private/i);
|
||||
// The PUBLIC document passes.
|
||||
await submitToIndex({ nuri: "did:ng:o:pub" }, { from: null, doc: "did:ng:o:pub", ts: 1 });
|
||||
const entries = await readIndex();
|
||||
expect(entries.map((e) => (e.ref as { nuri: string }).nuri)).toEqual(["did:ng:o:pub"]);
|
||||
resetCaps();
|
||||
});
|
||||
|
||||
test("INDEX_ACCOUNT lives in the reserved namespace (no typed username can equal it)", () => {
|
||||
// The index account occupies a key no user input can produce: it is prefixed
|
||||
// with a NUL control char, which a user cannot type into a username field and
|
||||
|
||||
Reference in New Issue
Block a user