refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph

Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont
ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever
l'ambiguïté à chaque phrase — et le contrat publié, lu par une application,
était le pire endroit pour laisser traîner ça.

packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill,
contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts →
e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md.

Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes,
y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est
fait occurrence par occurrence, pas par substitution.

Le contrat énonce désormais son identité en une phrase : « This package is a
polyfill of NextGraph's SDK. »
This commit is contained in:
Sylvain Duchesne
2026-08-10 17:14:25 +02:00
parent 49b046268e
commit 737729c9ce
88 changed files with 122 additions and 106 deletions
+121
View File
@@ -0,0 +1,121 @@
/**
* NURI primitives — the cap-less / cap-bearing distinction, kept as ONE object.
*
* Upstream a NURI is a single type, `NuriV0` — TEN fields: `identity, target,
* entire_store, objects, signature, branch, overlay, access, topic, locator`
* (`engine/net/src/app_protocol.rs:181-194`) — and a cap-less NURI is simply one
* whose `access` is empty. This module transcribes **two** of those ten (`target`,
* and the cap half of `access`); the other eight have no counterpart here.
* `did:ng:` is the URI SCHEME prefix (inboxes, branches and overlays all carry it) —
* it does NOT mean "without cap". The discriminant is the `:r:` segment:
*
* did:ng:o:{doc}:v:{overlay} — names, does NOT read (a {@link Nuri})
* did:ng:o:{doc}:v:{overlay}:r:{cap} — names AND reads (a {@link ReadCap})
*
* ── Why `:r:` and not `:k:` ────────────────────────────────────────────────
* Reported by NextGraph's developer and verified in the source: a **ReadCap** is
* `r:{base64url(serde_bare(ObjectRef))}` — `BlockRef::readcap_nuri()`,
* `engine/repo/src/types.rs:518-521` — where id AND key are serialized together
* into ONE opaque segment. The `:k:` forms are a different thing: they belong to
* **objects, files and commits** (`j:{id}:k:{key}`, `c:{id}:k:{key}`, `:510`/`:514`),
* where id and key are two separate segments. This library used `:k:` until
* 2026-07-30; it was the wrong letter *and* the wrong structure.
*
* These helpers are INTERNAL to the library. The parsed form {@link parseNuri}
* mirrors that PAIR — `target` and the cap — and not the type: it was described as a
* "1:1 mirror of `NuriV0`" until 2026-08-10, which claimed eight fields it has never
* carried. It never surfaces in the SDK-identical entry's signatures either — the
* real SDK takes plain `String`s and enforces at runtime, through cryptography, so no
* branded type and no parsed struct leaks outward.
*
* ── The stand-in key (deliberately NOT a secret) ───────────────────────────
* This library is deliberately insecure (see docs/vision.md). The only question it
* can answer is **do I hold this document's cap, or not** — so the key value is the
* constant `OK`, which says exactly that and pretends nothing more. What identifies
* the document is the NURI the key is attached to; the value carries no information.
* Real per-document encryption is P1b's job, and it replaces this one constant.
* Until then, possession is a SHAPE, not a protection.
*/
import type { Nuri, ReadCap } from "./types";
/** The URI scheme prefix every NextGraph reference carries. */
const SCHEME = "did:ng:";
/** The segment that turns a naming NURI into a reading one — upstream's ReadCap
* encoding (`readcap_nuri`), NOT the `:k:` used for objects/files/commits. */
/**
* The ReadCap discriminant. Exported because the emulated verifier mints with it
* (`emulated-verifier/caps.ts`); the model owns the grammar, minting is not part of it.
*/
export const CAP_SEGMENT = ":r:";
/**
* Is this string a NextGraph reference at all? A **type guard**: it is the door
* through which an untrusted `string` — a SPARQL binding, an ORM `@graph`, a value
* an app read back from storage or a URL — becomes a {@link Nuri}. Exported from
* the SDK entry so a consumer narrows its own strings the same way, rather than
* casting.
*/
export function isNuri(s: string): s is Nuri {
return s.startsWith(SCHEME);
}
/**
* Does this reference carry a read cap (a `:r:` segment)? A **type guard**: the
* ONLY narrowing from a bare string (or a {@link Nuri}) to a {@link ReadCap}.
* Nothing else may produce a `ReadCap` from a reference that carries no key —
* that would be deriving a cap from a bare reference, which the model forbids.
*/
export function hasReadCap(s: string): s is ReadCap {
return isNuri(s) && s.includes(CAP_SEGMENT);
}
/**
* The cap-less form of a reference — what it NAMES, with any cap stripped.
*
* The one internal cast of this module, and it is load-bearing: `slice` returns
* `string`, yet slicing a `did:ng:…` at the `:r:` boundary can only yield a
* `did:ng:…` — which the compiler cannot know. Keeping the cast HERE, in the
* primitive that defines the contract, is what lets every caller stay typed with
* no cast of its own.
*/
export function targetOf(nuri: Nuri): Nuri {
const i = nuri.indexOf(CAP_SEGMENT);
return i === -1 ? nuri : (nuri.slice(0, i) as Nuri);
}
/**
* The parsed form — upstream `NuriV0`'s `target` plus the cap half of its `access`,
* and none of the type's eight other fields; a cap-less NURI has no `readCap`.
* Library-internal (see the module header).
*/
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap } {
return hasReadCap(nuri) ? { target: targetOf(nuri), readCap: nuri } : { target: nuri };
}
/**
* The one door a caller's string comes through — validated, then typed.
*
* Public entry points take {@link NuriLike} so a consumer never has to narrow what it
* read from a URL, from storage or from JSON: the SDK will take a plain string too
* (`doc_subscribe(repo_o: String)`, `sdk/js/lib-wasm/src/lib.rs:1908`), so demanding a
* refined type here would manufacture a step to unlearn — and would force this library
* to publish a type guard the SDK will never have.
*
* This is where that permissive edge is paid for: once, at the boundary. Past it the
* whole library works on `Nuri`.
*
* Throws rather than returning `undefined`: a reference that is not one is a caller
* mistake, and swallowing it would produce an empty read with no explanation — the
* failure mode this library keeps paying for elsewhere.
*/
export function toNuri(s: string, op: string): Nuri {
if (!isNuri(s)) {
throw new Error(
`[ng-eventually] ${op}: not a NextGraph reference — expected a "did:ng:…" string, ` +
`got ${JSON.stringify(s)}`,
);
}
return s;
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Generic, NextGraph-shaped types. ZERO application domain.
*/
/**
* A NextGraph URI (document / store / inbox) in its **cap-less** form — it NAMES
* and locates, it does not grant the right to read: `did:ng:o:{doc}:v:{overlay}`.
* `did:ng:` is the URI scheme prefix, not a "without cap" marker; the discriminant
* is the `:r:` segment (see {@link ReadCap}).
*/
export type Nuri = `did:ng:${string}`;
/**
* A NextGraph URI that carries the document's read cap — `…:r:{cap}`. It NAMES
* *and* READS: reading is key possession, never an authorization list. This is the
* upstream name (`ReadCap`).
*
* ── Why a template literal type, and not a branded one ────────────────────
* Both this and {@link Nuri} are **still strings** — assignable to `string`,
* JSON-serializable, no wrapper object — so nothing has to be *un*-typed when the
* real SDK arrives and takes `nuri: String`. What the template buys is the one
* direction that matters: a `ReadCap` is freely usable wherever a `Nuri` is
* expected (a cap IS a NURI with the key inside — upstream's single `NuriV0`),
* while a bare `Nuri` passed where a `ReadCap` is required is a **compile error**.
* That confusion, left to runtime, silently turns "naming is not reading" into
* "naming is reading" — the exact inversion this model exists to remove.
*
* A consumer holding a plain `string` (from storage, a URL, JSON, a form) does NOT
* have to narrow it: every public entry takes {@link NuriLike} and validates at the
* door (`toNuri`), which is why no type guard is exported. Permissive in, precise
* out. The runtime checks stay regardless — a JavaScript consumer never meets the
* compiler, and a cast bypasses it.
*/
export type ReadCap = `did:ng:${string}:r:${string}`;
/** NextGraph-native store scopes. The *mapping* of entities to scopes is the
* consumer's concern; this layer only knows the three scopes exist. */
export type Scope = "public" | "protected" | "private";
/** The current identity id. Target: the wallet user (`session.user`). Polyfill:
* a chosen id, because everyone shares one wallet. */
export type PrincipalId = string;
/**
* Loose shape of the real `@ng-org/web` `ng` object that we wrap. Injected by
* the consumer at {@link configure} — we never hard-import the SDK, which keeps
* the build-alias safe (the app's `@ng-org/web` import can resolve to us) and
* makes the wrapper testable with a fake. Permissive on purpose: the real `ng`
* carries non-function members too, so we accept any property bag.
*/
export type NgLike = Record<string, any>;
/** Loose shape of `@ng-org/orm`'s `useShape` (a generic hook). */
export type UseShapeLike = (...args: any[]) => any;
/**
* The scopes that can carry an inbox. NOT `Scope`: upstream only the public and
* protected store repos get one — `new_store_default` attaches an inbox solely
* `if !private` (`engine/verifier/src/verifier.rs:2994`), and the engine's only two
* `AddInboxCap` commits are for those two (`engine/verifier/src/site.rs:127-152`).
*
* Typing it out means "the private inbox" cannot be written, rather than being written
* and returning nothing.
*/
/**
* A reference as a CALLER may hand it over: any string.
*
* The library returns precise `Nuri`s and accepts loose ones, and that asymmetry is not
* politeness — it is what keeps a consumer from writing something to unlearn. The wasm
* binding takes `nuri: String` (`doc_subscribe(repo_o: String)`,
* `sdk/js/lib-wasm/src/lib.rs:1908`), so the real SDK will accept a plain string too.
* Demanding a `Nuri` here would force every caller to narrow whatever it read from a URL
* or from storage — and therefore force this library to publish a type guard the SDK
* will never have. The need would be manufactured by our own signature.
*
* So: precise on the way out, permissive on the way in, and validated inside
* (`assertNuri`). The guards remain, internal, where the validation happens.
*/
export type NuriLike = Nuri | string;
export type InboxScope = Extract<Scope, "public" | "protected">;