0eb25286c8
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
147 lines
5.5 KiB
TypeScript
147 lines
5.5 KiB
TypeScript
/**
|
|
* public-store.test.ts — the emulated *"downloaded from the outerOverlay"*, in isolation.
|
|
*
|
|
* `cross-user-access.test.ts` proves the consequence end to end (Bob reads Alice's
|
|
* public document from a bare reference). This file pins the primitive itself: what it
|
|
* asks, what it refuses, and when it says nothing at all.
|
|
*/
|
|
import { test, expect, mock, afterEach } from "bun:test";
|
|
import { exposeReadCap, fetchReadCap, resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
|
|
import { mintCap } from "../src/emulated-verifier/caps";
|
|
import { getCaps } from "../src/shared-wallet/bootstrap";
|
|
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
|
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import type { Nuri } from "../src/model/types";
|
|
|
|
const SHIM = "urn:ng-eventually:shim";
|
|
const SESSION = { sessionId: "sid-ps", privateStoreId: "PRIV-PS" };
|
|
|
|
interface Quad { g: string; s: string; p: string; o: string }
|
|
|
|
/** A fake `ng` holding just enough to answer the Header-branch `exposedReadCap` query. */
|
|
function inject() {
|
|
const quads: Quad[] = [];
|
|
const sparql_update = mock(async (...a: unknown[]) => {
|
|
const query = a[1] as string;
|
|
const anchor = a[2] as string;
|
|
if (/^\s*DELETE WHERE/.test(query)) {
|
|
for (let i = quads.length - 1; i >= 0; i--) if (quads[i]!.g === anchor) quads.splice(i, 1);
|
|
return undefined;
|
|
}
|
|
const m = query.match(/<([^>]+)>\s+<([^>]+)>\s+"([^"]*)"/);
|
|
if (m) quads.push({ g: anchor, s: m[1]!, p: m[2]!, o: m[3]! });
|
|
return undefined;
|
|
});
|
|
const sparql_query = mock(async (...a: unknown[]) => ({
|
|
results: {
|
|
bindings: quads
|
|
.filter((q) => q.g === (a[3] as string) && q.p === `${SHIM}:exposedReadCap`)
|
|
.map((q) => ({ c: { value: q.o } })),
|
|
},
|
|
}));
|
|
configure({ ng: { doc_create: mock(async () => "did:ng:o:x"), sparql_update, sparql_query } as any, useShape: (() => {}) as any });
|
|
configureStoreRegistry({ getSession: async () => SESSION });
|
|
resetCaps();
|
|
resetPublicStoreFetches();
|
|
setCurrentUser(null);
|
|
return { sparql_query, quads };
|
|
}
|
|
|
|
afterEach(() => {
|
|
resetConfig();
|
|
resetStoreRegistry();
|
|
resetCaps();
|
|
setCurrentUser(null);
|
|
});
|
|
|
|
/** Arm the emulation without giving the current holder anything: some OTHER document. */
|
|
function armEmulation(): void {
|
|
setCurrentUser("someone-else");
|
|
getCaps().mint("did:ng:o:unrelated");
|
|
}
|
|
|
|
const PUB = "did:ng:o:pub" as Nuri;
|
|
|
|
test("a cap exposed on a document is downloaded by a holder that has nothing", async () => {
|
|
inject();
|
|
setCurrentUser("alice");
|
|
await exposeReadCap(PUB, mintCap(PUB));
|
|
|
|
setCurrentUser("bob");
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
expect(getCaps().capFor(PUB)).toBeUndefined();
|
|
|
|
expect(await fetchReadCap(PUB)).toBe(true);
|
|
expect(getCaps().capFor(PUB)).toBe(mintCap(PUB));
|
|
// …and what he got is a READ grant, recorded as such.
|
|
expect(getCaps().isReadOnlyPublicCap(PUB)).toBe(true);
|
|
expect(getCaps().isInPublicStore(PUB)).toBe(true);
|
|
});
|
|
|
|
test("a document that exposes nothing yields nothing — that is the normal case, not an error", async () => {
|
|
inject();
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
expect(await fetchReadCap("did:ng:o:protected" as Nuri)).toBe(false);
|
|
expect(getCaps().capFor("did:ng:o:protected" as Nuri)).toBeUndefined();
|
|
});
|
|
|
|
// A document speaks for itself and for nothing else. Without this, whoever can write
|
|
// into one public document could file caps for every document they care to name.
|
|
test("a cap naming ANOTHER document is refused, not filed", async () => {
|
|
const { quads } = inject();
|
|
setCurrentUser("alice");
|
|
await exposeReadCap(PUB, mintCap(PUB));
|
|
// Forge the exposed value so it names a different document.
|
|
quads[0]!.o = mintCap("did:ng:o:someone-elses" as Nuri);
|
|
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
expect(await fetchReadCap(PUB)).toBe(false);
|
|
expect(getCaps().capFor(PUB)).toBeUndefined();
|
|
expect(getCaps().capFor("did:ng:o:someone-elses" as Nuri)).toBeUndefined();
|
|
});
|
|
|
|
test("inert while no cap has been issued at all — nothing to obtain, nothing asked", async () => {
|
|
const { sparql_query } = inject();
|
|
setCurrentUser("bob");
|
|
expect(await fetchReadCap(PUB)).toBe(false);
|
|
expect(sparql_query).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("asked once per document: the outcome is memoised, in both directions", async () => {
|
|
const { sparql_query } = inject();
|
|
setCurrentUser("alice");
|
|
await exposeReadCap(PUB, mintCap(PUB));
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
|
|
await fetchReadCap(PUB);
|
|
const afterHit = sparql_query.mock.calls.length;
|
|
await fetchReadCap(PUB); // held now → not even the memo is consulted
|
|
expect(sparql_query.mock.calls.length).toBe(afterHit);
|
|
|
|
const absent = "did:ng:o:nothing-here" as Nuri;
|
|
await fetchReadCap(absent);
|
|
const afterMiss = sparql_query.mock.calls.length;
|
|
await fetchReadCap(absent); // a miss is remembered too
|
|
expect(sparql_query.mock.calls.length).toBe(afterMiss);
|
|
});
|
|
|
|
test("resetting the caps forgets the memo — a stale yes would hand back what is no longer held", async () => {
|
|
const { sparql_query } = inject();
|
|
setCurrentUser("alice");
|
|
await exposeReadCap(PUB, mintCap(PUB));
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
await fetchReadCap(PUB);
|
|
|
|
resetCaps(); // also calls resetPublicStoreFetches
|
|
armEmulation();
|
|
setCurrentUser("bob");
|
|
const before = sparql_query.mock.calls.length;
|
|
expect(await fetchReadCap(PUB)).toBe(true);
|
|
expect(sparql_query.mock.calls.length).toBeGreaterThan(before); // asked again
|
|
});
|