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:
@@ -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;
|
||||
}
|
||||
@@ -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">;
|
||||
Reference in New Issue
Block a user