Align the cap emulation on NextGraph's model, and confine it to a virtual user
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.
This commit is contained in:
+23
-36
@@ -251,33 +251,6 @@ async function main(): Promise<void> {
|
||||
check("post as another principal is rejected; self + anon allowed", r.spoofRejected && r.selfOk && r.anonOk, `spoof=${r.spoofRejected} self=${r.selfOk} anon=${r.anonOk}`);
|
||||
});
|
||||
|
||||
// ── discovery index ─────────────────────────────────────────────────────
|
||||
console.log("\n── discovery index ──");
|
||||
await step("discovery submit → read", async () => {
|
||||
const ref = { doc: "did:ng:o:some-public-doc", title: "t" };
|
||||
const r = await sdk<any>(frame, "discoverySubmitRead", ref);
|
||||
const refs = (r.entries || []).map((e: any) => JSON.stringify(e.ref));
|
||||
check("submitToIndex then readIndex returns the entry", refs.includes(JSON.stringify(ref)), `entries=${r.entries.length}`);
|
||||
});
|
||||
await step("discovery watchIndex fires reactively", async () => {
|
||||
await sdk(frame, "discoveryWatchStart");
|
||||
await frame.waitForFunction(() => (window as any).__sdk.discoveryWatchState().fires >= 1, { timeout: 20000 });
|
||||
const base = await sdkGet<any>(frame, "discoveryWatchState");
|
||||
await sdk(frame, "discoverySubmit", { doc: "did:ng:o:doc2", title: "t2", n: Date.now() });
|
||||
await frame.waitForFunction(
|
||||
(b) => (window as any).__sdk.discoveryWatchState().fires > (b as number),
|
||||
base.fires,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
const after = await sdkGet<any>(frame, "discoveryWatchState");
|
||||
check("watchIndex fires on a new submission", after.fires > base.fires, `fires=${after.fires}`);
|
||||
await sdk(frame, "discoveryWatchStop");
|
||||
});
|
||||
await step("reserved @index account isolation", async () => {
|
||||
const r = await sdk<any>(frame, "discoveryIndexIsolation");
|
||||
check("user '@index' resolves disjoint from the reserved index owner", r.disjoint === true, `disjoint=${r.disjoint}`);
|
||||
});
|
||||
|
||||
// ── store-registry ──────────────────────────────────────────────────────
|
||||
console.log("\n── store-registry ──");
|
||||
await step("ensureAccount idempotent", async () => {
|
||||
@@ -337,17 +310,31 @@ async function main(): Promise<void> {
|
||||
|
||||
// ── caps / read-filter (in-memory cap model) ────────────────────────────
|
||||
console.log("\n── caps / read-filter (in-memory cap model) ──");
|
||||
await step("read-filter: protected hidden from stranger", async () => {
|
||||
await step("read-filter: you read what your keyring holds, nothing else", async () => {
|
||||
const r = await sdk<any>(frame, "capsReadFilter");
|
||||
const ownerSeesProt = r.ownerView.includes("protected-item");
|
||||
const strangerHiddenProt = !r.strangerView.includes("protected-item");
|
||||
const bothSeePublic = r.ownerView.includes("public-item") && r.strangerView.includes("public-item");
|
||||
const bothSeeUngoverned = r.ownerView.includes("ungoverned-item") && r.strangerView.includes("ungoverned-item");
|
||||
check("owner reads protected; stranger does not; public+ungoverned visible to both", ownerSeesProt && strangerHiddenProt && bothSeePublic && bothSeeUngoverned, `owner=${JSON.stringify(r.ownerView)} stranger=${JSON.stringify(r.strangerView)}`);
|
||||
// The owner reads the documents whose caps their keyring holds — and NOT the
|
||||
// one it does not, even though its NURI is right there in the set.
|
||||
const ownerReadsHeld =
|
||||
r.ownerView.includes("protected-item") && r.ownerView.includes("public-item");
|
||||
const ownerMissesUnheld = !r.ownerView.includes("unheld-item");
|
||||
// A stranger holds nothing at all — a bare reference names without reading.
|
||||
const strangerReadsNothing = r.strangerView.length === 0;
|
||||
// …until the repo link of the PUBLISHED document reaches them.
|
||||
const linkOpensPublic =
|
||||
r.strangerWithLinkView.length === 1 && r.strangerWithLinkView.includes("public-item");
|
||||
check(
|
||||
"owner reads held docs only; stranger reads nothing; the repo link opens the published one",
|
||||
ownerReadsHeld && ownerMissesUnheld && strangerReadsNothing && linkOpensPublic,
|
||||
`owner=${JSON.stringify(r.ownerView)} stranger=${JSON.stringify(r.strangerView)} withLink=${JSON.stringify(r.strangerWithLinkView)}`,
|
||||
);
|
||||
});
|
||||
await step("read-filter: directed grant reveals the doc", async () => {
|
||||
const r = await sdk<any>(frame, "capsDirectedGrant");
|
||||
check("grantRead reveals the protected doc to the grantee", r.before === 0 && r.after === 1, `before=${r.before} after=${r.after}`);
|
||||
await step("shareCap: a cap delivered to an inbox reveals the doc", async () => {
|
||||
const r = await sdk<any>(frame, "capsShareCap");
|
||||
check(
|
||||
"shareCap → inbox processed → the shared doc becomes readable, and the delivery is not surfaced",
|
||||
r.before === 0 && r.after === 1 && r.surfacedDeposits === 0,
|
||||
`before=${r.before} after=${r.after} surfaced=${r.surfacedDeposits}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ── accounts (IdentityStore) ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user