npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fedibtc/peerbadge-sdk-wasm

v0.1.0

Published

WebAssembly bindings for the PeerBadge protocol.

Readme

PeerBadge SDK

WebAssembly bindings for the PeerBadge protocol, built on partially blind RSA verifiable credentials.

The library is intended to own the protocol-sensitive pieces of credential issuance and verification: holder blinding, issuer partial blind signing, holder finalization, holder authorization signing, runtime validation, and the WASM/TypeScript API surface around those operations.

It deliberately does not own app concerns such as browser storage, QR codes, Nostr relay I/O, HTTP fetching, UI state, subject-key custody, verifier policy, or revocation list refresh jobs.

Documentation is available at fedibtc.github.io/peerbadge-sdk. The source repository is fedibtc/peerbadge-sdk.

The generated npm package is @fedibtc/peerbadge-sdk-wasm. In this repository, tests import from the generated pkg/peerbadge_wasm.js file after pnpm run build.

Issuance Flow

The protocol separates issuer-visible credential information from the holder-hidden message that is blinded during issuance. For the current Fedi/Nostr use case, the hidden message is usually the holder's public key, but protocol methods accept any JSON value.

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 creates signed public metadata for verifiers and holders.
const issuer = IssuerContext.generate();
const issuerAuthority = issuer.issuerAuthority(revocationLocations);

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

// Issuer signs the blinded request while binding the visible credential info.
const response = issuer.issueCredential(credentialInfo, request);

// Holder unblinds and finalizes the response into a verifiable credential.
const credential = pending.finalize(issuerAuthority, response);

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

// Holder can authorize an external app key to use a selected credential.
// Subject-key generation, storage, and live authentication are app-owned.
const subjectPubkey = "33".repeat(32);
const holderAuthorizationRequest = {
  subject_pubkey: subjectPubkey,
} satisfies HolderAuthorizationRequest;
const holderAuthorization = holder.authorizeCredentialUse(
  holderAuthorizationRequest,
  credential,
);

verifier.verifyCredentialAuthorization(credential, holderAuthorization); // true

// Issuer can revoke a finalized credential. Transport/publication is app-owned.
const signedRevocation = issuer.revokeCredential(credential);
verifier.addRevocation(signedRevocation);
verifier.verifyCredential(credential); // throws: credential has been revoked

The finalized credential has this shape:

{
  version: 1,
  credential: {
    issuer_id_pubkey: "nostr-issuer-public-key",
    info: {
      schema: "example-membership-v1.0",
      trust_level: 7,
    },
    blind_msg: "anonymous-holder-public-key",
  },
  proof: {
    signature: "base64url-rsa-signature",
  },
}

During issuance, credential.info is public and credential.blind_msg is blinded. The holder creates an IssuanceRequest plus local pending state, the issuer returns an IssuanceResponse, and the holder finalizes that response into the credential shape above. The issuer partially blind-signs both pieces together: blind_msg is the hidden payload, and info is the visible credential data.

Holder authorization lets a wallet grant an external application key permission to use a selected credential without sharing the holder secret key. For the current authorization verifier, the finalized credential's blind_msg must be the holder public key string used by HolderContext.publicKey.

interface HolderAuthorizationRequest {
  readonly subject_pubkey: string;
}

interface HolderAuthorization {
  readonly version: 1;
  readonly authorization: HolderAuthorizationStatement;
  readonly proof: SchnorrSignatureProof;
}

interface HolderAuthorizationStatement {
  readonly holder_id_pubkey: string;
  readonly subject_pubkey: string;
  readonly credential_digest: CredentialDigest;
  readonly issued_at: Timestamp;
}

type CredentialDigest = string;
type Timestamp = number;

HolderContext.authorizeCredentialUse derives holder_id_pubkey, credential_digest and issued_at. Verifiers call VerificationContext.verifyCredentialAuthorization to check the credential, holder authorization signature, holder binding, authorized credential digest, and issued-at time. Applications still check that the current caller controls authorization.subject_pubkey, and they apply schema-specific policy to the credential.

PendingIssuance can be exported as a versioned string and imported again after a browser reload:

const { request, pending } = PendingIssuance.createRequest(
  issuerAuthority,
  info,
  blindMsg,
);
const pendingState = pending.exportState();

// Store request and pendingState in application storage while issuance is pending.
const importedPending = PendingIssuance.importState(pendingState);
const credential = importedPending.finalize(issuerAuthority, response);

The exported pending issuance state is sensitive holder-side issuance material. It is not a long-term holder private key, but it is required to unblind and finalize the issuer response, so applications should avoid logging or sharing it.

Public API

The current high-level API is organized around runtime contexts:

  • IssuerContext: generate/import/export issuer keys, create signed issuer authorities, issue credentials, and create signed revocations
  • PendingIssuance: create holder issuance requests and finalize issuer responses
  • HolderContext: generate/import/export holder identity keys and authorize external app keys to use credentials
  • VerificationContext: trust issuer authorities, ingest revocations, verify credentials, and verify holder authorizations

All validation failures cross the WASM boundary as thrown JavaScript errors. VerificationContext.verifyCredential and VerificationContext.verifyCredentialAuthorization return true when their checks pass.

The main methods are:

