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:
@@ -134,6 +134,7 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
test("post writes via the real injected ng.sparql_update (not makeNg), scoped to the inbox", async () => {
|
||||
setCurrentUser("alice"); // `from` is bound to the current identity
|
||||
await post(TARGET, { from: "alice", payload: { kind: "join" }, ts: 100 });
|
||||
expect(fake.sparql_update).toHaveBeenCalledTimes(1);
|
||||
const call = fake.sparql_update.mock.calls[0]!;
|
||||
@@ -143,12 +144,29 @@ test("post writes via the real injected ng.sparql_update (not makeNg), scoped to
|
||||
});
|
||||
|
||||
test("post → read round-trips payload, from and ts", async () => {
|
||||
setCurrentUser("alice"); // `from` is bound to the current identity
|
||||
await post(TARGET, { from: "alice", payload: { kind: "join", n: 3 }, ts: 100 });
|
||||
const deposits = await read(TARGET);
|
||||
expect(deposits).toHaveLength(1);
|
||||
expect(deposits[0]).toEqual({ from: "alice", payload: { kind: "join", n: 3 }, ts: 100 });
|
||||
});
|
||||
|
||||
// (c) `from` is BOUND to the current identity — a spoof (naming another
|
||||
// principal) is REJECTED; identifying as self or anonymous (null) is allowed.
|
||||
test("(c) post rejects a spoofed `from` (naming another principal); self/null allowed", async () => {
|
||||
setCurrentUser("alice");
|
||||
// SPOOF: alice tries to deposit AS bob → rejected.
|
||||
await expect(post(TARGET, { from: "bob", payload: { x: 1 }, ts: 1 })).rejects.toThrow(
|
||||
/spoof|current identity/i,
|
||||
);
|
||||
// Identifying as self → allowed.
|
||||
await post(TARGET, { from: "alice", payload: { x: 2 }, ts: 2 });
|
||||
// Explicit anonymous → allowed.
|
||||
await post(TARGET, { from: null, payload: { x: 3 }, ts: 3 });
|
||||
const froms = (await read(TARGET)).map((d) => d.from);
|
||||
expect(froms).toEqual(["alice", null]);
|
||||
});
|
||||
|
||||
test("from is optional — omitting it defaults to the current user", async () => {
|
||||
setCurrentUser("bob");
|
||||
await post(TARGET, { payload: { hi: 1 }, ts: 200 });
|
||||
|
||||
Reference in New Issue
Block a user