refactor: renommer client → sdk, et fusionner les deux portes en une
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.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* The access gate's identity resolution.
|
||||
*
|
||||
* This is the piece whose failure is SILENT: get the order wrong and the broker iframe
|
||||
* reads an empty identity, provisions a second virtual user, and the returning user
|
||||
* lands in an empty space with no error anywhere. So the order is pinned, not trusted.
|
||||
*/
|
||||
import { getCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { test, expect, afterEach } from "bun:test";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { ensureIdentity } from "../src/shared-wallet/access-gate";
|
||||
|
||||
const KEY = "ng-eventually:identity";
|
||||
|
||||
/** A localStorage double — the real one is absent in `bun test`. */
|
||||
function fakeStorage(initial: Record<string, string> = {}) {
|
||||
const map = new Map(Object.entries(initial));
|
||||
return {
|
||||
getItem: (k: string) => map.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => void map.set(k, v),
|
||||
removeItem: (k: string) => void map.delete(k),
|
||||
get size() { return map.size; },
|
||||
};
|
||||
}
|
||||
|
||||
/** Put the page in a given URL + storage state, as the browser would. */
|
||||
function inPage(search: string, storage: ReturnType<typeof fakeStorage>) {
|
||||
(globalThis as any).location = { search, href: "https://app.example" + search };
|
||||
(globalThis as any).localStorage = storage;
|
||||
(globalThis as any).history = { replaceState: () => {} };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setCurrentUser(null);
|
||||
resetConfig();
|
||||
resetStoreRegistry();
|
||||
delete (globalThis as any).location;
|
||||
delete (globalThis as any).localStorage;
|
||||
delete (globalThis as any).history;
|
||||
});
|
||||
|
||||
function configured() {
|
||||
configureStoreRegistry({
|
||||
getSession: async () => ({ sessionId: "s", privateStoreId: "did:ng:o:p" }),
|
||||
normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(),
|
||||
});
|
||||
configure({
|
||||
ng: {} as never,
|
||||
useShape: (() => {}) as never,
|
||||
sharedWallet: { fileUrl: "/w.ngw", password: "pw" },
|
||||
});
|
||||
}
|
||||
|
||||
test("an identity already set is left alone — the gate never re-asks", async () => {
|
||||
configured();
|
||||
inPage("", fakeStorage());
|
||||
setCurrentUser("alice");
|
||||
await ensureIdentity();
|
||||
expect(getCurrentUser()).toBe("alice");
|
||||
});
|
||||
|
||||
test("the URL parameter WINS over storage — it is the only thing that crosses the frontier", async () => {
|
||||
// The top-level page and the broker iframe have separate localStorage partitions, so a
|
||||
// value written on one side is not the value the other reads. The URL survives the
|
||||
// round-trip; storage does not. If storage won here, a user entering a second
|
||||
// identifier would keep being sent back to the first one's space.
|
||||
configured();
|
||||
inPage("?ng-id=fromurl", fakeStorage({ [KEY]: "fromstorage" }));
|
||||
await ensureIdentity();
|
||||
expect(getCurrentUser()).toBe("fromurl");
|
||||
});
|
||||
|
||||
test("the URL parameter is copied into THIS partition, so a plain reload still knows", async () => {
|
||||
configured();
|
||||
const storage = fakeStorage();
|
||||
inPage("?ng-id=carol", storage);
|
||||
await ensureIdentity();
|
||||
expect(storage.getItem(KEY)).toBe("carol");
|
||||
});
|
||||
|
||||
test("with no parameter, storage answers — a reload does not re-ask", async () => {
|
||||
configured();
|
||||
inPage("", fakeStorage({ [KEY]: "dana" }));
|
||||
await ensureIdentity();
|
||||
expect(getCurrentUser()).toBe("dana");
|
||||
});
|
||||
|
||||
test("nothing known and no DOM to ask on → it refuses loudly", async () => {
|
||||
// Continuing silently would provision an anonymous virtual space, which is the failure
|
||||
// this module exists to prevent. The error names what the caller must do.
|
||||
configured();
|
||||
inPage("", fakeStorage());
|
||||
await expect(ensureIdentity()).rejects.toThrow(/no DOM to ask on/i);
|
||||
});
|
||||
|
||||
test("no shared wallet configured → it refuses, rather than inventing a space", async () => {
|
||||
configure({ ng: {} as never, useShape: (() => {}) as never });
|
||||
inPage("", fakeStorage());
|
||||
await expect(ensureIdentity()).rejects.toThrow(/no shared wallet configured/i);
|
||||
});
|
||||
|
||||
test("the URL value is NORMALIZED on the way in — `@Erin` and `erin` are one space", async () => {
|
||||
// Ported from the consumer's `identifiant-resolution` feature, and it caught a real
|
||||
// defect here: the gate normalized what a user TYPED but not what the URL carried, so
|
||||
// a link with `?ng-id=@Erin` keyed onto a different virtual user than the same person
|
||||
// typing `erin`. One normalizer — the injected one — for all three entry paths.
|
||||
configured();
|
||||
inPage("?ng-id=@Erin", fakeStorage());
|
||||
await ensureIdentity();
|
||||
expect(getCurrentUser()).toBe("erin");
|
||||
});
|
||||
|
||||
test("a stored value is normalized too — an old entry cannot key onto a second space", async () => {
|
||||
configured();
|
||||
inPage("", fakeStorage({ [KEY]: "@Frank" }));
|
||||
await ensureIdentity();
|
||||
expect(getCurrentUser()).toBe("frank");
|
||||
});
|
||||
Reference in New Issue
Block a user