Quickstart

This guide shows the smallest useful TypeScript flow with the npm package: create an issuer authority, create a holder issuance request, issue a blind signature, finalize a credential, authorize an external app to use it, verify the authorization, and then revoke the credential.

The high-level API is organized around runtime contexts:

import type {
HolderAuthorizationRequest,
JsonValue,
RevocationLocation,
} from "@fedibtc/peerbadge-sdk-wasm";
import {
HolderContext,
IssuerContext,
PendingIssuance,
VerificationContext,
} from "@fedibtc/peerbadge-sdk-wasm";
const credentialInfo = {
schema: "example-membership-v1.0",
trust_level: 7,
} satisfies JsonValue;

> The schema strings in these examples are placeholders. Deployed first-party
> schemas (such as `fedi-trust-score-v1.0`) are defined once in
> [`crates/schemas`](../crates/schemas) — use its constructors and parsers instead
> of hand-building `info`, and never reuse a deployed schema string for a new
> credential type.


const revocationLocations = [
{
protocol: "nostr",
location: "wss://relay.example.com",
},
] satisfies readonly RevocationLocation[];

// Issuer: create a signed public issuer authority.
const issuer = IssuerContext.generate();
const issuerAuthority = issuer.issuerAuthority(revocationLocations);

// Holder: create a blinded issuance request and keep pending state locally.
const holder = HolderContext.generate();
const blindMsg = holder.publicKey;
const { request, pending } = PendingIssuance.createRequest(
issuerAuthority,
credentialInfo,
blindMsg,
);

// Issuer: bind visible credential info and blind-sign the holder request.
const response = issuer.issueCredential(credentialInfo, request);

// Holder: unblind the response and finalize the credential.
const credential = pending.finalize(issuerAuthority, response);

// Verifier: trust the issuer authority before accepting credentials.
const verifier = new VerificationContext();
verifier.addIssuerAuthority(issuerAuthority);

const verified = verifier.verifyCredential(credential);
console.log(verified); // true

// Holder: authorize an external application subject key to use this credential.
// Subject-key generation and live proof-of-possession are application-owned.
const subjectPubkey = "33".repeat(32);
const holderAuthorizationRequest = {
subject_pubkey: subjectPubkey,
} satisfies HolderAuthorizationRequest;
const holderAuthorization = holder.authorizeCredentialUse(
holderAuthorizationRequest,
credential,
);

// Verifier: check the credential, holder authorization, holder binding,
// authorized credential digest, and authorization issued-at time.
const authorized = verifier.verifyCredentialAuthorization(
credential,
holderAuthorization,
);
console.log(authorized); // true

// Issuer: create a signed revocation for the finalized credential.
const signedRevocation = issuer.revokeCredential(credential);

// Verifier: ingest revocations before presenting a credential as accepted.
verifier.addRevocation(signedRevocation);

try {
verifier.verifyCredential(credential);
} catch (error) {
console.log(error instanceof Error ? error.message : error);
// credential has been revoked
}

credentialInfo is visible to the issuer during signing and becomes credential.info in the final credential.

blindMsg is hidden from the issuer during signing and becomes credential.blind_msg after finalization. For the current Fedi/Nostr use case, this is usually the holder's public key, but the SDK accepts any JSON value. Holder authorization verification expects the common Fedi/Nostr shape where credential.blind_msg is the holder public key string.

HolderAuthorizationRequest names the subject key asking to act under the holder identity. The holder chooses the selected credential separately when signing. The SDK derives the signed credential_digest from that credential and sets the authorization issued_at field.

If the holder may reload or leave the page before receiving the issuer response, store the pending state:

const credentialInfo = {
schema: "example-membership-v1.0",
trust_level: 7,
} satisfies JsonValue;

const { request, pending } = PendingIssuance.createRequest(
issuerAuthority,
credentialInfo,
blindMsg,
);

const pendingState = pending.exportState();
localStorage.setItem("pending-issuance", pendingState);

// Send request to the issuer through your application transport.

Import that state before finalizing:

const pendingState = localStorage.getItem("pending-issuance");

if (!pendingState) {
throw new Error("missing pending issuance state");
}

const importedPending = PendingIssuance.importState(pendingState);

// Receive the issuer's response through your application transport.
const response = await receiveIssuanceResponse();
const credential = importedPending.finalize(issuerAuthority, response);
localStorage.removeItem("pending-issuance");

The exported pending state is sensitive holder-side issuance material. Do not log it or send it to the issuer.

The SDK owns protocol-sensitive operations:

  • Key generation and import/export.
  • Issuer authority signing.
  • Holder blinding and pending issuance state.
  • Issuer blind signing.
  • Holder finalization.
  • Holder authorization signing.
  • Credential verification.
  • Holder authorization verification.
  • Revocation signing and verification.
  • Canonical JSON encoding used by signatures and digests.

Your app still owns:

  • Storage for keys, pending issuance state, credentials, and trusted issuers.
  • Storage and transport for holder authorizations.
  • QR code generation and scanning.
  • Nostr relay, HTTP, file, or other transport.
  • UI state and user confirmation.
  • Subject-key generation, custody, and proof-of-possession.
  • Verifier policy and trust-list management.
  • Revocation refresh jobs.

The quickstart mirrors the tested flows in test/full-issuance-flow.test.ts and test/credential.test.ts. Future examples should stay close to those tests or be compiled directly in CI.