refactor(api): partager nomme le document, détenir répond par oui ou non
`shareCap(cap, toUser)` faisait tenir une clé à l'appelant. En amont il n'en
tient aucune : c'est le verifier qui remplit `ContactDetails.read_cap`, et une
inbox se résout depuis un profil. Cette signature a déjà changé deux fois
aujourd'hui — `(cap, toInbox)` puis `(cap, toUser)` — et les deux laissaient à
l'app quelque chose qu'elle ne tiendra pas plus tard.
- `inbox.share(doc, toUser)` : les deux choses qu'une application a, un document
et une personne. Ni la clé ni l'adresse n'apparaissent.
- `hasCap(doc)` remplace `capFor(doc)` et rend un BOOLÉEN. C'est la seule
question que le modèle admette, et l'unique appelant qui utilisait la valeur
s'en servait pour la passer à `shareCap`.
Les tests ont fait apparaître un besoin que ces retraits allaient casser :
obtenir le lien PARTAGEABLE d'un document publié, pour le faire circuler. C'est
distinct du partage dirigé et ça existe en amont — un `RepoLinkV0 { read_cap }`
est ce qu'on passe, `ContactDetails.read_cap` est la remise à quelqu'un. D'où
`linkTo(doc)`, seul endroit où une app tient légitimement une clé : on ne peut
pas faire circuler ce qu'on n'a pas le droit de toucher. La clé d'un document
protégé, elle, ne sort jamais par là — elle passe par `share`.
171 tests unitaires, e2e 42/42 en 3,5 min (synchro à froid 29s, stable contre
30s au run précédent — le wallet par batterie tient).
This commit is contained in:
@@ -614,12 +614,12 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
|||||||
```text
|
```text
|
||||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
|
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||||
docs: depositInto, docCreate, sparqlQuery, sparqlUpdate
|
docs: depositInto, docCreate, sparqlQuery, sparqlUpdate
|
||||||
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, shareCap, watch
|
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
|
||||||
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
storeRegistry: createEntityDoc, linkTo, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
||||||
```
|
```
|
||||||
|
|
||||||
### `@ng-eventually/client/polyfill` — `src/polyfill.ts`
|
### `@ng-eventually/client/polyfill` — `src/polyfill.ts`
|
||||||
|
|
||||||
```text
|
```text
|
||||||
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, capFor, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, shareCap
|
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, hasCap, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
type Nuri,
|
type Nuri,
|
||||||
type Scope,
|
type Scope,
|
||||||
} from "@ng-eventually/client";
|
} from "@ng-eventually/client";
|
||||||
import { capFor, configure, configureStoreRegistry, setCurrentUser } from "@ng-eventually/client/polyfill";
|
import { configure, configureStoreRegistry, setCurrentUser } from "@ng-eventually/client/polyfill";
|
||||||
import { ng as realNg, init as realInit } from "@ng-org/web";
|
import { ng as realNg, init as realInit } from "@ng-org/web";
|
||||||
|
|
||||||
// --- the domain, such as it is ---------------------------------------------
|
// --- the domain, such as it is ---------------------------------------------
|
||||||
@@ -135,16 +135,15 @@ async function readSharedNote(link: string): Promise<Note | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hand a reader the key to one of my notes.
|
* Hand a reader access to one of my notes.
|
||||||
*
|
*
|
||||||
* Names the PERSON. Where their inbox is, and whether they have one yet, is the
|
* Names the NOTE and the PERSON — the two things this app has. Neither the key nor the
|
||||||
* library's business — an application will never handle an inbox address once this is
|
* recipient's inbox appears: an application will handle neither once this is native
|
||||||
* native, so it does not handle one now.
|
* (upstream the verifier fills `ContactDetails.read_cap` itself), so it handles neither
|
||||||
|
* now. Refuses if the note is not mine to share.
|
||||||
*/
|
*/
|
||||||
async function shareNote(doc: Nuri, withUser: string): Promise<void> {
|
async function shareNote(doc: Nuri, withUser: string): Promise<void> {
|
||||||
const cap = capFor(doc);
|
await inbox.share(doc, withUser);
|
||||||
if (!cap) throw new Error("this note is not mine to share");
|
|
||||||
await inbox.shareCap(cap, withUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open a note for messages — only its owner can, and only they will read them. */
|
/** Open a note for messages — only its owner can, and only they will read them. */
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import {
|
|||||||
configure,
|
configure,
|
||||||
configureStoreRegistry,
|
configureStoreRegistry,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
capFor,
|
|
||||||
resetCaps,
|
resetCaps,
|
||||||
shareCap,
|
|
||||||
connectedUser,
|
connectedUser,
|
||||||
} from "@ng-eventually/client/polyfill";
|
} from "@ng-eventually/client/polyfill";
|
||||||
import {
|
import {
|
||||||
@@ -873,7 +871,7 @@ const identity = new IdentityStore(
|
|||||||
setCurrentUser(ownerId);
|
setCurrentUser(ownerId);
|
||||||
const doc = await storeRegistry.createEntityDoc(ownerId, "public");
|
const doc = await storeRegistry.createEntityDoc(ownerId, "public");
|
||||||
const ownerInbox = await storeRegistry.openDocumentInbox(doc);
|
const ownerInbox = await storeRegistry.openDocumentInbox(doc);
|
||||||
const link = capFor(doc)!; // the repo link the owner circulates
|
const link = storeRegistry.linkTo(doc); // the repo link the owner circulates
|
||||||
|
|
||||||
setCurrentUser(depositorId);
|
setCurrentUser(depositorId);
|
||||||
getCaps().learn(link);
|
getCaps().learn(link);
|
||||||
@@ -917,13 +915,12 @@ const identity = new IdentityStore(
|
|||||||
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
|
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
|
||||||
injectedSetItems = [{ "@graph": doc, "@id": "1", v: "shared-item" }];
|
injectedSetItems = [{ "@graph": doc, "@id": "1", v: "shared-item" }];
|
||||||
getCaps().open(doc, "protected");
|
getCaps().open(doc, "protected");
|
||||||
const cap = capFor(doc)!;
|
|
||||||
|
|
||||||
setCurrentUser(friendId);
|
setCurrentUser(friendId);
|
||||||
const before = [...(libUseShape(null, null) as Iterable<any>)].length;
|
const before = [...(libUseShape(null, null) as Iterable<any>)].length;
|
||||||
|
|
||||||
setCurrentUser("owner-O");
|
setCurrentUser("owner-O");
|
||||||
await shareCap(cap, friendId);
|
await inbox.share(doc, friendId);
|
||||||
|
|
||||||
setCurrentUser(friendId);
|
setCurrentUser(friendId);
|
||||||
const absorbed = await inbox.read(friendInbox); // processing it applies the cap
|
const absorbed = await inbox.read(friendInbox); // processing it applies the cap
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export {
|
|||||||
getStoreRegistryDeps,
|
getStoreRegistryDeps,
|
||||||
resetStoreRegistry,
|
resetStoreRegistry,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
capFor,
|
hasCap,
|
||||||
resetCaps,
|
resetCaps,
|
||||||
} from "./shared-wallet/bootstrap";
|
} from "./shared-wallet/bootstrap";
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export {
|
|||||||
// lives in `inbox.ts` because sharing IS an inbox deposit (upstream: a sealed
|
// lives in `inbox.ts` because sharing IS an inbox deposit (upstream: a sealed
|
||||||
// message carrying the cap), but it is surfaced here so the cap vocabulary stays
|
// message carrying the cap), but it is surfaced here so the cap vocabulary stays
|
||||||
// on the polyfill side of the boundary rather than in the SDK-identical entry.
|
// on the polyfill side of the boundary rather than in the SDK-identical entry.
|
||||||
export { shareCap } from "./surface/inbox";
|
export { share } from "./surface/inbox";
|
||||||
export { connectedUser } from "./emulated-verifier/connect";
|
export { connectedUser } from "./emulated-verifier/connect";
|
||||||
|
|
||||||
// --- what is deliberately NOT published --------------------------------------
|
// --- what is deliberately NOT published --------------------------------------
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
* The entry now only re-exports; the internals import this module instead.
|
* The entry now only re-exports; the internals import this module instead.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NgLike, UseShapeLike, Nuri, PrincipalId, ReadCap } from "../model/types";
|
import type { NgLike, UseShapeLike, Nuri, NuriLike, PrincipalId, ReadCap } from "../model/types";
|
||||||
import type { SharedWalletConfig } from "./access-gate";
|
import type { SharedWalletConfig } from "./access-gate";
|
||||||
|
import { toNuri } from "../model/nuri";
|
||||||
import type { RegistrySession } from "./account-registry";
|
import type { RegistrySession } from "./account-registry";
|
||||||
import { CapRegistry } from "../emulated-verifier/caps";
|
import { CapRegistry } from "../emulated-verifier/caps";
|
||||||
import { setAccessLog } from "./access-log";
|
import { setAccessLog } from "./access-log";
|
||||||
@@ -207,15 +208,20 @@ export function getCaps(): CapRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do I hold the cap of `nuri`? — the held-caps lookup, the ONLY way a cap is
|
* Do I hold this document's key?
|
||||||
* obtained besides being given one. Returns `undefined` when what I hold has none;
|
|
||||||
* that is the whole answer the model can give (there is no "may P read D?").
|
|
||||||
*
|
*
|
||||||
* Shorthand for `getCaps().capFor(nuri)`, exposed because it is the surface the
|
* The only question the model admits. There is no "may principal P read D" anywhere
|
||||||
* consumer actually uses.
|
* upstream and there cannot be: reading IS key possession, so a cap-introspection API
|
||||||
|
* would have to invent an ACL the engine does not have (`docs/api-contract.md` § 10).
|
||||||
|
*
|
||||||
|
* Returns a BOOLEAN, not the cap. It used to hand the value back, and the only consumer
|
||||||
|
* that used it did so to pass it to `shareCap` — which now takes the document instead.
|
||||||
|
* Nothing an application does requires holding a key: upstream it never sees one, the
|
||||||
|
* verifier fills `ContactDetails.read_cap` itself. So the surface answers the question
|
||||||
|
* and keeps the key.
|
||||||
*/
|
*/
|
||||||
export function capFor(nuri: Nuri): ReadCap | undefined {
|
export function hasCap(nuri: NuriLike): boolean {
|
||||||
return caps.capFor(nuri);
|
return caps.capFor(toNuri(nuri, "hasCap")) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ function capOfPayload(payload: unknown): ReadCap | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share ONE document's read cap with ONE recipient, addressed by their inbox.
|
* Share ONE document with ONE recipient.
|
||||||
*
|
*
|
||||||
* The unit of sharing is the DOCUMENT: never hand over a store's cap, which would
|
* The unit of sharing is the DOCUMENT: never hand over a store's cap, which would
|
||||||
* give away everything the store contains, present and future. The recipient needs
|
* give away everything the store contains, present and future. The recipient needs
|
||||||
@@ -282,18 +282,21 @@ function capOfPayload(payload: unknown): ReadCap | null {
|
|||||||
* The shape is right; the implementation is absent at both ends, so we emulate it
|
* The shape is right; the implementation is absent at both ends, so we emulate it
|
||||||
* meanwhile.
|
* meanwhile.
|
||||||
*/
|
*/
|
||||||
export async function shareCap(cap: ReadCap, toUser: string): Promise<void> {
|
export async function share(doc: NuriLike, toUser: string): Promise<void> {
|
||||||
if (!hasReadCap(cap)) {
|
const target = toNuri(doc, "inbox.share");
|
||||||
|
// Names the DOCUMENT and the PERSON — the two things an application has. Neither the
|
||||||
|
// key nor the address appears, because a caller will handle neither once this is
|
||||||
|
// native: upstream the verifier fills `ContactDetails.read_cap` itself, and an inbox
|
||||||
|
// is resolved from a profile. This took `(cap, toInbox)` at first, then `(cap, toUser)`;
|
||||||
|
// both made the caller hold something it will not hold later.
|
||||||
|
const cap = getCaps().capFor(target);
|
||||||
|
if (!cap) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"[ng-eventually] inbox.shareCap: expected a ReadCap (a NURI carrying `:r:`), " +
|
"[ng-eventually] inbox.share: this document is not yours to share — you hold no cap " +
|
||||||
`got a bare reference — naming is not reading: ${JSON.stringify(cap)}`,
|
`for it. A cap is looked up in what you hold, or it was delivered to you: ${JSON.stringify(target)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Takes the RECIPIENT, not their inbox address. Sharing is an act toward someone;
|
// Protected, because directed sharing is not a public announcement.
|
||||||
// which inbox carries it is the library's business, and an address is exactly what a
|
|
||||||
// caller will not have to handle once this is native. It used to take `toInbox`, which
|
|
||||||
// forced every consumer to resolve an address first — a step it would then have to
|
|
||||||
// unlearn. Protected, because directed sharing is not a public announcement.
|
|
||||||
await post(await userInbox(toUser, "protected"), { payload: { kind: LINK_KIND, cap } });
|
await post(await userInbox(toUser, "protected"), { payload: { kind: LINK_KIND, cap } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,37 @@ export {
|
|||||||
/** Open an inbox on a document you OWN, so others can deposit into it. */
|
/** Open an inbox on a document you OWN, so others can deposit into it. */
|
||||||
/** WHERE to deposit for a document — readable by any holder of it. `undefined` if none. */
|
/** WHERE to deposit for a document — readable by any holder of it. `undefined` if none. */
|
||||||
} from "../shared-wallet/account-registry";
|
} from "../shared-wallet/account-registry";
|
||||||
|
|
||||||
|
import { getCaps } from "../shared-wallet/bootstrap";
|
||||||
|
import { toNuri } from "../model/nuri";
|
||||||
|
import type { NuriLike, ReadCap } from "../model/types";
|
||||||
export { openDocumentInbox } from "../emulated-verifier/branch-registers";
|
export { openDocumentInbox } from "../emulated-verifier/branch-registers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shareable link of a document — what you circulate so someone can open it.
|
||||||
|
*
|
||||||
|
* Distinct from {@link share}, and both are needed: a link is what TRAVELS (a message,
|
||||||
|
* a QR code, a page), whereas sharing hands the access to one named person through
|
||||||
|
* their inbox. Upstream the same split exists — a `RepoLinkV0 { read_cap }` is the
|
||||||
|
* thing you pass around, and `ContactDetails.read_cap` is the directed delivery.
|
||||||
|
*
|
||||||
|
* This is the one place an application legitimately holds a key, because a public
|
||||||
|
* document's link IS meant to be handled: you cannot circulate what you may not touch.
|
||||||
|
* A protected document's key never comes out this way — it goes through `share`.
|
||||||
|
*
|
||||||
|
* Typed `ReadCap`, since that is what it is — a reference with the key inside. A
|
||||||
|
* `ReadCap` is assignable wherever a `Nuri` is expected (a cap IS a NURI carrying the
|
||||||
|
* key, upstream's one `NuriV0`), so it hands straight to any call that takes a
|
||||||
|
* reference. Throws if you hold nothing: a link you cannot open is not a link.
|
||||||
|
*/
|
||||||
|
export function linkTo(doc: NuriLike): ReadCap {
|
||||||
|
const target = toNuri(doc, "linkTo");
|
||||||
|
const cap = getCaps().capFor(target);
|
||||||
|
if (!cap) {
|
||||||
|
throw new Error(
|
||||||
|
"[ng-eventually] linkTo: you hold no key for this document, so there is no link " +
|
||||||
|
`to hand out: ${JSON.stringify(target)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ import {
|
|||||||
resetRegistryCache,
|
resetRegistryCache,
|
||||||
userInbox,
|
userInbox,
|
||||||
} from "../src/shared-wallet/account-registry";
|
} from "../src/shared-wallet/account-registry";
|
||||||
|
import { linkTo } from "../src/surface/placement";
|
||||||
import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers";
|
import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers";
|
||||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,capFor,resetCaps,setCurrentUser,shareCap,connectedUser} from "../src/polyfill";
|
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,hasCap,resetCaps,setCurrentUser,share,connectedUser} from "../src/polyfill";
|
||||||
import { post, postToDocument, read as readInbox } from "../src/surface/inbox";
|
import { post, postToDocument, read as readInbox } from "../src/surface/inbox";
|
||||||
import { readUnion } from "../src/surface/read-model";
|
import { readUnion } from "../src/surface/read-model";
|
||||||
import { sparqlUpdate } from "../src/surface/docs";
|
import { sparqlUpdate } from "../src/surface/docs";
|
||||||
@@ -219,9 +220,8 @@ async function aliceSetsUpHerDocuments() {
|
|||||||
// grants nothing. This is the whole point of the scenario.
|
// grants nothing. This is the whole point of the scenario.
|
||||||
await write(pubDoc, REFERS_TO, protDoc);
|
await write(pubDoc, REFERS_TO, protDoc);
|
||||||
|
|
||||||
const pubLink = capFor(pubDoc)!; // the shareable repo link of the public doc
|
const pubLink = linkTo(pubDoc); // the shareable repo link of the public doc
|
||||||
const protCap = capFor(protDoc)!; // the cap Alice may hand to whoever she chooses
|
return { protDoc, pubDoc, pubLink };
|
||||||
return { protDoc, pubDoc, pubLink, protCap };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Follow the reference found in the public document — what a reader actually does. */
|
/** Follow the reference found in the public document — what a reader actually does. */
|
||||||
@@ -245,19 +245,19 @@ test("Bob: reads the public document, sees the reference, and cannot read throug
|
|||||||
expect(ref).toBe(protDoc); // he can NAME Alice's protected document
|
expect(ref).toBe(protDoc); // he can NAME Alice's protected document
|
||||||
|
|
||||||
// …and that is all it gets him: no cap, no read. Publication is NOT recursive.
|
// …and that is all it gets him: no cap, no read. Publication is NOT recursive.
|
||||||
expect(capFor(ref)).toBeUndefined();
|
expect(hasCap(ref)).toBe(false);
|
||||||
expect(await readValues([ref], SECRET)).toEqual([]);
|
expect(await readValues([ref], SECRET)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Charlie: same public document, same reference — and he reads through it", async () => {
|
test("Charlie: same public document, same reference — and he reads through it", async () => {
|
||||||
inject();
|
inject();
|
||||||
const { protDoc, pubDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
|
const { protDoc, pubDoc, pubLink } = await aliceSetsUpHerDocuments();
|
||||||
const CHARLIE_INBOX = await userInbox("charlie", "protected");
|
const CHARLIE_INBOX = await userInbox("charlie", "protected");
|
||||||
|
|
||||||
// Alice decides Charlie may read that ONE document, and delivers its cap to his
|
// Alice decides Charlie may read that ONE document, and delivers its cap to his
|
||||||
// inbox. She names no principal to the registry; she addresses an inbox.
|
// inbox. She names no principal to the registry; she addresses an inbox.
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await shareCap(protCap, "charlie");
|
await share(protDoc, "charlie");
|
||||||
|
|
||||||
setCurrentUser("charlie");
|
setCurrentUser("charlie");
|
||||||
getCaps().learn(pubLink);
|
getCaps().learn(pubLink);
|
||||||
@@ -265,17 +265,17 @@ test("Charlie: same public document, same reference — and he reads through it"
|
|||||||
|
|
||||||
const ref = referenceFoundIn(await readValues([pubDoc], REFERS_TO));
|
const ref = referenceFoundIn(await readValues([pubDoc], REFERS_TO));
|
||||||
expect(ref).toBe(protDoc);
|
expect(ref).toBe(protDoc);
|
||||||
expect(capFor(ref)).toBe(protCap);
|
expect(hasCap(ref)).toBe(true);
|
||||||
expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]);
|
expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the ONLY difference between Bob and Charlie is each of them holds", async () => {
|
test("the ONLY difference between Bob and Charlie is each of them holds", async () => {
|
||||||
inject();
|
inject();
|
||||||
const { protDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
|
const { protDoc, pubLink } = await aliceSetsUpHerDocuments();
|
||||||
const CHARLIE_INBOX = await userInbox("charlie", "protected");
|
const CHARLIE_INBOX = await userInbox("charlie", "protected");
|
||||||
|
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await shareCap(protCap, "charlie");
|
await share(protDoc, "charlie");
|
||||||
|
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
getCaps().learn(pubLink);
|
getCaps().learn(pubLink);
|
||||||
@@ -294,7 +294,7 @@ test("the ONLY difference between Bob and Charlie is each of them holds", async
|
|||||||
// that was empty becomes full — with nothing re-declared and nobody re-authorized.
|
// that was empty becomes full — with nothing re-declared and nobody re-authorized.
|
||||||
test("dynamic: a cap delivered to Bob's inbox makes the refused document readable, and signals it", async () => {
|
test("dynamic: a cap delivered to Bob's inbox makes the refused document readable, and signals it", async () => {
|
||||||
inject();
|
inject();
|
||||||
const { pubDoc, pubLink, protCap } = await aliceSetsUpHerDocuments();
|
const { protDoc, pubDoc, pubLink } = await aliceSetsUpHerDocuments();
|
||||||
const BOB_INBOX = await userInbox("bob", "protected");
|
const BOB_INBOX = await userInbox("bob", "protected");
|
||||||
|
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
@@ -316,7 +316,7 @@ test("dynamic: a cap delivered to Bob's inbox makes the refused document readabl
|
|||||||
// Alice delivers the cap. Bob's client processes his inbox — the only thing that
|
// Alice delivers the cap. Bob's client processes his inbox — the only thing that
|
||||||
// happens; no "receive" call exists.
|
// happens; no "receive" call exists.
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await shareCap(protCap, "bob");
|
await share(protDoc, "bob");
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
await readInbox(BOB_INBOX);
|
await readInbox(BOB_INBOX);
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ test("dynamic: a cap delivered to Bob's inbox makes the refused document readabl
|
|||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
// …and the read that was empty now yields the content.
|
// …and the read that was empty now yields the content.
|
||||||
expect(capFor(ref)).toBe(protCap);
|
expect(hasCap(ref)).toBe(true);
|
||||||
expect(latest).toEqual(["the-protected-content"]);
|
expect(latest).toEqual(["the-protected-content"]);
|
||||||
expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]);
|
expect(await readValues([ref], SECRET)).toEqual(["the-protected-content"]);
|
||||||
unsub();
|
unsub();
|
||||||
@@ -334,7 +334,7 @@ test("dynamic: a cap delivered to Bob's inbox makes the refused document readabl
|
|||||||
|
|
||||||
test("a bare reference to the PUBLIC document is not enough either — the link is", async () => {
|
test("a bare reference to the PUBLIC document is not enough either — the link is", async () => {
|
||||||
inject();
|
inject();
|
||||||
const { pubDoc, pubLink } = await aliceSetsUpHerDocuments();
|
const { protDoc, pubDoc, pubLink } = await aliceSetsUpHerDocuments();
|
||||||
|
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
// Bob knows the public document's NURI but was never given its link.
|
// Bob knows the public document's NURI but was never given its link.
|
||||||
@@ -350,11 +350,11 @@ test("a bare reference to the PUBLIC document is not enough either — the link
|
|||||||
// Re-reading a queue to recover state is using it as a database.
|
// Re-reading a queue to recover state is using it as a database.
|
||||||
test("a Link is APPLIED durably: the cap survives with the inbox emptied", async () => {
|
test("a Link is APPLIED durably: the cap survives with the inbox emptied", async () => {
|
||||||
const ng = inject();
|
const ng = inject();
|
||||||
const { protDoc, protCap } = await aliceSetsUpHerDocuments();
|
const { protDoc } = await aliceSetsUpHerDocuments();
|
||||||
const bobInbox = await userInbox("bob", "protected");
|
const bobInbox = await userInbox("bob", "protected");
|
||||||
|
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await shareCap(protCap, "bob");
|
await share(protDoc, "bob");
|
||||||
|
|
||||||
// Bob connects: the library restores + drains, with nothing asked of the app.
|
// Bob connects: the library restores + drains, with nothing asked of the app.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
@@ -374,7 +374,7 @@ test("a Link is APPLIED durably: the cap survives with the inbox emptied", async
|
|||||||
|
|
||||||
// Connecting restores it — from the User branch, since the inbox has nothing left.
|
// Connecting restores it — from the User branch, since the inbox has nothing left.
|
||||||
await connectedUser();
|
await connectedUser();
|
||||||
expect(capFor(protDoc)).toBe(protCap);
|
expect(hasCap(protDoc)).toBe(true);
|
||||||
expect(await readValues([protDoc], SECRET)).toEqual(["the-protected-content"]);
|
expect(await readValues([protDoc], SECRET)).toEqual(["the-protected-content"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ test("a document has its own inbox: anyone deposits, only the owner reads", asyn
|
|||||||
const doc = await createEntityDoc("alice", "public");
|
const doc = await createEntityDoc("alice", "public");
|
||||||
const aliceInbox = await openDocumentInbox(doc);
|
const aliceInbox = await openDocumentInbox(doc);
|
||||||
expect(aliceInbox).not.toBe(await userInbox("alice", "protected"));
|
expect(aliceInbox).not.toBe(await userInbox("alice", "protected"));
|
||||||
const link = capFor(doc)!; // the repo link alice circulates — links DO travel
|
const link = linkTo(doc); // the repo link alice circulates — links DO travel
|
||||||
|
|
||||||
// Bob RESOLVES the address himself, from the document. The only thing he is handed
|
// Bob RESOLVES the address himself, from the document. The only thing he is handed
|
||||||
// is the link, which is the one thing the model says circulates. The address is not
|
// is the link, which is the one thing the model says circulates. The address is not
|
||||||
@@ -423,7 +423,7 @@ test("opening an inbox on someone else's document is refused, not silently forke
|
|||||||
const doc = await createEntityDoc("alice", "public");
|
const doc = await createEntityDoc("alice", "public");
|
||||||
const aliceInbox = await openDocumentInbox(doc);
|
const aliceInbox = await openDocumentInbox(doc);
|
||||||
|
|
||||||
const link = capFor(doc)!;
|
const link = linkTo(doc);
|
||||||
|
|
||||||
// Bob holds the document — that is a READ right, and it is not ownership.
|
// Bob holds the document — that is a READ right, and it is not ownership.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
@@ -437,7 +437,7 @@ test("a fresh document has NO inbox — one belongs to one document, and only it
|
|||||||
inject();
|
inject();
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
const doc = await createEntityDoc("alice", "public");
|
const doc = await createEntityDoc("alice", "public");
|
||||||
const link = capFor(doc)!;
|
const link = linkTo(doc);
|
||||||
|
|
||||||
// Not "the owner's inbox by default": upstream an inbox belongs to exactly ONE repo
|
// Not "the owner's inbox by default": upstream an inbox belongs to exactly ONE repo
|
||||||
// (the verifier routes by `inboxes: PubKey → RepoId`), so pointing several documents
|
// (the verifier routes by `inboxes: PubKey → RepoId`), so pointing several documents
|
||||||
@@ -457,7 +457,7 @@ test("opening an inbox publishes ONE address, and re-opening does not accumulate
|
|||||||
const dedicated = await openDocumentInbox(doc);
|
const dedicated = await openDocumentInbox(doc);
|
||||||
expect(await openDocumentInbox(doc)).toBe(dedicated); // idempotent
|
expect(await openDocumentInbox(doc)).toBe(dedicated); // idempotent
|
||||||
|
|
||||||
const link = capFor(doc)!;
|
const link = linkTo(doc);
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
getCaps().learn(link);
|
getCaps().learn(link);
|
||||||
expect(await documentInboxAddress(doc)).toBe(dedicated);
|
expect(await documentInboxAddress(doc)).toBe(dedicated);
|
||||||
@@ -492,14 +492,14 @@ test("connecting drains BOTH levels: the user's inbox and its documents'", async
|
|||||||
// Two deposits, one at each level, both made by someone else.
|
// Two deposits, one at each level, both made by someone else.
|
||||||
setCurrentUser("carol");
|
setCurrentUser("carol");
|
||||||
const carolDoc = await createEntityDoc("carol", "protected");
|
const carolDoc = await createEntityDoc("carol", "protected");
|
||||||
await shareCap(capFor(carolDoc)!, "alice"); // a Link, to alice herself
|
await share(carolDoc, "alice"); // a Link, to alice herself
|
||||||
await post(docInbox, { payload: { onTheDocument: true }, ts: 2 });
|
await post(docInbox, { payload: { onTheDocument: true }, ts: 2 });
|
||||||
|
|
||||||
// Alice connects: one call, both queues.
|
// Alice connects: one call, both queues.
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await connectedUser();
|
await connectedUser();
|
||||||
|
|
||||||
expect(capFor(carolDoc)).toBeDefined(); // the Link was applied
|
expect(hasCap(carolDoc)).toBe(true); // the Link was applied
|
||||||
expect(await readValues([protDoc], SECRET)).toEqual([]); // (protDoc holds no secret here)
|
expect(await readValues([protDoc], SECRET)).toEqual([]); // (protDoc holds no secret here)
|
||||||
const left = await readInbox(docInbox);
|
const left = await readInbox(docInbox);
|
||||||
expect(left.map((d) => d.payload)).toEqual([{ onTheDocument: true }]); // consumer data stays
|
expect(left.map((d) => d.payload)).toEqual([{ onTheDocument: true }]); // consumer data stays
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||||
import { test, expect, mock, afterAll } from "bun:test";
|
import { test, expect, mock, afterAll } from "bun:test";
|
||||||
import { createEntityDoc, resetRegistryCache, userInbox, listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
import { createEntityDoc, resetRegistryCache, userInbox, listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||||
|
import { linkTo } from "../src/surface/placement";
|
||||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||||
import type { ReadCap } from "../src/model/types";
|
import type { ReadCap } from "../src/model/types";
|
||||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,capFor,resetCaps,setCurrentUser,shareCap} from "../src/polyfill";
|
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,hasCap,resetCaps,setCurrentUser,share} from "../src/polyfill";
|
||||||
import { read as readInbox } from "../src/surface/inbox";
|
import { read as readInbox } from "../src/surface/inbox";
|
||||||
import { filterReadable } from "../src/emulated-verifier/read-filter";
|
import { filterReadable } from "../src/emulated-verifier/read-filter";
|
||||||
|
|
||||||
@@ -221,7 +222,7 @@ test("(a) sharing one document's cap to ONE inbox reveals it there, and only the
|
|||||||
// bob's OWN inbox — the only cross-wallet act there is.
|
// bob's OWN inbox — the only cross-wallet act there is.
|
||||||
const bobInbox = await userInbox("bob", "protected");
|
const bobInbox = await userInbox("bob", "protected");
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
await shareCap(capFor(shared)!, "bob");
|
await share(shared, "bob");
|
||||||
|
|
||||||
// bob processes his inbox — no dedicated "receive" operation exists.
|
// bob processes his inbox — no dedicated "receive" operation exists.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
@@ -239,12 +240,12 @@ test("a cap deposit is absorbed, not surfaced as a consumer deposit", async () =
|
|||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
const doc = await createEntityDoc("alice", "protected");
|
const doc = await createEntityDoc("alice", "protected");
|
||||||
const bobInbox = await userInbox("bob", "protected");
|
const bobInbox = await userInbox("bob", "protected");
|
||||||
await shareCap(capFor(doc)!, "bob");
|
await share(doc, "bob");
|
||||||
|
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
const deposits = await readInbox(bobInbox);
|
const deposits = await readInbox(bobInbox);
|
||||||
expect(deposits).toEqual([]); // infrastructure, not consumer data
|
expect(deposits).toEqual([]); // infrastructure, not consumer data
|
||||||
expect(capFor(doc)).toBeDefined(); // …but it landed in bob's held caps
|
expect(hasCap(doc)).toBe(true); // …but it landed in bob's held caps
|
||||||
});
|
});
|
||||||
|
|
||||||
// (b) A bare reference grants nothing; the repo link of a published document does.
|
// (b) A bare reference grants nothing; the repo link of a published document does.
|
||||||
@@ -254,7 +255,7 @@ test("(b) a bare reference reads nothing; the repo link of a published document
|
|||||||
const pub = await createEntityDoc("alice", "public");
|
const pub = await createEntityDoc("alice", "public");
|
||||||
const items = [item(pub, "u1")];
|
const items = [item(pub, "u1")];
|
||||||
expect(getCaps().isPublished(pub)).toBe(true);
|
expect(getCaps().isPublished(pub)).toBe(true);
|
||||||
const link = capFor(pub)!;
|
const link = linkTo(pub);
|
||||||
|
|
||||||
// bob HAS the document's bare NURI (it is right there in `items`) and reads nothing.
|
// bob HAS the document's bare NURI (it is right there in `items`) and reads nothing.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
@@ -270,14 +271,13 @@ test("(c) switching identity switches heldByHolder — a returning identity keep
|
|||||||
inject();
|
inject();
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
const doc = await createEntityDoc("alice", "protected");
|
const doc = await createEntityDoc("alice", "protected");
|
||||||
const cap = capFor(doc);
|
expect(hasCap(doc)).toBe(true);
|
||||||
expect(cap).toBeDefined();
|
|
||||||
|
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
expect(capFor(doc)).toBeUndefined();
|
expect(hasCap(doc)).toBe(false);
|
||||||
|
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
expect(capFor(doc)).toBe(cap!); // durable across the switch — nothing re-declared
|
expect(hasCap(doc)).toBe(true); // durable across the switch — nothing re-declared
|
||||||
});
|
});
|
||||||
|
|
||||||
// A virtual user IS a shim account, and the shim keys accounts through the
|
// A virtual user IS a shim account, and the shim keys accounts through the
|
||||||
@@ -289,18 +289,17 @@ test("one held caps per virtual WALLET, not per spelling of its id", async () =>
|
|||||||
|
|
||||||
setCurrentUser("@Alice");
|
setCurrentUser("@Alice");
|
||||||
const doc = await createEntityDoc("@Alice", "protected");
|
const doc = await createEntityDoc("@Alice", "protected");
|
||||||
const cap = capFor(doc);
|
expect(hasCap(doc)).toBe(true);
|
||||||
expect(cap).toBeDefined();
|
|
||||||
|
|
||||||
// Same account, spelled differently — same shim account, so the same held caps.
|
// Same account, spelled differently — same shim account, so the same held caps.
|
||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
expect(capFor(doc)).toBe(cap!);
|
expect(hasCap(doc)).toBe(true);
|
||||||
setCurrentUser(" ALICE ");
|
setCurrentUser(" ALICE ");
|
||||||
expect(capFor(doc)).toBe(cap!);
|
expect(hasCap(doc)).toBe(true);
|
||||||
|
|
||||||
// A genuinely different account still holds nothing.
|
// A genuinely different account still holds nothing.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
expect(capFor(doc)).toBeUndefined();
|
expect(hasCap(doc)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// THE BREACH P1a OPENED. Caps travel as inbox deposits, so an unguarded inbox read
|
// THE BREACH P1a OPENED. Caps travel as inbox deposits, so an unguarded inbox read
|
||||||
@@ -314,14 +313,14 @@ test("an inbox may be DEPOSITED into by anyone, and READ only by its owner", asy
|
|||||||
const bobInbox = await userInbox("bob", "protected");
|
const bobInbox = await userInbox("bob", "protected");
|
||||||
|
|
||||||
// Alice deposits into bob's inbox — allowed, and it grants her nothing back.
|
// Alice deposits into bob's inbox — allowed, and it grants her nothing back.
|
||||||
await shareCap(capFor(secret)!, "bob");
|
await share(secret, "bob");
|
||||||
await expect(readInbox(bobInbox)).rejects.toThrow(/does not belong to the connected wallet/i);
|
await expect(readInbox(bobInbox)).rejects.toThrow(/does not belong to the connected wallet/i);
|
||||||
expect(capFor(secret)).toBeDefined(); // still hers, obviously
|
expect(hasCap(secret)).toBe(true); // still hers, obviously
|
||||||
|
|
||||||
// Mallory knows the NURI of bob's inbox and tries to pocket what is in it.
|
// Mallory knows the NURI of bob's inbox and tries to pocket what is in it.
|
||||||
setCurrentUser("mallory");
|
setCurrentUser("mallory");
|
||||||
await expect(readInbox(bobInbox)).rejects.toThrow(/does not belong to the connected wallet/i);
|
await expect(readInbox(bobInbox)).rejects.toThrow(/does not belong to the connected wallet/i);
|
||||||
expect(capFor(secret)).toBeUndefined(); // nothing was absorbed
|
expect(hasCap(secret)).toBe(false); // nothing was absorbed
|
||||||
|
|
||||||
// Anonymous owns no inbox at all.
|
// Anonymous owns no inbox at all.
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
@@ -330,7 +329,7 @@ test("an inbox may be DEPOSITED into by anyone, and READ only by its owner", asy
|
|||||||
// Bob reads his own, and only then does the cap land.
|
// Bob reads his own, and only then does the cap land.
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
await readInbox(bobInbox);
|
await readInbox(bobInbox);
|
||||||
expect(capFor(secret)).toBeDefined();
|
expect(hasCap(secret)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a fresh session rebuilds the held caps from the scope index (the emulated AddRepo)", async () => {
|
test("a fresh session rebuilds the held caps from the scope index (the emulated AddRepo)", async () => {
|
||||||
@@ -373,7 +372,7 @@ test("a document's cap is READ from the Store branch, never recomputed", async (
|
|||||||
|
|
||||||
expect(await listMyEntityDocs("alice", "protected")).toEqual([doc]);
|
expect(await listMyEntityDocs("alice", "protected")).toEqual([doc]);
|
||||||
// Recomputing would have produced `:r:OK`; this is what was stored.
|
// Recomputing would have produced `:r:OK`; this is what was stored.
|
||||||
expect(capFor(doc)).toBe(`${doc}:r:FROM-THE-STORE` as ReadCap);
|
expect(getCaps().capFor(doc)).toBe(`${doc}:r:FROM-THE-STORE` as ReadCap);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The listing and the keys are separate upstream (Main vs Store branch), and the
|
// The listing and the keys are separate upstream (Main vs Store branch), and the
|
||||||
@@ -398,5 +397,5 @@ test("creation mints the cap ONCE — the stored value is the one held", async (
|
|||||||
const doc = await createEntityDoc("alice", "protected");
|
const doc = await createEntityDoc("alice", "protected");
|
||||||
|
|
||||||
const stored = ng._quads.find((q) => q.p === "urn:ng-eventually:shim:readCap")!;
|
const stored = ng._quads.find((q) => q.p === "urn:ng-eventually:shim:readCap")!;
|
||||||
expect(capFor(doc)).toBe(stored.o as ReadCap); // same value, not two mints that agree by luck
|
expect(getCaps().capFor(doc)).toBe(stored.o as ReadCap); // same value, not two mints that agree by luck
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ test("readUnion tolerates holes in the list, and refuses a malformed reference",
|
|||||||
// assembling a list from optional values should not have to compact it — while a
|
// assembling a list from optional values should not have to compact it — while a
|
||||||
// non-reference is a caller mistake worth a loud error. Validating before filtering
|
// non-reference is a caller mistake worth a loud error. Validating before filtering
|
||||||
// turned the first into the second.
|
// turned the first into the second.
|
||||||
inject();
|
inject({});
|
||||||
await expect(readUnion(["", null as never, undefined as never])).resolves.toEqual([]);
|
await expect(readUnion(["", null as never, undefined as never])).resolves.toEqual([]);
|
||||||
await expect(readUnion(["not-a-nuri"])).rejects.toThrow(/not a NextGraph reference/i);
|
await expect(readUnion(["not-a-nuri"])).rejects.toThrow(/not a NextGraph reference/i);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user