TypeScript SDK
The TypeScript SDK is byte-for-byte interoperable with Go, Python, and Rust. It runs in Node 20+
and in modern browsers — the cryptographic primitives use audited pure-JS implementations from
the Noble suite (@noble/ed25519, @noble/hashes) plus @noble/post-quantum for ML-DSA-65.
No native dependencies, no WebAssembly required.
Stability: every primitive on this page is stable in 1.0.0-alpha.7. There are no
preview / experimental APIs in the snippets below.
Install
Section titled “Install”npm publish for @identities-ai/ratify-protocol is queued behind org approval. For now,
install from source:
git clone https://github.com/identities-ai/ratify-protocolcd ratify-protocol/sdks/typescriptnpm installnpm run build # → dist/
# In your projectnpm install ../ratify-protocol/sdks/typescriptOnce @identities-ai is approved on npm, the canonical install will be:
Three minutes, end to end
Section titled “Three minutes, end to end”import { PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK, generateAgent, generateChallenge, generateHumanRoot, issueDelegation, signChallenge, verifyBundle, type DelegationCert, type ProofBundle,} from "@identities-ai/ratify-protocol";
// 1. Alice generates her hybrid (Ed25519 + ML-DSA-65) root identity.const { root: alice, privateKey: alicePriv } = await generateHumanRoot();
// 2. Her AI agent generates its own hybrid keypair.const { agent, privateKey: agentPriv } = await generateAgent( "Alice's Scheduler", "custom",);
// 3. Alice signs a delegation cert for the agent.const now = Math.floor(Date.now() / 1000);const cert: DelegationCert = { cert_id: "cert-001", version: PROTOCOL_VERSION, issuer_id: alice.id, issuer_pub_key: alice.public_key, subject_id: agent.id, subject_pub_key: agent.public_key, scope: [SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK], issued_at: now, expires_at: now + 7 * 24 * 3600, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) },};await issueDelegation(cert, alicePriv);
// 4. Verifier issues a challenge. Agent signs it.const challenge = generateChallenge();const challengeAt = Math.floor(Date.now() / 1000);const challengeSig = await signChallenge(challenge, challengeAt, agentPriv);
// 5. Agent assembles a proof bundle.const bundle: ProofBundle = { agent_id: agent.id, agent_pub_key: agent.public_key, delegations: [cert], challenge, challenge_at: challengeAt, challenge_sig: challengeSig,};
// 6. Verifier runs the verifier.const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND,});
if (result.valid) { console.log("✓ Authorized"); console.log(" human_id: ", result.human_id); console.log(" agent_id: ", result.agent_id); console.log(" granted_scope: ", result.granted_scope); console.log(" identity_status:", result.identity_status);} else { console.log("✗ Rejected:", result.identity_status, "—", result.error_reason);}Real output (IDs are derived from the freshly generated keys, so they’ll differ on every run):
✓ Authorized human_id: 996dc9a99007e7239b20d08bc845337b agent_id: c7f98cc2bc4fcb492147b08aa887a470 granted_scope: [ 'meeting:attend', 'meeting:speak' ] identity_status: authorized_agentverifyBundle is async — every cryptographic operation goes through crypto.subtle (in
the browser) or Node’s native crypto where available.
Browser usage
Section titled “Browser usage”The same SDK works in browsers. No bundler-specific configuration required for Vite, Next.js,
SvelteKit, or Astro. The cryptographic primitives use crypto.subtle where available and fall
back to pure-JS Noble implementations elsewhere.
import { verifyBundle } from "@identities-ai/ratify-protocol";
// Bundle arrives from your server as JSONconst result = await verifyBundle(parsedBundle, { required_scope: "files:read",});Approximate bundle size for the verifier path: ~50 KB minified + gzipped (most of which is the ML-DSA-65 verifier — Ed25519 alone would be much smaller).
Verifier-side: branching on identity_status
Section titled “Verifier-side: branching on identity_status”verifyBundle always resolves with a VerifyResult. Inspect result.valid first; if it’s
false, the specific failure mode is in result.identity_status. The type is a string-literal
union — your editor will autocomplete the full set:
import type { IdentityStatus } from "@identities-ai/ratify-protocol";
switch (result.identity_status as IdentityStatus) { case "authorized_agent": // ✓ All checks passed. result.granted_scope is the intersection of // every cert's scope (effective scope across the chain). break;
case "expired": // ✗ At least one cert in the chain is past its expires_at, OR not-yet-valid. break;
case "revoked": // ✗ A cert ID in the chain matched a revoked entry returned by your // revocation provider (or legacy is_revoked closure). break;
case "scope_denied": // ✗ required_scope was not in the chain's effective (intersected) scope, // OR an advanced PolicyProvider rejected the bundle. break;
case "constraint_denied": // ✗ A first-class constraint (geo/time/amount/rate) evaluated to // "outside the allowed range". break;
case "constraint_unverifiable": // ✗ A constraint required input the caller didn't provide // (e.g. cert has geo_circle but context.current_lat was undefined). break;
case "constraint_unknown": // ✗ A cert declared a constraint type this SDK build doesn't recognize. // Fail-closed by design. break;
case "delegation_not_authorized": // ✗ An intermediate cert in the chain sub-delegated, but its parent // never granted "identity:delegate". The sub-delegation gate. break;
case "invalid": // ✗ Catch-all for structural / cryptographic failures. // result.error_reason carries a stable machine-readable prefix // (e.g. "stale_challenge: challenge is 31 seconds old (max 300)"). break;
default: // The compiler will flag this if the protocol adds new statuses. const _exhaustive: never = result.identity_status;}The verifier is fail-closed by default — any error path returns valid: false. The
machine-readable failure codes inside error_reason (e.g. stale_challenge, bad_signature,
broken_chain, key_mismatch) are stable; route audit pipelines on those without parsing
prose.
Constraints (geo / time / amount / rate / speed)
Section titled “Constraints (geo / time / amount / rate / speed)”When a delegation declares constraints, supply the runtime context:
import type { VerifierContext } from "@identities-ai/ratify-protocol";
const ctx: VerifierContext = { current_lat: 37.7749, current_lon: -122.4194,};
const result = await verifyBundle(bundle, { required_scope: "drone:deliver", context: ctx,});In TypeScript, the has_* flags from the Go API are inferred from field presence — leaving
current_lat and current_lon undefined is equivalent to “no location supplied” and any
geo_circle / geo_polygon / geo_bbox constraint on the cert will return
constraint_unverifiable.
The full constraint vocabulary (geo_circle, geo_polygon, geo_bbox, time_window,
max_speed_mps, max_amount, max_rate) and the VerifierContext field requirements are
documented in Constraints.
Sub-delegation
Section titled “Sub-delegation”Agent-to-agent delegation uses the same primitive. Alice grants Agent A meeting:attend plus
the identity:delegate privilege; A then issues a sub-delegation to Agent B. Without
identity:delegate on A’s grant from Alice, A’s sub-delegation will be rejected with
delegation_not_authorized.
import { SCOPE_IDENTITY_DELEGATE } from "@identities-ai/ratify-protocol";
// Alice → A: meeting:attend + identity:delegateconst aliceToA: DelegationCert = { cert_id: "cert-alice-to-a", version: PROTOCOL_VERSION, issuer_id: alice.id, issuer_pub_key: alice.public_key, subject_id: agentA.id, subject_pub_key: agentA.public_key, scope: [SCOPE_MEETING_ATTEND, SCOPE_IDENTITY_DELEGATE], issued_at: now, expires_at: now + 86400, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) },};await issueDelegation(aliceToA, alicePriv);
// A → B: a subset of A's grantconst aToB: DelegationCert = { cert_id: "cert-a-to-b", version: PROTOCOL_VERSION, issuer_id: agentA.id, issuer_pub_key: agentA.public_key, subject_id: agentB.id, subject_pub_key: agentB.public_key, scope: [SCOPE_MEETING_ATTEND], issued_at: now, expires_at: now + 3600, signature: { ed25519: new Uint8Array(0), ml_dsa_65: new Uint8Array(0) },};await issueDelegation(aToB, agentAPriv);
// Chain order: [leaf, ..., root]. A→B is the leaf, Alice→A is the root.const bundle: ProofBundle = { agent_id: agentB.id, agent_pub_key: agentB.public_key, delegations: [aToB, aliceToA], challenge, challenge_at: challengeAt, challenge_sig: agentBSig,};
const result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND,});The effective granted_scope is the lex-sorted intersection of every cert in the chain.
identity:delegate doesn’t propagate to B because A didn’t grant it onward — that’s the
intentional behavior.
Provider hooks (SPEC §17)
Section titled “Provider hooks (SPEC §17)”Every hook from the §17 surface is exported as an interface:
import type { RevocationProvider, PolicyProvider, AuditProvider, ConstraintEvaluator, AnchorResolver, PolicyVerdict,} from "@identities-ai/ratify-protocol";
const result = await verifyBundle(bundle, { required_scope: "meeting:attend", revocation: myRevocationProvider, // §17.1 policy: myPolicyProvider, // §17.2 audit: myAuditProvider, // §17.3 constraint_evaluators: myExtensionRegistry, // §17.7 anchor_resolver: myAnchorResolver, // §17.8 policy_verdict: cachedVerdict, // §17.6 policy_secret: policySecret, context: ctx,});Each hook has fail-closed semantics — a RevocationProvider that resolves to an error fails
the bundle with identity_status: "invalid" + error_reason: "revocation_error: ...", not a
silent “unknown means allow.” See
Provider architecture for the full reference.
Type-safe scope strings
Section titled “Type-safe scope strings”The SDK exports every canonical scope as a constant. Use the constants in your code so the compiler catches typos before they reach the verifier:
import { SCOPE_MEETING_ATTEND, SCOPE_MEETING_SPEAK, SCOPE_PAYMENTS_AUTHORIZE, SCOPE_IDENTITY_DELEGATE,} from "@identities-ai/ratify-protocol";
// Compiler-safeconst scopes = [SCOPE_MEETING_ATTEND, SCOPE_PAYMENTS_AUTHORIZE];
// Compiler can't help you here — typo "meting:attend" would only surface at verify timeconst scopesUnsafe = ["meeting:attend", "payments:athorize"];The full canonical 53-scope vocabulary is documented at Scopes and
exposed in src/scope.ts.
Running the conformance suite
Section titled “Running the conformance suite”cd ratify-protocol/sdks/typescriptnpm run test:conformanceYou should see all 59 canonical wire-format fixtures plus the 10 alpha.7 cross-SDK byte-equivalence vectors pass. If any fail, the bytes have drifted from the reference and the SDK is not interoperable — file a bug.
Where to next
Section titled “Where to next”- Protocol concepts: Delegate → Present → Verify — every primitive in depth
- Constraints — full constraint vocabulary +
VerifierContextreference - Provider architecture — the §17 hooks plus the alpha.7 levers (
VerificationReceipt,PolicyVerdict) - Integration guides — surface-specific patterns