c5b4703687
Aucun parcours n'avait jamais marché le chemin d'un nouvel arrivant : tous pré-injectaient l'identifiant dans l'URL, ce qui fait résoudre l'identité sans jamais afficher la barrière. La suite était verte par-dessus un lien de téléchargement pointant sur un fichier que personne ne servait — et le serveur de test répondait la page HTML de l'application pour tout chemin inconnu, donc un fichier manquant ne POUVAIT pas échouer. Le nouveau parcours part d'un profil vide : barrière, téléchargement réel, import dans l'application portefeuille, saisie de l'identifiant, remise au broker, retour dans l'iframe. Il vérifie neuf points, dont celui qui compte — l'identifiant a survécu et l'identité rapportée est celle qui a été saisie. Le harnais, lui, se pendait au lieu d'échouer. Cause observée : le tuyau devtools de Chromium lâche et Playwright n'émet ni close ni disconnected, si bien que la suite bloquait dans son propre nettoyage sans imprimer ni résumé ni l'échec déjà en route. Toutes les attentes sont désormais bornées et nomment ce qu'elles attendaient ; vérifié en cassant délibérément une attente, et observé en conditions réelles — trois minutes et « gave up waiting for: alice to sign in » là où j'ai tué trois exécutions d'une heure ce matin. Deux exécutions simultanées ne se détruisent plus : verrou atomique sur le profil, et récupération d'un navigateur laissé par une exécution tuée. Le marqueur devient .user-consumed — il n'a jamais attesté d'une disponibilité, seulement qu'un lot avait déjà pris l'utilisateur de ce profil. Au passage, la destruction du profil dépendait du marqueur, écrit en FIN de lot : une exécution tuée avant laissait un profil que la suivante réutilisait, et héritait de sa casse. Elle dépend maintenant du profil. La suite applicative reste non mesurée sur cette machine : un conteneur en boucle de redémarrage recycle son interface réseau, et sept exécutions sur dix échouent sur le transport. Trois sont passées 21/21.
137 lines
8.1 KiB
Markdown
137 lines
8.1 KiB
Markdown
---
|
|
type: contract
|
|
summary: The API @ng-eventually/polyfill exposes to an application — signatures, guaranteed behaviour, and what it does not offer
|
|
---
|
|
|
|
# contract_polyfill-surface — `@ng-eventually/polyfill`
|
|
|
|
## Scope
|
|
|
|
This package is a polyfill of NextGraph's SDK.
|
|
|
|
This package covers placement (creating and listing an application's documents by scope), reading (a document's subjects, one-shot or reactive), sharing a document with a named user, and depositing into inboxes. It does not cover user management, display names, transport, or the operation of a deployment.
|
|
|
|
### Deployment requirements
|
|
|
|
An application using this package must:
|
|
|
|
- serve a wallet file (`.ngw`) from its own bundle, and pass its URL and password to `configure` as `sharedWallet: { fileUrl, password }`;
|
|
- call `ensureIdentity()` in a browser context before rendering its interface; it mounts a barrier in the document.
|
|
|
|
## Surface
|
|
|
|
Full typed shape: the package's `types` entry, `@ng-eventually/polyfill`. A type is published only when a published signature uses it. The load-bearing signatures:
|
|
|
|
```ts
|
|
// ── bootstrap ────────────────────────────────────────────────────────────
|
|
export function configure(c: EventuallyConfig): void;
|
|
export interface EventuallyConfig {
|
|
ng: NgLike; // the `ng` object from @ng-org/web
|
|
useShape: UseShapeLike; // `useShape` from @ng-org/orm
|
|
getSession?: () => Promise<RegistrySession>; // resolve the session (a thunk)
|
|
normalizeId?: (id: string) => string;
|
|
sharedWallet?: SharedWalletConfig; // { fileUrl, password, importUrl? }
|
|
currentUser?: PrincipalId;
|
|
debugAccessLog?: boolean;
|
|
init?: (...args: any[]) => any;
|
|
initNg?: (...args: any[]) => any;
|
|
}
|
|
|
|
// ── identity — one await before the application renders ──────────────────
|
|
export async function ensureIdentity(): Promise<PrincipalId>; // returns who you are
|
|
|
|
// ── addressing ───────────────────────────────────────────────────────────
|
|
export type Nuri = `did:ng:${string}`;
|
|
export type NuriLike = Nuri | string;
|
|
export type Scope = "public" | "protected" | "private";
|
|
|
|
// ── placement: where an application's documents live ─────────────────────
|
|
export const storeRegistry: { // no identity parameter — a session is one user's
|
|
createEntityDoc(scope: Scope): Promise<Nuri>;
|
|
listMyEntityDocs(scope: Scope): Promise<Nuri[]>;
|
|
resolveScopeGraph(scope: Scope): Promise<Nuri>;
|
|
resolveWriteGraph(scope: Scope): Promise<Nuri>;
|
|
openDocumentInbox(doc: NuriLike): Promise<Nuri>;
|
|
};
|
|
|
|
// ── reading ──────────────────────────────────────────────────────────────
|
|
export async function readUnion(docs: NuriLike[]): Promise<UnionSubject[]>;
|
|
export interface UnionSubject { subject: string; graph: Nuri; props: Record<string, string[]> }
|
|
export function useShape(shapeType: unknown, scope: unknown): unknown; // read-filtered view
|
|
export function watchShape(query: ShapeQuery): ShapeObservable;
|
|
export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
|
|
export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
|
|
|
|
// ── low-level document / SPARQL primitives ───────────────────────────────
|
|
export const docs: {
|
|
docCreate(sessionId: string, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
|
|
sparqlQuery(sessionId: string, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
|
sparqlUpdate(sessionId: string, query: string, anchor?: NuriLike, label?: string): Promise<void>;
|
|
};
|
|
|
|
// ── inbox: giving to read, and depositing ────────────────────────────────
|
|
export const inbox: {
|
|
share(doc: NuriLike, toUser: string): Promise<void>; // give a reader the key
|
|
post(targetInbox: NuriLike, opts: PostOptions): Promise<void>;
|
|
postToDocument(doc: NuriLike, opts: PostOptions): Promise<void>;
|
|
read(targetInbox: NuriLike): Promise<Deposit[]>; // only your own
|
|
readForDocument(doc: NuriLike): Promise<Deposit[]>;
|
|
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 }
|
|
|
|
// ── the wrapped SDK objects ──────────────────────────────────────────────
|
|
export const ng: Record<string, any>; // call this instead of the `ng` passed to `configure`
|
|
export function init(...args: any[]): any; // likewise — not the `init` passed to `configure`
|
|
export function initNg(...args: any[]): any;
|
|
```
|
|
|
|
## Guarantees
|
|
|
|
Every entry accepts `NuriLike` and validates at the door; what it returns is a precise `Nuri`. No type guard is published.
|
|
|
|
A returned reference carries no key — not `createEntityDoc`, not `listMyEntityDocs`, not `UnionSubject.subject` / `.graph`. A reference found inside a document yields a name, not a key.
|
|
|
|
You read a document whose key you hold: you created it, it was shared with you, or it sits in a public store, which serves its read key to whoever asks. No call answers "may I read this?".
|
|
|
|
What was shared with you becomes readable after `ensureIdentity()`.
|
|
|
|
`readUnion` returns one entry per distinct subject present in a document. `subject` is that subject's IRI exactly as written, and is a `string`, because a subject may be any IRI; `graph` is the document reference you passed in, and is the `Nuri` to hand back to this surface. Properties of different subjects are never merged, and the same subject IRI found in two documents stays two entries, told apart by `graph`. Several objects in one document are allowed. Recommended placement is one document per business entity: access is granted per document.
|
|
|
|
`urn:ng-eventually:` is reserved. Triples whose **subject** falls under that prefix are dropped on read and never returned by `readUnion`; every other IRI is returned.
|
|
|
|
Only a document's owner writes to it. Holding its read key never grants a write.
|
|
|
|
`inbox.share(doc, toUser)` names the document and the person; the recipient calls nothing. It refuses a recipient nobody has signed in as, rather than creating them.
|
|
|
|
`inbox.post` refuses a target that is not an inbox; to reach a document's owner, use `inbox.postToDocument(doc, …)`. Anyone may deposit into an inbox; only its owner reads it.
|
|
|
|
`ensureIdentity()` settles the identity, completes the connection work it starts, and returns the identity. It takes no identifier, and no other call takes one.
|
|
|
|
`createEntityDoc` throws if the document cannot be recorded in its store.
|
|
|
|
## Non-guarantees
|
|
|
|
**No display name.** `ensureIdentity()` returns an opaque identifier: do not parse it, split it, or render it as a readable name.
|
|
|
|
**No revocation.** `inbox.share` cannot be undone.
|
|
|
|
**Nothing per reader on a document in a public store.** No grant, no revocation, no audience list.
|
|
|
|
**No delegated writing.** A received key never grants a write, and no call adds a writer to a document.
|
|
|
|
**No mailbox model.** Do not build on the raw deposit list.
|
|
|
|
**No cross-broker reference.** A returned reference resolves for users of the same broker.
|
|
|
|
**No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear.
|
|
|
|
## Change policy
|
|
|
|
This surface changes, and shrinks. The package does not offer semantic-version stability.
|
|
|
|
Re-pull this contract at every upgrade.
|