class IssuerContext {
  static generate(): IssuerContext;
  static importSecretKey(secretKey: IssuerSecretKeys): IssuerContext;
  exportSecretKey(): IssuerSecretKeys;
  issuerAuthority(revocation: readonly RevocationLocation[]): IssuerAuthority;
  issueCredential(info: JsonValue, request: IssuanceRequest): IssuanceResponse;
  revokeCredential(credential: SignedCredential): SignedRevocation;
}

class HolderContext {
  static generate(): HolderContext;
  static importSecretKey(secretKey: string): HolderContext;
  exportSecretKey(): string;
  readonly publicKey: string;
  authorizeCredentialUse(
    request: HolderAuthorizationRequest,
    credential: SignedCredential,
  ): HolderAuthorization;
}

class PendingIssuance {
  static createRequest(
    issuerAuthority: IssuerAuthority,
    info: JsonValue,
    blindMsg: JsonValue,
  ): PendingIssuanceResult;

  finalize(
    issuerAuthority: IssuerAuthority,
    response: IssuanceResponse,
  ): SignedCredential;
}

class VerificationContext {
  constructor();
  addIssuerAuthority(issuerAuthority: IssuerAuthority): void;
  addRevocation(revocation: SignedRevocation): void;
  verifyCredential(credential: SignedCredential): boolean;
  verifyCredentialAuthorization(
    credential: SignedCredential,
    authorization: HolderAuthorization,
  ): boolean;
}

Status

This checklist tracks coarse reusable-library readiness rather than every internal implementation detail.

  • [x] Rust/WASM build and TypeScript/Rust test workflows
  • [x] Runtime issuer, holder, and verifier contexts exposed through WASM/TypeScript
  • [x] Signed issuer authority creation and verification
  • [x] Holder issuance request flow with retained pending unblinding state
  • [x] Issuer issuance response flow with partially blind signing over hidden blind_msg plus visible credential.info
  • [x] Holder finalization into a verifiable credential with an unblinded signature
  • [x] Holder authorization signing for external app credential use
  • [x] Credential verification against trusted issuer authorities
  • [x] Credential digesting plus signed revocation creation and verification
  • [x] Revocation-aware credential verification
  • [x] Holder authorization verification against credential binding and issued-at time
  • [x] RFC 8785/JCS canonical JSON encoding with domain-separated credential, issuer authority, revocation, and holder authorization digests/signatures
  • [x] Deterministic protocol snapshots for issuer authorities, issuance messages, credentials, revocations, holder authorizations, and verifier outcomes
  • [ ] Expose machine-readable error or verification result codes across the WASM boundary
  • [ ] Complete a security review of the pbRSA suite, domain separation, randomness, key handling, replay risk, and malformed input behavior

Development

devenv shell
pnpm install
pnpm run build
pnpm test

pnpm run build builds the WASM package with the speed-oriented Cargo profile and runs wasm-opt -O3. Run it inside devenv shell so secp256k1-sys uses Nix LLVM clang for wasm32 C code.

Useful scripts:

  • pnpm run docs rebuilds the WASM package, generates the TypeDoc API reference, generates rustdoc, and copies both into dist/docs/api; run it inside devenv shell.
  • pnpm run docs:serve rebuilds the full docs site and serves it locally with Vite; run it inside devenv shell.
  • pnpm run build:wasm:sys-rng builds with the old direct system RNG path instead of the default thread RNG.
  • pnpm run test:rust runs Rust unit tests for the full workspace.
  • pnpm run test:ts rebuilds the WASM package, typechecks TypeScript, and runs Vitest.
  • pnpm run check runs typecheck and the full test suite.
  • pnpm run publish:dry-run builds and validates the generated package before publishing.

Publishing to npm

Publish @fedibtc/peerbadge-sdk-wasm with the Publish PeerBadge SDK GitHub Actions workflow. The workflow uses npm trusted publishing, so it does not require an npm token. The trusted publisher is configured for staged publishing only: the workflow uploads the package to the npm staging queue, and a package maintainer must approve it with 2FA before the version goes live.

  1. Set the release version in package.json and under [workspace.package] in Cargo.toml. Do not add a leading v.
  2. Update the workspace dependency versions and Cargo.lock when the Cargo workspace version changes.
  3. Merge the version changes into master.
  4. Open Actions > Publish PeerBadge SDK > Run workflow in GitHub.
  5. Select master, enter the release version, and run the workflow. An npm-publish environment reviewer must approve the deployment.
  6. Once the workflow succeeds, approve the staged version on npmjs.com (package > Staging) or from the CLI: npm stage list @fedibtc/peerbadge-sdk-wasm to get the stage id, then npm stage approve <stage-id> and enter the 2FA code when prompted. Use npm stage download <stage-id> to inspect the tarball first if needed.
  7. Confirm with npm view @fedibtc/peerbadge-sdk-wasm@<version> version and npm dist-tag ls @fedibtc/peerbadge-sdk-wasm.

The workflow stops if the selected branch is not master or if either source version does not match the input. It installs the locked dependencies and runs pnpm run check. It then checks the generated package name and version. It also confirms that the version does not already exist on npm. If all checks pass, it stages the public package from pkg.

Run pnpm run publish:dry-run inside devenv shell if you want to inspect the generated package before you start the workflow.