Files
ng-eventually/.project/concepts/app-contract/contract_polyfill-surface.md
T
Sylvain Duchesne cc8a95d303 feat: le polyfill possède la session et la normalisation des identités
Pour démarrer, une application devait écrire une promesse autour du callback
d'init(), attraper l'événement loggedin, puis fournir un thunk getSession qui
dépiaute session_id et les trois identifiants de store dans notre forme. Plus un
normalizeId. C'est précisément la plomberie que ce paquet existe pour absorber :
chaque application la réécrirait à l'identique, et c'est elle qui a produit deux
défauts aujourd'hui — un blocage et un partage cassé en silence.

En amont, une session est RENDUE ; une application n'en assemble jamais une à
partir de champs bruts. Et les identités virtuelles sont une invention du
polyfill, donc leur normalisation lui appartient.

Le wrapper init() enveloppe désormais le callback de l'appelant : il capture
l'événement, en dérive la session, puis appelle le callback avec le même
événement. Le paquet n'appelle jamais init de sa propre initiative — il
l'enveloppe. Sans callback, il capture quand même.

getSession et normalizeId quittent la surface publiée. Le chemin d'injection
reste pour les harnais, mais inatteignable depuis l'entrée : vérifié par un
import à l'exécution et par un configure() refusé à la compilation.

Défaut trouvé et corrigé en route : le broker envoie session_id en NOMBRE, et le
convertir en chaîne faisait refuser tous les appels par le binding wasm. La
valeur ne fait que transiter, elle est relayée telle quelle. Reste que toute la
chaîne la type string — inexactitude antérieure à ce commit, à traiter à part.

Une application écrit maintenant : configure({ ng, useShape, init, sharedWallet }).
2026-08-12 17:39:12 +02:00

8.5 KiB

type, summary
type summary
contract 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:

// ── 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
  sharedWallet?: SharedWalletConfig;             // { fileUrl, password, importUrl? }
  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.

The session is the package's, not yours. You never build one, and no call takes one. Call this package's init (not the one you passed to configure): it captures the session the SDK delivers to init's callback and keeps it, then calls your callback with that same event untouched — so an application that wants the session_id for the docs primitives reads it there, and one that does not may pass no callback at all. Identity normalisation is the package's too: @Alice, alice and ALICE are one person.

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.