49b046268e
Récriture complète de `contract_sdk-surface.md` sur le cadrage du propriétaire du projet : **le contrat dit ce que le SDK met à disposition, point.** Ce qui en sort, et pourquoi ça n'y avait pas sa place : - **tout état d'implémentation** — chiffrement, confidentialité, valeur de remplacement, ce qui est émulé, ce qui n'est pas encore fait. Le lecteur est un agent qui développe une application appelante : s'il lit qu'une chose est provisoire, il conçoit des compensations — sa propre couche de chiffrement, un choix de ne pas stocker telle donnée, un avertissement d'interface — toutes fausses et toutes à retirer. Il doit pouvoir considérer que ce SDK **est** celui de NextGraph ; - **la fabrique** — « polyfill », « portefeuille partagé », « multi-utilisateurs », la migration, ce que l'application supprimera un jour, les écarts par rapport à la cible ; - **l'argumentaire** — ce que le modèle « permet », ce que telle règle « achète », la confidentialité composable. Un appelant a besoin de savoir qu'une référence rendue ne porte pas de clé, pas de savoir ce que ça lui apporte. Ce qui entre : les **trois obligations de déploiement**, vérifiées dans le code — servir un `.ngw` depuis son bundle et le passer à `configure`, être ouverte via la redirection du broker, appeler `ensureIdentity()` dans un contexte navigateur avant de rendre — et trois clauses contraignantes qui manquaient : l'identifiant rendu est **opaque**, le préfixe `urn:ng-eventually:` est **réservé sur les sujets**, et le placement recommandé est un document par entité métier, plusieurs objets dans un document restant permis. `## Guarantees` devient une suite d'énoncés plats. `## Non-guarantees` ne liste que des **absences de capacité** — pas de nom d'affichage, pas de révocation, rien par lecteur sur un document en store public, pas d'écriture déléguée — jamais un manque par rapport à autre chose. L'application d'exemple n'affiche plus l'identifiant comme un nom : elle le montre pour ce qu'il est, un identifiant technique. C'était exactement ce que la clause « opaque » interdit, dans le fichier censé montrer le bon geste. 202 tests, typechecks propres, `lint` sans erreur. 148 → 135 lignes.
136 lines
8.1 KiB
Markdown
136 lines
8.1 KiB
Markdown
---
|
|
type: contract
|
|
summary: The API @ng-eventually/sdk exposes to an application — signatures, guaranteed behaviour, and what it does not offer
|
|
---
|
|
|
|
# contract_sdk-surface — `@ng-eventually/sdk`
|
|
|
|
## Scope
|
|
|
|
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 }`;
|
|
- be opened through the broker redirect, `https://nextgraph.net/redir/#/?o=<the app's url>` — outside it there is no session;
|
|
- 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/sdk`. 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;
|
|
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.
|