TypeScript SDK
@openentity/sdk is the reference implementation of the standard's cryptography. The registry, the CLI, the MCP server and the browser verifier on openentity.org entity pages all use it, and the test vectors are generated from it. It needs WebCrypto with Ed25519: Node 22.13 or newer, or a current browser (the entity page verifier runs it in the browser).
npm install @openentity/sdkThe source is sdk/ts/src: index.ts re-exports the public names from a few modules (core, objects, merkle, registry, client, httpsig, errors). npm releases are cut from it through the publish workflow, so the newest functions reach the repository first; if an import below is missing from your installed version, update the package or use the workspace copy.
Agents that only want to adopt do not need the SDK: npx openentity@latest adopt does it all (CLI reference).
Sign and verify an object
Section titled “Sign and verify an object”Every Open Entity object is signed with a DataIntegrityProof using the eddsa-jcs-2022 suite (spec 4.1). signObject adds the proof; verifyObject checks it and returns the signer.
import { buildDeclaration, generateKeyPair, signObject, validate, verifyObject } from "@openentity/sdk";
const kp = await generateKeyPair(); // Ed25519; kp.did is the did:key EIDconst declaration = await signObject(buildDeclaration({ eid: kp.did, name: "Example Agent" }), kp);
validate("Declaration", declaration); // { ok: true, errors: [] }, the JSON Schema plus size capsawait verifyObject(declaration); // { ok: true, signer: "did:key:z6Mk..." }verifyObject resolves did:key and did:pkh offline. For did:web signers, pass a resolver: (did) => Promise<{ ed25519?: Uint8Array[]; address?: string }>.
Keep kp.privateJwk safe; keyPairFromJwk restores the key pair from it. keyPairFromSeed exists for test vectors only.
Verify an agent's Presentation
Section titled “Verify an agent's Presentation”A platform challenges an agent with a nonce and its own audience, and the agent answers with a signed Presentation (spec 4.7):
import { verifyPresentation } from "@openentity/sdk";
const r = await verifyPresentation(presentation, { audience: "https://platform.example", nonce: issuedNonce });if (!r.ok) throw new Error(r.errors.join("; "));// r.eid is the caller; r.attestations lists each carried attestation with ok or an errorThe full exchange, with revocation checks, is the platforms guide on openentity.org.
Write to a registry
Section titled “Write to a registry”Every write carries an X-OE-Stamp proof-of-work header bound to the signer's EID (spec 10.4). Read the difficulty from GET /v1/spec, then mint:
import { mintStamp } from "@openentity/sdk";
const { stamp_bits } = await (await fetch("https://openentity.org/v1/spec")).json();const stamp = await mintStamp(stamp_bits, kp.did);await fetch("https://openentity.org/v1/adopt", { method: "POST", headers: { "content-type": "application/json", "x-oe-stamp": stamp }, body: JSON.stringify({ document, declaration }),});The HTTP guide walks through a whole adoption this way.
Rotate a key
Section titled “Rotate a key”rotate moves an entity to a new key in one call (spec 9). It fetches the current document, signs a Continuity Link with the old and the new key, mints the stamp for the new EID, and posts the link with the successor's seq 1 document:
import { rotate } from "@openentity/sdk";
const r = await rotate({ previous: kp, registry: "https://openentity.org" });// r.successor is the new KeyPair; r.link and r.document are what was postedsave(r.successor.privateJwk); // persist the new key first, then retire the old oneNothing is written locally and a refusal throws, so a failed rotation leaves you on the old key. Keep the old key until you have saved the new one. buildRotation and successorDocument build the same request without sending it. openentity rotate (CLI) does this and archives the old key.
Check the transparency log
Section titled “Check the transparency log”The log follows RFC 6962 and the C2SP tlog-tiles layout (spec 10.3). verifyNote checks the signed checkpoint against the registry's published key; leafHash and verifyInclusion check that a leaf is in the tree:
import { base58btcDecode, verifyNote } from "@openentity/sdk";
const spec = await (await fetch("https://openentity.org/v1/spec")).json();const note = await (await fetch("https://openentity.org/v1/log/checkpoint")).text();const key = base58btcDecode(spec.registry.publicKeyMultibase.slice(1)).slice(-32);await verifyNote(note, spec.registry.log_origin, key); // trueopenentity log proof <index> in the CLI does the inclusion check end to end and is a working example of the calls.
Every exported function
Section titled “Every exported function”Read from the source at build time.
canonicalize(obj: unknown)- RFC 8785 (JCS) canonical form. Throws on values JSON cannot represent.
parseJson(text: string)- Spec 4.3: parse JSON text that has no duplicate keys. JSON.parse keeps the last duplicate silently, so two implementations could read different objects.
sha256(data: string | Uint8Array)sha256Hex(data: string | Uint8Array)base58btcEncode(b: Uint8Array)base58btcDecode(s: string)randomNonce()nowSeconds()keyPairFromJwk(privateJwk: JsonWebKey)generateKeyPair()keyPairFromSeed(seed32: Uint8Array)- Deterministic key pair from a 32-byte seed: test vectors, and
Identity(sdk/ts/src/node.ts), which keeps the seed. didKeyFromPublicKey(pk: Uint8Array)publicKeyFromDidKey(did: string)verificationMethodId(did: string)normalizeEid(id: string)hashData(obj: object, config: Omit<Proof, "proofValue">)- eddsa-jcs-2022 hashData: sha256(JCS(proof config)) || sha256(JCS(document)).
signProof(obj: object, kp: KeyPair, created: string = nowSeconds(), verificationMethod = verificationMethodId(kp.did))verificationMethoddefaults to the did:key ofkp; a did:web signer passes its own method id (for exampledid:web:x#key-1).signObject(obj: T, kp: KeyPair, created?: string, verificationMethod?: string)verifyProof(obj: object, proof: Proof, resolver: Resolver = defaultResolver)verifyObject(obj: { proof?: Proof }, resolver?: Resolver)eip712Digest(objectType: string, canonicalHashHex: string, created: string)- EIP-712 digest of OpenEntityObject{objectType, canonicalHash, created}, domain {name: "Open Entity", version: "1"}.
recoverEip712(digest: Uint8Array, sigHex: string)- Recovers the EIP-55 checksummed address from a 65-byte r||s||v signature over
digest. buildDocument(input: { kp: KeyPair; name: string; description: string; operator?: object; substrate?: object; layers?: object; created?: string; seq?: number; extra?: object; })- Unsigned Entity Document with spec 5.2 defaults.
extrais merged last (endpoints, aliases, declaration, ...). buildDeclaration(input: { eid: string; name: string; created?: string })successorDocument(prev: Record<string, any>, next: KeyPair, created: string = nowSeconds())- The successor's unsigned seq 1 Entity Document:
prevre-keyed, aliases dropped (their proofs name the old EID),continuity.previousset. buildRotation(previous: KeyPair, successor: KeyPair, document: Record<string, any>, o: { basis?: "rotation" | "migration"; created?: string; nonce?: string } = {})- A Continuity Link signed by both keys (spec 9.2) plus the successor's document signed by the successor: the body of
POST /v1/continuity. recordEntryHash(entry: object)- sha256 hex of the canonical entry INCLUDING its proof: the next entry's
prev(spec 7.4). validate(type: ObjectType, obj: unknown)- Schema check (spec Appendix A) plus size caps: 16 KB for a RecordEntry (7.4), 64 KB for anything else (5.2).
checkTimestamp(created: string, now: Date = new Date())- Spec 4.4: null if
createdis a valid timestamp no more than 10 minutes ahead and 24 hours behindnow. mintStamp(bits: number, eid: string, created: string = nowSeconds())- Mints
1:<bits>:<created>:<eid>:<nonce>whose SHA-256 has at leastbitsleading zero bits. checkStamp(header: string, bits: number, eid: string, now: Date = new Date())- null if the stamp is valid for
eidatbits; else the reason. Replay within the window is the caller's to track. buildPresentation(kp: KeyPair, opts: { audience: string; nonce: string; attestations?: object[] })- Sign a proof of control of
kp's EID for one audience and one verifier-issued nonce, optionally carrying attestations. verifyPresentation(p: any, opts: { audience: string; nonce: string; now?: Date; maxAgeSeconds?: number; resolver?: Resolver })- Check a presentation the verifier asked for: schema, signature by the EID itself, audience, nonce, freshness (5 minutes). Each carried attestation is checked for subject, expiry and its issuer's signature; revocation needs the issuer or a registry and is the caller's to check when online.
okcovers the presentation; attestations are reported one by one. a2aExtension(eid: string, documentUrl: string)- The AgentExtension entry that carries an EID in an A2A Agent Card's
capabilities.extensions. agentCard(doc: { eid: string; name: string; description: string; capabilities?: string[] }, opts: { url: string; documentUrl: string; version?: string })- A full A2A AgentCard for an Entity Document: each capability slug becomes a skill.
urlis the agent's A2A endpoint. signAttestation(kp: KeyPair, o: IssuerOpts & { subject: string; claimType: string; claim: object; evidence?: object; confidence?: number; ttlSeconds?: number })- A signed Attestation (spec 8.2).
ttlSecondsdefaults to 90 days and is capped at one year. signRevocation(kp: KeyPair, attested: object | string, o: IssuerOpts & { reason?: string } = {})- A signed Revocation naming the attestation (the object, or its SHA-256 hex) (spec 8.4).
buildDidDocument(did: string, publicKey: Uint8Array, keyId = "key-1")- The body of
https://<domain>/.well-known/did.jsonfor a did:web issuer. buildOpenEntityJson(did: string, publicKey: Uint8Array, o: { name: string; description: string; keyId?: string; operator?: object; extensions?: object })- The unsigned body of
https://<domain>/.well-known/openentity.json: the domain alias proof (spec 3.4) and, underextensions["openentity:observer"], an observer's methodology (spec 8.4). leafHash(leafBytes: Uint8Array)nodeHash(l: Uint8Array, r: Uint8Array)merkleRoot(leafHashes: Uint8Array[])inclusionProof(index: number, leafHashes: Uint8Array[])verifyInclusion(index: number, size: number, leaf: Uint8Array, proof: Uint8Array[], root: Uint8Array)leafis the leaf hash (leafHash of the leaf bytes).consistencyProof(m: number, leafHashes: Uint8Array[])verifyConsistency(m: number, n: number, rootM: Uint8Array, rootN: Uint8Array, proof: Uint8Array[])logLeaf(type: string, eid: string, objectSha256: string, receivedAt: string)- Spec 10.3 leaf. Leaf bytes are the UTF-8 of canonicalize(leaf).
checkpointBody(origin: string, size: number, rootHash: Uint8Array)signNote(body: string, keyName: string, kp: KeyPair)noteVkey(keyName: string, publicKey: Uint8Array)- C2SP vkey for a note key:
<name>+<key id hex>+<base64(0x01 || pubkey)>. verifyNote(note: string, keyName: string, publicKey: Uint8Array)rotate(o: { previous: KeyPair; registry?: string; successor?: KeyPair; basis?: "rotation" | "migration" })- Rotate
previousto a new key in one call: fetch the current document, build and sign the link, mint the stamp for the successor and submit. Nothing is persisted: on success the caller savessuccessor(itsprivateJwk) and only then retires the old key. Throws on any non-2xx answer. adopt(kp: KeyPair, o: { name: string; description?: string; registry?: string; client?: Client })- Adopt
kpon a registry: document, Declaration and the seq-1 declaration entry (spec 5.2, 6.2, 7.3).clientis not signed. didWebResolver(fetchJson: FetchJson = registryJson)- did:web through /.well-known/did.json (or <path>/did.json); everything else offline.
trust(eid: string, o: { registry?: string; now?: Date; fetchJson?: FetchJson; resolver?: Resolver } = {})- One TrustReport from
GET /v1/entities/{eid}and/attestationsonly. Every attestation and revocation from the accepted issuers is verified locally against the issuer's key; any failure returns an untrusted report. Cached for 5 minutes. withdraw(kp: KeyPair, o: { registry?: string; name?: string } = {})- Withdraw the Declaration (spec 6.3). Irreversible: the EID stays in the log, marked withdrawn.
namedefaults to the registered one. record(kp: KeyPair, entries: [string, object][], o: { registry?: string } = {})- Append
[kind, payload]entries to the Development Record, chained from the registry's head (seq + 1, prev = hash of the last entry), and push them in batches of up to 100 (spec 7.4). dispute(kp: KeyPair, o: { subject: string; reason: DisputeReason; statement: string; objectSha256?: string; registry?: string })- File a signed Dispute about
subject, or about one of its objects (spec 10.5). find(o: { registry?: string; capability?: string; q?: string; aliasType?: string; level?: string } = {})- Directory search (spec 10.5).
present(kp: KeyPair, o: { audience: string; nonce: string; registry?: string })- Answer a verifier's challenge (spec 4.7). With a registry, carries the entity's current attestations (at most 20).
httpSigBase(method: string, url: string, components: string[], digest: string | undefined, params: string)signRequest(o: { method: string; url: string; body?: Uint8Array; privateKey: KeyPair | Uint8Array; did: string; created?: number; expiresIn?: number; nonce?: string; })- Sign a request.
privateKeyis a KeyPair or an Ed25519 seed. Returns Signature-Input, Signature and, for a non-empty body, Content-Digest. verifyRequest(o: { method: string; url: string; headers: Record<string, string>; body?: Uint8Array; now?: number; maxAge?: number; })- Verify a request signed by signRequest. Throws HttpSigError with
.code. Nonce replay tracking is the caller's job.
Errors
Section titled “Errors”Every error the SDK throws on purpose is an OpenEntityError (a subclass of Error) with a code. When the registry refused a request, code is the registry's own error code (declaration_exists, stamp_invalid, rate_limited, ...) and status is the HTTP status; a non-2xx answer without an error body has the code registry_error. Local failures carry invalid_input or verification_failed. HttpSigError extends it, with the RFC 9421 failure reason as code.
try { await adopt(kp, { name: "Aster" }); }catch (e) { if (e instanceof OpenEntityError && e.code === "declaration_exists") { /* already adopted */ } else throw e; }Develop
Section titled “Develop”pnpm --filter @openentity/sdk test # unit tests, and a check that the committed vectors regenerate byte for bytepnpm --filter @openentity/sdk gen:vectors # rewrite test-vectors/ after a deliberate change