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:
@@ -15,6 +15,8 @@
|
||||
|
||||
import { getConfig } from "./polyfill";
|
||||
import { logAccess, enabled as accessLogEnabled } from "./access-log";
|
||||
import { isNuri } from "./nuri";
|
||||
import { assertMayReach } from "./reach";
|
||||
import type { Nuri } from "./types";
|
||||
|
||||
// The low common point for ALL document access: every read in the SDK routes
|
||||
@@ -50,6 +52,15 @@ export async function docCreate(
|
||||
): Promise<Nuri> {
|
||||
const { ng } = getConfig();
|
||||
const nuri = await ng.doc_create(sessionId, crdt, cls, dest, store);
|
||||
// The BROKER boundary. `ng` is a permissive property bag (`NgLike`), so what
|
||||
// comes back is `any` and this function's `Promise<Nuri>` would otherwise be an
|
||||
// unchecked promise — every typed NURI downstream rests on it. Validate once,
|
||||
// here, rather than let a non-reference propagate as a document.
|
||||
if (typeof nuri !== "string" || !isNuri(nuri)) {
|
||||
throw new Error(
|
||||
`[ng-eventually] docCreate: the broker returned something that is not a NextGraph reference: ${JSON.stringify(nuri)}`,
|
||||
);
|
||||
}
|
||||
// A container creation is a WRITE; the NURI only exists after the call.
|
||||
logAccess("WRITE", nuri, "docCreate");
|
||||
return nuri;
|
||||
@@ -68,11 +79,38 @@ export async function sparqlUpdate(
|
||||
label = "sparqlUpdate",
|
||||
): Promise<void> {
|
||||
const { ng } = getConfig();
|
||||
// The boundary: a write may only touch what the connected virtual user reaches.
|
||||
if (anchor !== undefined) assertMayReach(anchor, "docs.sparqlUpdate");
|
||||
// `label` is a lib-internal access-log tag, NOT forwarded to `ng`.
|
||||
logAccess("WRITE", anchor ?? "(no anchor)", label);
|
||||
return ng.sparql_update(sessionId, query, anchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit into ANOTHER virtual user's inbox — the one write that legitimately
|
||||
* crosses the boundary, and therefore the one that skips {@link assertMayReach}.
|
||||
*
|
||||
* Why this is a separate primitive rather than a flag: depositing is not "a write
|
||||
* that happens to be allowed", it is a different act. You cannot read the inbox you
|
||||
* deposit into, you hold no cap for it, and you get nothing back — upstream it is an
|
||||
* anonymous sealed box. Naming the exception makes it greppable and keeps
|
||||
* {@link sparqlUpdate} free of a bypass that would otherwise be reusable for
|
||||
* anything.
|
||||
*
|
||||
* The recipient's ownership of the inbox is what bounds this: `inbox.post` is the
|
||||
* only caller, and reading is guarded separately (`inbox.read`).
|
||||
*/
|
||||
export async function depositInto(
|
||||
sessionId: string,
|
||||
query: string,
|
||||
targetInbox: Nuri,
|
||||
label = "deposit",
|
||||
): Promise<void> {
|
||||
const { ng } = getConfig();
|
||||
logAccess("WRITE", targetInbox, label, " (cross-user deposit)");
|
||||
return ng.sparql_update(sessionId, query, targetInbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a SPARQL SELECT/CONSTRUCT/ASK query → the raw SDK result.
|
||||
*
|
||||
@@ -87,6 +125,10 @@ export async function sparqlQuery(
|
||||
label = "sparqlQuery",
|
||||
): Promise<unknown> {
|
||||
const { ng } = getConfig();
|
||||
// The boundary: an ANCHORED read may only touch what the connected virtual user
|
||||
// reaches. An anchorless query spans the local union — a different problem (it is
|
||||
// O(wallet size), and the read path never uses it), not one this guard can bound.
|
||||
if (anchor !== undefined) assertMayReach(anchor, "docs.sparqlQuery");
|
||||
// `label` is a lib-internal access-log tag, NOT forwarded to `ng`.
|
||||
const result = await ng.sparql_query(sessionId, query, base, anchor);
|
||||
// Log AFTER the read so the row count (a strong leak signal: a doc rendering
|
||||
|
||||
Reference in New Issue
Block a user