refactor(api): l'application ne nomme plus son identité — elle l'apprend

Lot C de la revue adverse. Cinq corrections, dont une qui change la forme de la surface.

**L'application ne pouvait pas obtenir son identité par l'API.** `ensureIdentity()`
rendait `void`, `getCurrentUser` n'est plus publié — et pourtant `createEntityDoc(id, …)`
et `listMyEntityDocs(id, …)` l'exigeaient. L'app d'exemple s'en sortait en lisant
`localStorage["ng-eventually:identity"]` et le paramètre `?ng-id`, deux constantes
PRIVÉES du portail d'accès. Une frontière qu'aucun consommateur ne devrait voir, et
encore moins dont il devrait dépendre.

Vérifié au niveau 2 avant de trancher : `session_start(wallet_name, user_id)` prend
l'identité — donc en amont l'application la DÉTIENT, elle la tient du portefeuille
qu'elle a ouvert. Ici c'est le portail qui la choisit, donc c'est au portail de la
rendre. Deux changements, tous deux vers la cible :

- `ensureIdentity()` rend l'identité qu'il a établie ;
- `createEntityDoc(scope)`, `listMyEntityDocs(scope)`, `resolveWriteGraph(scope)`
  perdent leur paramètre d'identité. En amont `doc_create(session_id, …)` ne porte
  aucun utilisateur : une session EST celle d'un utilisateur. Passer la sienne à chaque
  appel de placement était un geste sans successeur.

L'application garde l'identité pour l'afficher, et ne la passe plus à rien.

**`inbox.share` provisionnait un destinataire inexistant.** Une faute de frappe créait
les trois stores et l'inbox de ce nom, et la clé atterrissait où personne ne regarde —
sans la moindre erreur. En amont on ne peut pas viser un nom qu'on invente : un dépôt est
scellé vers une clé d'inbox qui vous est parvenue par un contact entrant. Refuser est
fidèle ; provisionner était l'invention.

**`createEntityDoc` avalait l'échec de ses deux écritures** et rendait quand même une
référence — le document n'était dans aucun store, donc la session suivante ne le listait
pas et sa lecture rendait vide, en silence. Il lève maintenant, comme `doc_create` en
amont propage les siennes.

**Deux entrées prenaient `Nuri` au lieu de `NuriLike`** (`inbox.watch`,
`openDocumentInbox`), ce qui contredisait la raison même pour laquelle aucune garde de
type n'est publiée. Et **deux messages d'erreur nommaient des symboles retirés**
(`storeRegistry.documentInboxAddress`, `setCurrentUser`) : une erreur qui envoie vers une
fonction inexistante est pire qu'une erreur muette.

Contrat d'API et feuille `contract_sdk-surface` mis à jour ; `readForDocument` et le refus
de `share` obtiennent enfin leur règle en §9.

189 tests unitaires, e2e 40/40 et applicatif 12/12.
This commit is contained in:
Sylvain Duchesne
2026-08-10 10:31:15 +02:00
parent b7dc8ca2c3
commit cdc09a1a1d
10 changed files with 193 additions and 91 deletions
@@ -33,7 +33,7 @@ export interface EventuallyConfig {
}
// ── identity — one await before the application renders ──────────────────
export async function ensureIdentity(): Promise<void>;
export async function ensureIdentity(): Promise<PrincipalId>; // returns who you are
// ── addressing ───────────────────────────────────────────────────────────
export type Nuri = `did:ng:${string}`;
@@ -43,12 +43,12 @@ export type Scope = "public" | "protected" | "private";
export type InboxScope = "public" | "protected";
// ── placement: where an application's documents live ─────────────────────
export const storeRegistry: {
createEntityDoc(id: string, scope: Scope): Promise<Nuri>;
listMyEntityDocs(id: string, scope: Scope): Promise<Nuri[]>;
export const storeRegistry: { // no identity parameter — the session is one user's
createEntityDoc(scope: Scope): Promise<Nuri>;
listMyEntityDocs(scope: Scope): Promise<Nuri[]>;
resolveScopeGraph(scope: Scope): Promise<Nuri>;
resolveWriteGraph(id: string, scope: Scope): Promise<Nuri>;
openDocumentInbox(doc: Nuri): Promise<Nuri>;
resolveWriteGraph(scope: Scope): Promise<Nuri>;
openDocumentInbox(doc: NuriLike): Promise<Nuri>;
};
// ── reading ──────────────────────────────────────────────────────────────
@@ -69,13 +69,13 @@ export const docs: {
// ── inbox: giving to read, and depositing ────────────────────────────────
export const inbox: {
share(doc: NuriLike, toUser: string): Promise<void>; // give a reader the key
post(targetInbox: Nuri, opts: PostOptions): Promise<void>;
post(targetInbox: NuriLike, opts: PostOptions): Promise<void>;
postToDocument(doc: NuriLike, opts: PostOptions): Promise<void>;
read(targetInbox: Nuri): Promise<Deposit[]>; // only your own
read(targetInbox: NuriLike): Promise<Deposit[]>; // only your own
readForDocument(doc: NuriLike): Promise<Deposit[]>;
readSynced(targetInbox: Nuri): Promise<Deposit[]>;
processInbox(targetInbox: Nuri): Promise<Deposit[]>;
watch(targetInbox: Nuri, onDeposits: (d: Deposit[]) => void): () => void;
readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
watch(targetInbox: NuriLike, onDeposits: (d: Deposit[]) => void): () => void;
materialize: typeof read;
};
export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number }
@@ -104,7 +104,11 @@ export function initNg(...args: any[]): any;
**An inbox is read only by its owner.** Anyone may deposit; only the owner reads.
**`ensureIdentity()` is the whole of signing in.** It resolves who you are and waits for the connection work (restoring what others shared with you, draining your inboxes). It takes **no identifier**, deliberately — naming your own identity is the part that disappears, so it is not in the signature. After it resolves, what was shared with you is readable.
**`ensureIdentity()` is the whole of signing in, and it tells you who you are.** It settles the identity, waits for the connection work (restoring what others shared with you, draining your inboxes), and **returns the identity**. It takes no identifier — naming your own identity is the part that disappears — but it hands one back, because knowing which user you are is something an application legitimately has upstream too. Keep it for display; **no call takes it**: a session belongs to one user, so placement is named by scope alone.
**Sharing names a person who exists.** `inbox.share(doc, toUser)` refuses a recipient nobody has signed in as, rather than creating them — you cannot address a name you invented.
**A failed creation fails.** `createEntityDoc` throws if the document cannot be recorded in its store, instead of returning a reference that would read empty forever.
**One polyfill-era call.** `configure` is the only published symbol with no counterpart in the target, and therefore the whole of what an application deletes at migration. Everything else is replaced in place by the real SDK.