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:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* ConnectionRegistry — BILATERAL connection materialization (T03.h).
|
||||
*
|
||||
* A connection is live only when BOTH sides have asserted the other. A unilateral
|
||||
* (self-declared) assertion yields no neighbour — the defence against a reader who
|
||||
* fakes a connection to an owner to read that owner's protected documents.
|
||||
*/
|
||||
import { test, expect } from "bun:test";
|
||||
import { ConnectionRegistry, bilateralConnections } from "../src/connections";
|
||||
|
||||
test("a UNILATERAL assertion yields NO neighbour", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assert("mallory", "alice"); // mallory self-declares; alice never asserts back
|
||||
expect([...reg.neighbors("mallory")]).toEqual([]);
|
||||
expect([...reg.neighbors("alice")]).toEqual([]);
|
||||
});
|
||||
|
||||
test("a BILATERAL assertion (both sides) materializes the link", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assert("alice", "bob");
|
||||
reg.assert("bob", "alice");
|
||||
expect([...reg.neighbors("alice")]).toEqual(["bob"]);
|
||||
expect([...reg.neighbors("bob")]).toEqual(["alice"]);
|
||||
});
|
||||
|
||||
test("mixed: only the reciprocated peers surface", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assert("alice", "bob"); // reciprocated below
|
||||
reg.assert("bob", "alice");
|
||||
reg.assert("alice", "carol"); // NOT reciprocated by carol
|
||||
reg.assert("dave", "alice"); // dave asserts alice, alice never asserts dave
|
||||
expect([...reg.neighbors("alice")].sort()).toEqual(["bob"]);
|
||||
});
|
||||
|
||||
test("self-assertion and empty are ignored", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assert("alice", "alice");
|
||||
reg.assert("", "bob");
|
||||
reg.assert("alice", "");
|
||||
expect([...reg.neighbors("alice")]).toEqual([]);
|
||||
});
|
||||
|
||||
test("bilateralConnections adapts to the Connections interface", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assertAll([
|
||||
{ from: "alice", to: "bob" },
|
||||
{ from: "bob", to: "alice" },
|
||||
]);
|
||||
const conns = bilateralConnections(reg);
|
||||
expect([...conns.neighbors("alice")]).toEqual(["bob"]);
|
||||
expect([...conns.neighbors("carol")]).toEqual([]);
|
||||
});
|
||||
|
||||
test("clear() removes all assertions", () => {
|
||||
const reg = new ConnectionRegistry();
|
||||
reg.assert("alice", "bob");
|
||||
reg.assert("bob", "alice");
|
||||
reg.clear();
|
||||
expect([...reg.neighbors("alice")]).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user