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:
@@ -32,7 +32,7 @@ import { subscribeDoc } from "./subscribe";
|
||||
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
||||
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
|
||||
import { addLink, documentInboxAddress, isOwnInbox } from "../emulated-verifier/branch-registers";
|
||||
import { userInbox, isKnownInbox } from "../shared-wallet/account-registry";
|
||||
import { userInbox, isKnownInbox, resolveAccount } from "../shared-wallet/account-registry";
|
||||
import { escapeLiteral } from "./sparql";
|
||||
import { hasReadCap, toNuri } from "../model/nuri";
|
||||
import {
|
||||
@@ -229,8 +229,10 @@ export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promis
|
||||
* nowhere for this to go. Throwing rather than returning quietly is the whole lesson of
|
||||
* this path: a deposit that vanishes without an error is worse than a refusal, and it
|
||||
* is exactly the bug per-document inboxes shipped with
|
||||
* (`docs/briefs/2026-08-03-document-inbox-addressing.md`). Call
|
||||
* `storeRegistry.documentInboxAddress(doc)` first when "no inbox" is an expected case.
|
||||
* (`docs/briefs/2026-08-03-document-inbox-addressing.md`). When "no inbox" is an expected
|
||||
* case for the caller, catch it — there is deliberately no published way to ask an
|
||||
* address in advance, because an application must name a document or a person, never an
|
||||
* inbox.
|
||||
*/
|
||||
export async function postToDocument(docLike: NuriLike, opts: PostOptions): Promise<void> {
|
||||
const doc = toNuri(docLike, "inbox.postToDocument");
|
||||
@@ -337,6 +339,21 @@ export async function share(doc: NuriLike, toUser: string): Promise<void> {
|
||||
// (The private store has no inbox at all — `new_store_default` attaches one only
|
||||
// `if !private`, `verifier.rs:2994` — hence `InboxScope`, which makes "the private
|
||||
// inbox" unwritable rather than merely empty.)
|
||||
// The recipient must EXIST. `userInbox` provisions on first sight, so sharing with a
|
||||
// name nobody has signed in as used to succeed silently: it minted that name's three
|
||||
// stores and an inbox, and the cap landed where nobody will ever look. A mistyped
|
||||
// recipient is the ordinary case, and it produced no error at all.
|
||||
//
|
||||
// Upstream you cannot address a name you invented: a deposit is sealed to an inbox
|
||||
// PUBKEY (`InboxMsg::new`, `engine/net/src/types.rs:4299`) that reached you through an
|
||||
// inbound `ContactDetails` — someone has to have reached you first. Refusing is the
|
||||
// faithful behaviour; provisioning was the invention.
|
||||
if ((await resolveAccount(toUser)) === null) {
|
||||
throw new Error(
|
||||
`[ng-eventually] inbox.share: no such recipient — nobody has signed in as ` +
|
||||
`${JSON.stringify(toUser)}. Sharing does not create the person you share with.`,
|
||||
);
|
||||
}
|
||||
await post(await userInbox(toUser, "protected"), { payload: { kind: LINK_KIND, cap } });
|
||||
}
|
||||
|
||||
@@ -372,7 +389,7 @@ async function assertOwnInbox(targetInbox: Nuri, op: string): Promise<void> {
|
||||
if (getCurrentUser() === null) {
|
||||
throw new Error(
|
||||
`[ng-eventually] inbox.${op}: no identity is set, so no inbox belongs to this ` +
|
||||
"session — call setCurrentUser() first. Depositing (post/share) stays open.",
|
||||
"session — call `ensureIdentity()` first. Depositing (post/share) stays open.",
|
||||
);
|
||||
}
|
||||
if (!(await isOwnInbox(targetInbox))) {
|
||||
@@ -570,10 +587,13 @@ export async function processInbox(targetInboxLike: NuriLike): Promise<Deposit[]
|
||||
* the ORM fan-out hang — see {@link subscribeDoc}.)
|
||||
*/
|
||||
export function watch(
|
||||
targetInbox: Nuri,
|
||||
targetInboxLike: NuriLike,
|
||||
onDeposits: (deposits: Deposit[]) => void,
|
||||
_opts?: { intervalMs?: number },
|
||||
): () => void {
|
||||
// Permissive in, precise out — like every other public entry. It took a bare `Nuri`
|
||||
// until 2026-08-10, which contradicted the very reason no type guard is published.
|
||||
const targetInbox = toNuri(targetInboxLike, "inbox.watch");
|
||||
let stopped = false;
|
||||
let lastCount = -1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user