Skip to content

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/sdk

The 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).

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 EID
const declaration = await signObject(buildDeclaration({ eid: kp.did, name: "Example Agent" }), kp);
validate("Declaration", declaration); // { ok: true, errors: [] }, the JSON Schema plus size caps
await 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.

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 error

The full exchange, with revocation checks, is the platforms guide on openentity.org.

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 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 posted
save(r.successor.privateJwk); // persist the new key first, then retire the old one

Nothing 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.

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); // true

openentity log proof <index> in the CLI does the inclusion check end to end and is a working example of the calls.

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))
verificationMethod defaults to the did:key of kp; a did:web signer passes its own method id (for example did: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. extra is 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: prev re-keyed, aliases dropped (their proofs name the old EID), continuity.previous set.
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 created is a valid timestamp no more than 10 minutes ahead and 24 hours behind now.
mintStamp(bits: number, eid: string, created: string = nowSeconds())
Mints 1:<bits>:<created>:<eid>:<nonce> whose SHA-256 has at least bits leading zero bits.
checkStamp(header: string, bits: number, eid: string, now: Date = new Date())
null if the stamp is valid for eid at bits; 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. ok covers 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. url is 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). ttlSeconds defaults 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.json for 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, under extensions["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)
leaf is 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 previous to 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 saves successor (its privateJwk) 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 kp on a registry: document, Declaration and the seq-1 declaration entry (spec 5.2, 6.2, 7.3). client is 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 /attestations only. 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. name defaults 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. privateKey is 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.

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; }
pnpm --filter @openentity/sdk test # unit tests, and a check that the committed vectors regenerate byte for byte
pnpm --filter @openentity/sdk gen:vectors # rewrite test-vectors/ after a deliberate change