Files
ng-eventually/packages/client/test/access-gate.test.ts
T
Sylvain Duchesne 54f8389e9e refactor(api): l'app nomme une personne ou un document, jamais une adresse d'inbox
L'app d'exemple a servi de juge, et elle a immédiatement montré ce que
l'inventaire ne montrait pas : pour partager une note elle résolvait l'inbox du
destinataire, pour lire ses messages elle résolvait l'adresse de la sienne. Deux
gestes qu'aucune application n'aura à faire une fois la chose native — donc deux
gestes qu'elle ne doit pas apprendre.

- `shareCap(cap, toUser)` remplace `shareCap(cap, toInbox)`. Partager est un acte
  envers quelqu'un ; où est son inbox regarde la bibliothèque.
- `inbox.readForDocument(doc)` : le propriétaire lit ses messages en nommant la
  note, comme le déposant la nomme pour en laisser un.
- `storeRegistry.userInbox` et `documentInboxAddress` sortent de la surface
  publiée. Ils restent joignables en interne, où le shim en a besoin.

Sortent aussi de `/polyfill`, chacun parce qu'une app qui code contre apprend ce
qu'il faudra désapprendre :

- `getCaps` / `CapRegistry` — la salle des machines. La question du consommateur
  est `capFor(doc)` : est-ce que je le détiens ? Le registre n'a ni successeur ni
  forme inerte ; ce qui s'appuie dessus sera à réécrire, pas à laisser en place.
- `getCurrentUser` — une app sait qui elle a connecté ; le redemander à la
  bibliothèque est une commodité du wallet partagé.
- `virtualUsers` / `IdentityStore` — se souvenir d'une identité entre deux
  sessions est aussi le travail de l'app en amont. L'écran d'accès persiste ce
  dont IL a besoin ; rien d'autre n'a à être exposé.

Reste sur `/polyfill` ce qu'une app appelle vraiment : `configure` et
`setCurrentUser`. Le reste y est du test ou de l'injection interne.

170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la
bibliothèque, l'exemple et le harnais.
2026-08-05 18:55:30 +02:00

119 lines
4.6 KiB
TypeScript

/**
* 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,resetConfig,resetStoreRegistry,setCurrentUser} from "../src/polyfill";
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");
});