ae9c32e271
Two batches, verified against nextgraph-rs throughout. P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>), the exact inversion of key possession. It is now possession: `capFor(nuri)` is the only question, there is no principal parameter anywhere, and nothing turns a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link deposit; receiving needs no operation. `Nuri` and `ReadCap` are template literal types, so passing a bare reference where a cap belongs is a compile error, with runtime guards behind it for JavaScript callers. The virtual user boundary. Every access function is now confined to the connected user, through two rules on one criterion (possession), implemented in two places so a lapse in either is caught by the other: authorization at the passage points, and "do not even attempt" at the callers. The polyfill's own machinery moved to physical.ts — unguarded, never exported — which replaced an exemption list: the machinery no longer gets waved through the guard, it calls something the guard never saw. Removed, as emulating capabilities the target does not have: - discovery.ts and its global index. There is no discovery in NextGraph; you follow links. It also pooled user data across wallets. - the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts, loadShim), which was cross-user enumeration by construction. - resolveInboxAnchor, a single inbox common to every user. Caps are now stored where NextGraph stores them, and read back rather than recomputed: AddRepo on the store's Store branch for documents a user creates, AddLink on its User branch for caps received. Inboxes belong to someone — the user's own, plus one per document — and connecting a user drains them all; that is the library's job, not the app's. Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO have a register (AddLink), contrary to what this repo's notes claimed; and "wallet" upstream means keyring — what owns three stores is a user, so the vocabulary follows. The cap value is the constant OK: the only question the emulation answers is whether a cap is held. P1b replaces that one constant with a real key. After this the shape is right and the isolation is still fake. Nothing here may be described as anonymous or private.
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
import { test, expect, mock, beforeEach } from "bun:test";
|
|
import { docCreate, sparqlUpdate, sparqlQuery } from "../src/docs";
|
|
|
|
// The reach guard is process-wide: once ANY cap exists it applies to every reader.
|
|
// This suite declares none, so it must not inherit another suite's enforcement.
|
|
beforeEach(() => {
|
|
resetCaps();
|
|
setCurrentUser(null);
|
|
});
|
|
import * as ngProxy from "../src/ng-proxy";
|
|
|
|
// NOTE ORDER: the "not configured → throw" case MUST run before any configure()
|
|
// call, because configure() sets a module-level singleton with no public reset.
|
|
|
|
test("throws a clear error when configure() was not called", async () => {
|
|
await expect(docCreate("sid", "Graph", "data:graph", "store")).rejects.toThrow(
|
|
/configure\(\) must be called before use/,
|
|
);
|
|
await expect(sparqlUpdate("sid", "INSERT DATA {}")).rejects.toThrow(
|
|
/configure\(\) must be called before use/,
|
|
);
|
|
await expect(sparqlQuery("sid", "SELECT * {}")).rejects.toThrow(
|
|
/configure\(\) must be called before use/,
|
|
);
|
|
});
|
|
|
|
// From here on, a fake real `ng` is injected via configure().
|
|
import { configure, resetCaps, setCurrentUser } from "../src/polyfill";
|
|
|
|
function fakeNg() {
|
|
return {
|
|
doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"),
|
|
sparql_update: mock(async (..._a: unknown[]) => undefined),
|
|
sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })),
|
|
// A sentinel: makeNg(), if ever used, would `.bind` and call THIS through
|
|
// the JS Proxy. We assert the primitives call the raw fns above directly.
|
|
};
|
|
}
|
|
|
|
function inject() {
|
|
const ng = fakeNg();
|
|
configure({ ng: ng as any, useShape: (() => {}) as any });
|
|
return ng;
|
|
}
|
|
|
|
test("docCreate calls the real injected ng.doc_create with the exact args", async () => {
|
|
const ng = inject();
|
|
const nuri = await docCreate("sid-1", "Graph", "data:graph", "store", undefined);
|
|
expect(nuri).toBe("did:ng:o:new-doc");
|
|
expect(ng.doc_create).toHaveBeenCalledTimes(1);
|
|
expect(ng.doc_create.mock.calls[0]).toEqual(["sid-1", "Graph", "data:graph", "store", undefined]);
|
|
});
|
|
|
|
test("sparqlUpdate forwards (sessionId, query, anchor) to the real ng.sparql_update", async () => {
|
|
const ng = inject();
|
|
await sparqlUpdate("sid-2", "INSERT DATA { GRAPH <did:ng:o:a> { <s> <p> <o> } }", "did:ng:o:a");
|
|
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
|
expect(ng.sparql_update.mock.calls[0]).toEqual([
|
|
"sid-2",
|
|
"INSERT DATA { GRAPH <did:ng:o:a> { <s> <p> <o> } }",
|
|
"did:ng:o:a",
|
|
]);
|
|
});
|
|
|
|
test("sparqlUpdate passes anchor=undefined when omitted", async () => {
|
|
const ng = inject();
|
|
await sparqlUpdate("sid-3", "INSERT DATA {}");
|
|
expect(ng.sparql_update.mock.calls[0]).toEqual(["sid-3", "INSERT DATA {}", undefined]);
|
|
});
|
|
|
|
test("sparqlQuery forwards (sessionId, query, base, anchor) and returns the raw result", async () => {
|
|
const ng = inject();
|
|
const res = await sparqlQuery("sid-4", "SELECT ?e { GRAPH <g> { ?s ?p ?e } }", undefined, "did:ng:o:g");
|
|
expect(res).toEqual({ results: { bindings: [] } });
|
|
expect(ng.sparql_query).toHaveBeenCalledTimes(1);
|
|
expect(ng.sparql_query.mock.calls[0]).toEqual([
|
|
"sid-4",
|
|
"SELECT ?e { GRAPH <g> { ?s ?p ?e } }",
|
|
undefined,
|
|
"did:ng:o:g",
|
|
]);
|
|
});
|
|
|
|
test("the primitives do NOT route through the public ng proxy (makeNg)", async () => {
|
|
// makeNg builds a JS Proxy over the injected ng. If a primitive went through
|
|
// it, calls would land on the proxy's `get` trap, not on our raw mock fns.
|
|
// Spy on makeNg: it must never be invoked by the docs primitives.
|
|
const spy = mock(ngProxy.makeNg);
|
|
const ng = inject();
|
|
await docCreate("sid", "Graph", "data:graph", "store");
|
|
await sparqlUpdate("sid", "INSERT DATA {}");
|
|
await sparqlQuery("sid", "SELECT * {}");
|
|
expect(spy).toHaveBeenCalledTimes(0);
|
|
// And the raw injected fns were reached directly:
|
|
expect(ng.doc_create).toHaveBeenCalledTimes(1);
|
|
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
|
expect(ng.sparql_query).toHaveBeenCalledTimes(1);
|
|
});
|