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

ns-auth-sdk

v1.14.3

Published

Decentralized SSO library - Authentication, membership, and profile management

Readme

Stoic Identity

The simplest way of doing Auth with seamless and decentralized Key-Management for SSO, Authentication, Membership, and Profile-Management

Who is it for?

Being trusted by Financial Institutions for Client Onboarding and Digital Communities for Membership Management. Examples include NSAuth and OneBoard. The SDK enables client-side managing of private-keys with WebAuthn passkeys (FIDO2 credentials). By leveraging passkeys, users avoid traditional private‑key backups and password hassles, relying instead on biometric or device‑based authentication. The keys are compatible with common blockchains like Bitcoin and Ethereum and data is stored as events on public relays and can be encrypted.

The Open Alternative for Auth

Open‑source, client‑side, decentralized single‑sign‑on (SSO) like NSAuth is superior because it puts the user’s identity and cryptographic keys directly in the hands of the individual, eliminating reliance on any central authority that could become a single point of failure, a privacy sinkhole, or a bottleneck for policy updates. By storing a self‑sovereign credential on the device’s secure enclave and validating access against a signed, versioned member list, every interaction—from unlocking a gym door to logging into an online course is verified instantly without ever transmitting personal identifiers. This architecture enables real‑time privilege changes (a badge upgrade or a revocation propagates the moment the list is updated), removes passwords and phishing risk through biometric or hardware‑key authentication, and works uniformly for anyone, including stateless persons or diaspora communities, because trust is derived from cryptographic proofs rather than government‑issued IDs. Moreover, being open source lets developers audit the code, contribute improvements, and ensure transparency, while the decentralized design guarantees that no single entity can unilaterally alter membership rules, providing stronger governance, auditability, and privacy than traditional, server‑centric SSO solutions.

Technology

Choose your Backend

NSAuth is designed as a frontend SSO where data can be synced in a trust minimized way. The basics are there for extensions to interoperate with public or private DLT networks or even regular webservers.

Two Approaches

PRF Direct Method

Derive the private key directly from the PRF value produced by a passkey.

Password Fallback Method

If PRF is not supported (e.g., on older browsers or devices without WebAuthn), the SDK automatically falls back to a password-protected key. The user's password encrypts the private key using AES-256-GCM, and the encrypted bundle is stored locally. The password is never stored - it's only used to derive the encryption key on-demand.

Encryption Method

Encrypt an existing private key with a key derived from the passkey’s PRF output. WebAuthn PRF Extension The PRF (Pseudo‑Random Function) extension, part of WebAuthn Level 3, yields deterministic 32‑byte high‑entropy values from an authenticator’s internal private key and a supplied salt. The same credential ID and salt always generate the same PRF output, which never leaves the device except during authentication.

Using PRF Values as Private Keys

A 32‑byte PRF output can serve as a private key if it falls within the secp256k1 range (1 ≤ value < n). The chance of falling outside this range is astronomically low (~2⁻²²⁴), so explicit range checks are generally unnecessary.

Restoration Steps

Install the client on a new device. Fetch the latest kind 30100 event for the target public key. Extract the PWKBlob and decrypt it with the passkey’s PRF value. Use the recovered private key for signing. Multiple passkeys can each have their own PWKBlob, allowing redundancy across devices.

Installation

Install from npm:

npm install ns-auth-sdk
# or
pnpm install ns-auth-sdk
# or
yarn add ns-auth-sdk

Quick Start

1. Initialize Services

import { AuthService, RelayService } from 'ns-auth-sdk';
import { EventStore } from 'ns-auth-sdk';

// Initialize auth service
const authService = new AuthService({
  rpId: 'your-domain.com',
  rpName: 'Your App Name',
  storageKey: 'identity',
  cacheOnCreation: true, // Enable early caching (default: true)
});

// Initialize relay service with EventStore
const relayService = new RelayService({
  relayUrls: ['wss://relay.io'],
});

// Initialize with EventStore
const eventStore = new EventStore(/* config */);
relayService.initialize(eventStore);

2. Create a Passkey

// Create a passkey (triggers biometric)
const credentialId = await authService.createPasskey('[email protected]');

// Create Key
const keyInfo = await authService.createKey(credentialId, undefined, {
  username: '[email protected]',
  recoveryPassword: 'my-recovery-password', // optional
});

// Store keyInfo for later use
authService.setCurrentKeyInfo(keyInfo);

Password Fallback (when PRF unavailable)

When PRF is not supported, username is required and password is used to derive the key:

// Check if password fallback is needed
const prfSupported = await authService.checkPRFSupport();

if (!prfSupported) {
  // Username is REQUIRED when PRF unavailable
  const keyInfo = await authService.createKey(undefined, userPassword, {
    username: '[email protected]',
  });
}

// Sign events
const signedEvent = await authService.signEvent(event);

API Reference

Methods

  • createPasskey(username?: string): Promise<Uint8Array> - Create a new passkey
  • createKey(credentialId?: Uint8Array, password?: string, options?: KeyOptions): Promise<KeyInfo> - Create key from passkey (auto-detects PRF support, uses password fallback if needed)
  • getPublicKey(): Promise<string> - Get current public key
  • signEvent(event: Event): Promise<Event> - Sign an event
  • getCurrentKeyInfo(): KeyInfo | null - Get current key info
  • setCurrentKeyInfo(keyInfo: KeyInfo): void - Set current key info
  • hasKeyInfo(): boolean - Check if key info exists
  • clearStoredKeyInfo(): void - Clear stored key info
  • checkPRFSupport(): Promise<boolean> - Check if PRF is supported
  • deriveSaltFromUsername(username?: string): Promise<string> - Derive salt from username (SHA-256)

Recovery Methods

  • addPasswordRecovery(password: string): Promise<KeyInfo> - Add password recovery to an existing PRF key
  • activateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo> - Recover using password with a new passkey credential ID from a new device
  • getRecoveryForKind0(): RecoveryData | null - Get recovery data for publishing to kind-0
  • publishRecoveryToKind0(relayService: RelayService, content?: string): Promise<boolean> - Publish recovery data to a kind-0 event, preserving existing content and tags
  • parseRecoveryTag(tags: string[][]): KeyRecovery | null - Parse recovery tag from event
  • verifyRecoverySignature(kind0: Event): Promise<boolean> - Verify recovery signature (async)

RelayService Methods

  • fetchKind0Event(pubkey: string): Promise<{ content: string; tags: string[][] } | null> - Fetch the full kind-0 event (content + tags)
  • publishRecoveryToKind0(pubkey: string, recoveryData: RecoveryData, signEvent: (event: Event) => Promise<Event>, content?: string): Promise<boolean> - Publish or update kind-0 with recovery data, preserving existing content and tags

KeyOptions

interface KeyOptions {
  username?: string;           // Required when PRF unavailable
  password?: string;           // Required when PRF unavailable
  recoveryPassword?: string;   // Password for recovery (optional)
}

RecoveryData

interface RecoveryData {
  recoveryPubkey: string;
  recoverySalt: string;
  createdAt?: number;
  signature?: string; // Schnorr signature from recovery key signing the current pubkey
}

Types

// KeyInfo with optional recovery
interface KeyInfo {
  credentialId: string;
  pubkey: string;
  salt: string;
  username?: string;
  recovery?: KeyRecovery;
}

// Key recovery configuration
interface KeyRecovery {
  recoveryPubkey: string;
  recoverySalt: string;
  createdAt?: number;
  signature?: string; // Schnorr signature from recovery key signing the current pubkey
}

// Sign options
interface SignOptions {
  clearMemory?: boolean;
  tags?: string[][];
  password?: string;
}

Recovery Flow

The SDK supports password-based recovery for passkey-protected keys. When creating a key, you can optionally provide a recovery password:

// Create key with recovery enabled
const keyInfo = await authService.createKey(credentialId, undefined, {
  username: '[email protected]',
  recoveryPassword: 'my-recovery-password',
});

// The recovery data is stored in kind-0 tags:
// ["r", recoveryPubkey, recoverySalt, createdAt, signature]

Publishing recovery data to relays:

The recovery data must be published to a kind-0 event so it can be fetched on a new device. The SDK preserves all existing profile content and tags, only amending the recovery "r" tag:

import { RelayService } from 'ns-auth-sdk';

const relayService = new RelayService({
  relayUrls: ['wss://relay.io'],
});
relayService.initialize(eventStore);

// Publish recovery data — existing kind-0 content and tags are preserved
await authService.publishRecoveryToKind0(relayService);

// Or provide custom content instead of fetching the existing kind-0
await authService.publishRecoveryToKind0(relayService, JSON.stringify({
  name: 'My Name',
  about: 'My bio',
  picture: 'https://example.com/avatar.jpg',
}));

Recovery on a new device:

// On new device - create new passkey first
const newCredentialId = await authService.createPasskey('[email protected]');

// Recover using password
const keyInfo = await authService.activateWithPassword('my-recovery-password', newCredentialId);

Verification:

Anyone can verify ownership by fetching the kind-0 and checking the signature:

import { parseRecoveryTag, verifyRecoverySignature } from 'ns-auth-sdk';

// Fetch kind-0 from relay
const kind0 = await relayService.fetchProfile(pubkey);

// Verify recovery signature (async)
const isValid = await verifyRecoverySignature(kind0);
if (isValid) {
  console.log('Recovery key holder controls this identity');
}

Cross-Origin Identity

The SDK stores KeyInfo in localStorage by default. For cross-origin scenarios (e.g., multiple websites sharing the same identity), use the Storage Access API:

// Identity provider hosted at identity.example.com
// All sites embed: <iframe src="https://identity.example.com/auth">

// Inside the iframe, request storage access:
if (document.hasStorageAccess) {
  await document.requestStorageAccess();
}

// Now localStorage is shared across all sites embedding the iframe
const authService = new AuthService({
  rpId: 'identity.org.com',  // Shared rpId for all sites
  rpName: 'org',
});

// Create or load identity — same keyInfo across all embedding sites
const keyInfo = authService.getCurrentKeyInfo();

How it works:

  1. Host the identity provider on a single origin (e.g., identity.org.com)
  2. All sites embed it as an iframe: <iframe src="https://identity.org.com/auth">
  3. The iframe calls document.requestStorageAccess() to access its first-party storage
  4. The passkey uses the shared rpId (identity.org.com), so all sites derive the same key
  5. KeyInfo in localStorage is shared across all embedding sites

Browser support: Storage Access API is supported in all major browsers (Chrome, Firefox, Safari, Edge).

Security note: The user will see a permission prompt the first time a site requests storage access. After granting, access is remembered for that site.

Configuration Options

interface KeyManagerOptions {
  cacheOptions?: {
    enabled: boolean;
    timeoutMs?: number;
    cacheOnCreation?: boolean; // Cache key immediately after derivation (default: true)
  };
  storageOptions?: {
    enabled: boolean;
    storage?: Storage;
    storageKey?: string;
  };
  prfOptions?: {
    rpId?: string;
    timeout?: number;
    userVerification?: UserVerificationRequirement;
  };
}

Cache Options:

  • enabled: Enable/disable key caching
  • timeoutMs: Cache timeout in milliseconds (default: 30 minutes)
  • cacheOnCreation: When true, caches the key immediately after createKey() to reduce biometric prompts from 2-3 to 1-2. This is enabled by default for better user experience.

RelayService

Service for communicating with relays.

Methods

  • initialize(eventStore: EventStore): void - Initialize with EventStore
  • getRelays(): string[] - Get current relay URLs
  • setRelays(urls: string[]): void - Set relay URLs
  • publishEvent(event: Event, timeoutMs?: number): Promise<boolean> - Publish event
  • fetchProfile(pubkey: string): Promise<ProfileMetadata | null> - Fetch profile
  • fetchProfileRoleTag(pubkey: string): Promise<string | null> - Fetch role tag
  • fetchFollowList(pubkey: string): Promise<FollowEntry[]> - Fetch follow list
  • fetchMultipleProfiles(pubkeys: string[]): Promise<Map<string, ProfileMetadata>> - Fetch multiple profiles
  • queryProfiles(pubkeys?: string[], limit?: number): Promise<Map<string, ProfileMetadata>> - Query profiles
  • publishFollowList(pubkey: string, followList: FollowEntry[], signEvent: (event: Event) => Promise<Event>): Promise<boolean> - Publish follow list

Integration with Applesauce

This library is designed to work seamlessly with applesauce-core. The RelayService uses applesauce's EventStore for all relay operations. All applesauce-core exports are re-exported from ns-auth-sdk for convenience.

import { EventStore } from 'ns-auth-sdk';
import { RelayService } from 'ns-auth-sdk';

const eventStore = new EventStore({
  // configuration
});

const relayService = new RelayService();
relayService.initialize(eventStore);

Event Support

The SDK helpers for building events:

import { 
  Helpers, 
  finalizeEvent, 
  getPublicKey, 
  generateSecretKey 
} from 'ns-auth-sdk';

// Generate a new key pair
const sk = generateSecretKey();
const pubkey = getPublicKey(sk);

// Create and sign an event
const event = finalizeEvent({
  kind: 0,
  content: 'My Event',
  created_at: Math.floor(Date.now() / 1000),
  tags: [],
}, sk);

Security Guidance

  • Configure a strict Content Security Policy (CSP) in the host app to restrict script and image sources.
  • Add rate limiting or debouncing around profile queries and event publishing in the host app or API layer.
  • Avoid surfacing raw error details to end users; log detailed errors in secure logs.

Multisig (Threshold Signing)

The SDK supports M-of-N threshold signing via FROST (Flexible Round-Optimized Schnorr Threshold signatures). This enables organizations to require multiple members to collaborate on signing — no single person can act alone.

The resulting signatures are standard BIP-340 Schnorr signatures, indistinguishable from single-signer signatures. Your pubkey never changes.

How It Works

  1. Admin creates an identity and enables multisig — the passkey-derived secret is split into N shares
  2. Admin distributes share credentials to members (via QR code, encrypted message, etc.)
  3. Members import their share into their own identity
  4. Signing requires M-of-N members to coordinate via Nostr relays

Share credentials are encrypted with the identity's secret (PRF or password-derived) before storage in localStorage. The plaintext share is never persisted.

Installation

Multisig requires peer dependencies:

npm install ns-auth-sdk @frostr/bifrost @frostr/igloo-core

Setup Flow (Org Admin)

import { AuthService } from 'ns-auth-sdk';

const adminAuth = new AuthService({
  rpId: 'org.com',
  rpName: 'org',
});

// 1. Create the org identity
const credentialId = await adminAuth.createPasskey('[email protected]');
const keyInfo = await adminAuth.createKey(credentialId);
adminAuth.setCurrentKeyInfo(keyInfo);

// 2. Enable multisig: 2-of-3 threshold
const { groupCredential, shareCredential } = await adminAuth.enableMultisig({
  threshold: 2,
  totalMembers: 3,
  relays: ['wss://relay.io'],
});

// 3. Distribute shares to members
// allShareCredentials[0] → admin (stored encrypted in their localStorage)
// allShareCredentials[1] → member B
// allShareCredentials[2] → member C

Import Flow (Members)

const memberAuth = new AuthService({
  rpId: 'org.com',
  rpName: 'org',
});

// Member creates their own passkey (for local auth)
const credentialId = await memberAuth.createPasskey('[email protected]');
const keyInfo = await memberAuth.createKey(credentialId);
memberAuth.setCurrentKeyInfo(keyInfo);

// Member imports their MultiSig share (received from admin)
await memberAuth.importMultisigShare({
  groupCredential: 'bfgroup1...',
  shareCredential: 'bfshare1...',
  relays: ['wss://relay.io'],
});

// The share is encrypted with the member's passkey-derived secret and stored

Signing

// Default: auto-detects mode (multisig if configured, individual otherwise)
const signed = await memberAuth.signEvent(event);

// Force individual signing (personal actions, no peer coordination needed)
const personal = await memberAuth.signEvent(event, { mode: 'individual' });

// Force multisig (org actions, requires threshold approval)
const org = await memberAuth.signEvent(event, { mode: 'multisig' });

// Password-protected identity: provide password to decrypt the share
const signed = await memberAuth.signEvent(event, {
  mode: 'multisig',
  password: 'my-password',
});

Multisig API Reference

AuthService Methods

  • enableMultisig(options: MultisigSetupOptions): Promise<MultisigSetupResult> — Split the identity's secret into shares, encrypt the local share, and store config
  • isMultisig(): boolean — Check if multisig is enabled on the current identity
  • getMultisigConfig(): MultisigConfig | null — Get the public multisig configuration
  • getPeerStatus(): Promise<PeerInfo[]> — Check which group members are online
  • rotateShares(newThreshold?: number, newTotal?: number): Promise<string[]> — Generate a new keyset, invalidating old shares
  • importMultisigShare(options: MultisigImportOptions): Promise<KeyInfo> — Import a share credential into an existing identity

Types

interface MultisigSetupOptions {
  threshold: number;      // M: minimum signers required
  totalMembers: number;   // N: total shares
  relays: string[];       // Nostr relays for coordination
  members?: MultisigMember[];    // Optional: members to invite via Welcome messages
  groupManager?: GroupManager;   // Optional: required if members are provided
}

interface MultisigMember {
  pubkey: string;                    // Member's Nostr pubkey
  keyPackageEvent?: any;            // Member's key package event (kind 443)
}

interface MultisigSetupResult {
  keyInfo: KeyInfo;
  groupCredential: string;       // bfgroup1...
  shareCredential: string;        // bfshare1... (this member's share)
  membersInvited: number;         // number of members invited via Welcome messages
}

interface MultisigImportOptions {
  groupCredential: string;
  shareCredential: string;
  relays: string[];
}

interface MultisigConfig {
  threshold: number;
  totalMembers: number;
  relays: string[];
  groupCredential: string;
  shareCredential: string;
  shareIndex: number;
}

interface PeerInfo {
  pubkey: string;
  status: 'online' | 'offline' | 'unknown';
  latency?: number;
}

type SigningMode = 'individual' | 'multisig';

interface SignOptions {
  mode?: SigningMode;
  clearMemory?: boolean;
  tags?: string[][];
  password?: string;
}

Share Rotation

If a share is compromised or a member leaves, rotate the shares:

// Generate new shares (same threshold and member count)
const newShares = await adminAuth.rotateShares();

// Or change the threshold
const newShares = await adminAuth.rotateShares(3, 5); // 3-of-5

// Distribute new shares to members — old shares are now invalid

Group Messaging

The SDK integrates an end-to-end encrypted group messaging protocol. This provides the coordination layer for multisig groups and general encrypted communication.

Architecture

Organization

MultiSig (2-of-3 directors)           Group (all employees)
├── Alice (director) ◄──────────────► ├── Alice (director)
├── Bob (director)   ◄──────────────► ├── Bob (director)
└── Carol (director) ◄──────────────► ├── Carol (director)
                                      ├── Dave (engineer)
                                      └── Eve (designer)
  • MultiSig = signing authority (directors only, threshold-based)
  • Group = encrypted communication (everyone, read/write)

Installation

npm install ns-auth-sdk @internet-privacy/marmot-ts

Quick Start

import { AuthService, GroupManager, RelayService, EventStore } from 'ns-auth-sdk';

// 1. Initialize services
const authService = new AuthService({ rpId: 'org.com', rpName: 'org' });
const relayService = new RelayService({ relayUrls: ['wss://relay.io'] });
const eventStore = new EventStore(/* config */);
relayService.initialize(eventStore);

// 2. Create identity
const credentialId = await authService.createPasskey('[email protected]');
await authService.createKey(credentialId);

// 3. Initialize group manager (relays are required)
const groupManager = new GroupManager({
  signer: authService,
  network: relayService,
  relays: ['wss://relay.io'],
});
await groupManager.initialize();

// 4. Start the event ingestion pipeline (required for receiving messages)
await groupManager.start();

// 5. Subscribe to incoming messages
groupManager.onMessage((message) => {
  console.log(`${message.senderPubkey}: ${message.content}`);
});

// 6. Create a group
const group = await groupManager.createGroup('org', {
  description: 'Company-wide encrypted communication',
  adminPubkeys: ['alice-pubkey-hex'],
  relays: ['wss://relay.io'],
});

// 7. Invite a member
await groupManager.inviteMember(group, 'bob-pubkey-hex');

// 8. Send a message
await groupManager.sendMessage(group, 'Hello team!');

Share Distribution via Welcome Messages

Share credentials are sent as part of the group's encrypted Welcome message — only the recipient can decrypt them, even if other employees are in the same group:

You can use the members and groupManager options to automatically distribute shares via Welcome messages:

// Admin enables multisig with Welcome-based share distribution
const { groupCredential, membersInvited } = await adminAuth.enableMultisig({
  threshold: 2,
  totalMembers: 3,
  relays: ['wss://relay.io'],
  members: [
    { pubkey: 'bob-pubkey-hex', keyPackageEvent: bobKeyPackage },
    { pubkey: 'carol-pubkey-hex', keyPackageEvent: carolKeyPackage },
  ],
  groupManager,
});

// Members automatically receive their shares via Welcome messages
// membersInvited = 2

Or manually send shares after creating a group:

// Admin creates a group
const group = await groupManager.createGroup('org Directors', {
  adminPubkeys: [adminPubkey],
  relays: ['wss://relay.io'],
});

// Manually invite member and send their share via encrypted Welcome
await groupManager.inviteMember(group, 'bob-pubkey-hex');
await groupManager.sendShareToMember(
  group,
  'bob-pubkey-hex',
  shareCredential,  // Use shareCredential from enableMultisig result
  groupCredential,
  ['wss://relay.io']
);

// Bob receives the Welcome message, decrypts it, and imports the share
const welcomeRumor = /* decrypted NIP-59 gift wrap */;
const joinedGroup = await groupManager.joinGroupFromWelcome(welcomeRumor);

// Parse the share message from the group
groupManager.onMessage((message) => {
  const share = groupManager.parseGroupMessage(message);
  if (share?.type === 'share-credential') {
    memberAuth.importMultisigShare({
      groupCredential: share.groupCredential,
      shareCredential: share.shareCredential,
      relays: share.relays,
    });
  }
});

Coordination Helpers

The SDK provides utilities for structured group messages:

import { createSigningRequest, parseSigningRequest, createShareMessage, parseShareMessage } from 'ns-auth-sdk';

// Send a signing request to the group
const request = createSigningRequest('doc-42', 'Update Document');
await groupManager.sendMessage(group, request);

// Parse incoming messages
groupManager.onMessage((message) => {
  const signingRequest = groupManager.parseGroupMessage(message);
  if (signingRequest?.type === 'signing-request') {
    console.log(`Signing requested: ${signingRequest.description}`);
    // Trigger MultiSig signing flow
  }

  if (signingRequest?.type === 'share-credential') {
    console.log('Share credential received');
    // Import the share
  }
});

Invite Processing

When a member is invited to a group, they receive a GiftWrap (kind 1059) containing a Welcome message. The GroupManager automatically syncs invites from relays. After the user interacts (triggering decryption), call processInvites():

// Start begins automatic invite sync from relays
await groupManager.start();

// After user interaction (e.g., clicking "Check Invites"), decrypt and process
await groupManager.processInvites();
// This auto-joins groups from Welcome messages

Unread Management

The GroupManager tracks unread state per group, persisted to IndexedDB:

// React to unread changes
groupManager.unreadGroupIds$.subscribe((unreadIds) => {
  console.log('Unread groups:', unreadIds);
});

// Mark a group as read when user opens it
await groupManager.markGroupSeen(groupIdHex);

Group Messaging API Reference

GroupManager Methods

  • constructor(options: { signer: any; network: any; relays: string[]; dbName?: string }) — Create a GroupManager (relays are required, no fallback)
  • initialize(): Promise<void> — Initialize with IndexedDB storage backends
  • start(): Promise<void> — Start the full pipeline: ingestion, invite sync, key package tracking (required for receiving messages)
  • stop(): void — Stop all pipelines and clean up subscriptions
  • createGroup(name: string, options?: CreateGroupOptions): Promise<Group> — Create a new MLS group (auto-calls selfUpdate for forward secrecy)
  • joinGroupFromWelcome(welcomeRumor: Event): Promise<Group> — Join a group from a Welcome message (auto-calls selfUpdate)
  • inviteMember(group: Group, memberPubkey: string): Promise<void> — Invite a member via key package
  • sendMessage(group: Group, content: string, kind?: number): Promise<void> — Send an encrypted message
  • sendShareToMember(group: Group, memberPubkey: string, shareCredential: string, groupCredential: string, relays: string[]): Promise<void> — Send a share credential to a member
  • onMessage(handler: (msg: GroupMessage) => void): () => void — Subscribe to all group messages (returns unsubscribe function)
  • parseGroupMessage(message: GroupMessage): ShareCredentialMessage | SigningRequest | null — Parse structured messages
  • getGroups(): Promise<Group[]> — List all loaded groups
  • leaveGroup(groupId: Uint8Array | string): Promise<void> — Leave a group
  • destroyGroup(groupId: Uint8Array | string): Promise<void> — Destroy a group and purge local data
  • close(): void — Alias for stop()
  • markGroupSeen(groupIdHex: string, seenAt?: number): Promise<void> — Mark a group as read
  • processInvites(): Promise<void> — Decrypt pending gift wraps and auto-join groups from Welcome messages
  • getInviteReader(): InviteReader — Get the underlying InviteReader for advanced invite handling
  • unreadGroupIds$: BehaviorSubject<string[]> — Observable of unread group IDs (persisted across page reloads)

Default Storage Backends

  • IndexedDBGroupStateBackend — Default IndexedDB backend for group state (stores MLS group bytes)
  • IndexedDBKeyPackageStore — Default IndexedDB backend for key packages (stores kind-443 key material)
  • IndexedDBKeyValueBackend — Generic key-value store backend for InviteStore and other needs

All use a unified IndexedDB database (default: group) with no external dependencies. Custom backends can be provided via the GroupManager constructor.

Coordination Helpers

  • createSigningRequest(eventId: string, description: string): string — Create a signing request message
  • parseSigningRequest(message: GroupMessage): SigningRequest | null — Parse a signing request
  • createShareMessage(shareCredential: string, groupCredential: string, relays: string[]): string — Create a share credential message
  • parseShareMessage(message: GroupMessage): ShareCredentialMessage | null — Parse a share credential message

Root of Trust

The SDK supports optional "root of trust" verification through multiple credential types. This enables cryptographic verification of identity - both individual and organizational.

Architecture

VerificationStatus
├── verified: boolean        // true = any trust established
├── individual: {
│   ├── eudi?: EUDIClaims    // EUDI PID
│   └── zkp?: ZKPClaims      // ZKPassport (zero-knowledge)
└── organization: {
    ├── vlei?: RootOfTrustInfo     // vLEI credentials
    └── eudiLegal?: EUDILegalClaims // EUDI Legal PID
}

Two separate services handle verification:

  • IndividualTrustService - EUDI PID + ZKPassport
  • OrganizationTrustService - vLEI + EUDI Legal PID

Installation

Root of trust is optional. Install only what you need:

# Individual verification
npm install ns-auth-sdk jose           # EUDI
npm install ns-auth-sdk @zkpassport/sdk # ZKPassport

# Organization verification
npm install ns-auth-sdk @global-vlei/signify-ts # vLEI

Quick Start

import { 
  IndividualTrustService, 
  OrganizationTrustService,
  RelayService 
} from 'ns-auth-sdk';

// Create services
const individualTrust = new IndividualTrustService();
const organizationTrust = new OrganizationTrustService();
const relayService = new RelayService({ relayUrls: ['wss://relay.io'] });

Individual Trust (EUDI + ZKPassport)

EUDI (PID)

EUDI provides cryptographic verification of individual identity through the European Digital Identity Wallet.

import { IndividualTrustService } from 'ns-auth-sdk';

const individualTrust = new IndividualTrustService();

// Connect to EUDI verifier (e.g., walt.id)
await individualTrust.connectEUDI({
  url: 'https://verifier.example.com',
  clientId: 'your-client-id',
});

// Verify EUDI credentials
const claims = await individualTrust.verifyEUDI();
// { holderDID, givenName, familyName, dateOfBirth, nationality, verifiedAt, expiresAt? }

ZKPassport (Zero-Knowledge)

ZKPassport provides zero-knowledge proofs without revealing underlying data.

// Connect to ZKPassport
await individualTrust.connectZKP({ appId: 'your-app-id' });

// Verify with zero-knowledge (supports ALL countries)
const claims = await individualTrust.verifyZKP({
  scope: 'global-adult',
  purpose: 'Age verification',
});
// Returns: { uniqueIdentifier, proofSaid?, firstName?, nationality?, verifiedAt, expiresAt? }

Storing Individual Trust

import { AuthService } from 'ns-auth-sdk';

const authService = new AuthService();

// Store EUDI
await authService.setIndividualTrust({ eudi: eudiClaims });

// Store ZKP
await authService.setIndividualTrust({ zkp: zkpClaims });

// Store both
await authService.setIndividualTrust({ eudi: eudiClaims, zkp: zkpClaims });

// Check verification status
const status = authService.getTrustStatus();
// { verified: true, individual: { eudi?, zkp? }, organization? }

Publishing to Profile

// Publish EUDI to your kind-0
await authService.publishEUDIToProfile(relayService);

// Publish ZKP to your kind-0
await authService.publishZKPToProfile(relayService);

// Fetch from any pubkey
const eudiTags = await relayService.fetchEUDITags(pubkey);
const zkpTags = await relayService.fetchZKPTags(pubkey);

Organization Trust (vLEI + EUDI Legal PID)

vLEI

vLEI provides cryptographic verification of organizational identity through ACDC credentials.

import { OrganizationTrustService } from 'ns-auth-sdk';

const orgTrust = new OrganizationTrustService();

// Connect to KERIA agent
await orgTrust.connectKERIA('https://keria.example.com', 'yourbran');

// Verify vLEI credentials
const vleiClaims = await orgTrust.verifyVLEI();
// { aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt? }

EUDI Legal PID

EUDI also supports Legal Person Identification Data (LPID) for organizations via OpenID4VCI.

// Connect to EUDI verifier for Legal PID
await orgTrust.connectEUDI({
  url: 'https://verifier.example.com',
  clientId: 'your-client-id',
});

// Verify Legal PID
const legalClaims = await orgTrust.verifyEUDILegal();
// { holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt? }

Storing Organization Trust

import { AuthService } from 'ns-auth-sdk';

const authService = new AuthService();

// Store vLEI
await authService.setOrganizationTrust(vleiClaims);

// Store EUDI Legal PID
await authService.setOrganizationTrust(legalClaims);

// Check verification status
const status = authService.getTrustStatus();
// { verified: true, individual?, organization: { vlei?, eudiLegal? } }

Publishing to Profile

// Publish vLEI to organization's kind-0
await authService.publishVLEIToProfile(relayService);

// Publish EUDI Legal to organization's kind-0
await authService.publishEUDILegalToProfile(relayService);

// Fetch from any org pubkey
const vleiTags = await relayService.fetchVLEITags(pubkey);
const eudiLegalTags = await relayService.fetchEUDILegalTags(pubkey);

FROST Multisig + Organization Trust

Organization trust integrates with FROST multisig for organizational authority:

// Enable multisig requiring organization verification
const result = await authService.enableMultisig({
  threshold: 2,
  totalMembers: 3,
  relays: ['wss://relay.io'],
  organization: {
    lei: '549300TLMP5S...',
    legalName: 'org',
  },
  requireVLEI: true,  // All signers must have valid org trust
});

Kind-0 Tag Formats

All tags use generic list format (not JSON):

// EUDI (individual)
['eudi', holderDID, givenName, familyName, dateOfBirth, nationality, verifiedAt, expiresAt?]

// ZKPassport (individual)
['zkp', uniqueIdentifier, proofSaid?, firstName?, nationality?, expiresAt?]

// vLEI (organization)
['vlei', aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt?]

// EUDI Legal PID (organization)
['eudil', holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt?]

// ZK Membership
['zkm', groupId, root, createdAt, expiresAt?]

ZK Membership

Anonymous group membership where only the group root (Merkle tree root) is published to kind-0, not individual members.

How It Works

  1. Identity Creation: Generate a private key + commitment (Poseidon hash)
  2. Group Creation: Admin creates group with their commitment as initial root
  3. Add Members: New members' commitments update the Merkle tree root
  4. Publish Root: Only the root is published to kind-0 (not members)
  5. Proof Generation: Members generate ZK proofs proving membership without revealing identity
  6. Nullifiers: Each proof includes a nullifier to prevent double-signing

Quick Start

import { ZKMService } from 'ns-auth-sdk';

const zkmService = new ZKMService();

// Generate your identity (private key + commitment)
const identity = await zkmService.generateIdentity();
// { privateKey: '...', commitment: '0xabc123...' }

// Create a group (admin is first member)
const group = await zkmService.createGroup('my-group');
// { id: 'my-group', root: '0xdef456...', members: [...] }

// Add members (updates root)
const newMember = await zkmService.generateIdentity();
const updatedGroup = await zkmService.addMember(group, newMember);

// Publish group root to kind-0 (only root, no members!)
await relayService.publishZKMGroupToKind0(pubkey, { 
  groupId: group.id, 
  root: updatedGroup.root 
}, signEvent);

// Generate anonymous membership proof
const proof = await zkmService.generateProof(group, 'scope-1');
// { groupId, root, proof, nullifier, scope, issuedAt }

// Verify proof against known root
const isValid = await zkmService.verifyProof(proof, knownRoot);

Tag Format

['zkm', groupId, root, createdAt, expiresAt?]

Only the group root is published - members remain private.

Verification

import { verifyZKMTag, verifyAllRootOfTrusts } from 'ns-auth-sdk';

const result = verifyZKMTag(kind0Event);
// Only verifies root exists and is valid
// Does NOT reveal which member you are

Root of Trust Verification

Anyone can verify root of trust credentials from a user's kind-0 profile without needing to query the original issuer. The SDK provides unified verification utilities.

Quick Start

import { 
  verifyAllRootOfTrusts, 
  hasAnyVerifiedRootOfTrust,
  getVerificationSummary 
} from 'ns-auth-sdk';

// Fetch kind-0 from relay
const kind0 = await relayService.fetchKind0Event(pubkey);

// Verify all root of trust credentials
const results = await verifyAllRootOfTrusts(kind0);

// Check if any trust exists
const hasTrust = await hasAnyVerifiedRootOfTrust(kind0);

// Get human-readable summary
const summary = await getVerificationSummary(kind0);
console.log(summary);
// {
//   hasAnyTrust: true,
//   individualVerified: true,
//   organizationVerified: false,
//   membershipVerified: true,
//   details: ['EUDI verified', 'ZKPassport verified', 'ZK Membership verified']
// }

Individual Verification

EUDI (with Nostr key linkage)

EUDI verification includes signature verification to prove the EUDI holder also controls this Nostr key:

import { verifyEUDITag } from 'ns-auth-sdk';

const result = await verifyEUDITag(kind0Event);

if (result.valid) {
  console.log('EUDI verified!', result.claims);
  // { uniqueIdentifier, signature, isOver18, nationality?, verifiedAt, expiresAt? }
}

Verification steps:

  1. Extract uniqueIdentifier (SHA256 of holderDID) and signature from tag
  2. Reconstruct message: EUDI:${uniqueIdentifier}:${verifiedAt}
  3. Verify signature matches the pubkey in the kind-0 event

ZKPassport (zero-knowledge)

import { verifyZKPTag } from 'ns-auth-sdk';

const result = verifyZKPTag(kind0Event);

if (result.valid) {
  console.log('ZKPassport verified!', result.claims);
  // { uniqueIdentifier, proofSaid?, firstName?, nationality?, verifiedAt, expiresAt? }
}

Organization Verification

vLEI

import { verifyVLEITag } from 'ns-auth-sdk';

const result = verifyVLEITag(kind0Event);

if (result.valid) {
  console.log('vLEI verified!', result.claims);
  // { aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt? }
}

EUDI Legal PID

import { verifyEUDILegalTag } from 'ns-auth-sdk';

const result = await verifyEUDILegalTag(kind0Event);

if (result.valid) {
  console.log('EUDI Legal verified!', result.claims);
  // { holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt? }
}

Group Membership Verification

ZK Membership (Semaphore-style)

import { verifyZKMTag } from 'ns-auth-sdk';

const result = verifyZKMTag(kind0Event);

if (result.valid) {
  console.log('ZK Membership verified!', result.claims);
  // { groupId, root, createdAt, expiresAt? }
  // Note: Only root is verified, member identity is NOT revealed
}

API Reference

// Verify individual root of trust types
verifyVLEITag(kind0Event): RootOfTrustVerificationResult
verifyEUDITag(kind0Event): Promise<RootOfTrustVerificationResult>
verifyEUDILegalTag(kind0Event): Promise<RootOfTrustVerificationResult>
verifyZKPTag(kind0Event): RootOfTrustVerificationResult
verifyZKMTag(kind0Event): RootOfTrustVerificationResult

// Verify all at once
verifyAllRootOfTrusts(kind0Event): Promise<AllVerificationResults>

// Quick checks
hasAnyVerifiedRootOfTrust(kind0Event): Promise<boolean>
getVerificationSummary(kind0Event): Promise<VerificationSummary>

// Types
interface RootOfTrustVerificationResult {
  type: 'vlei' | 'eudi' | 'eudil' | 'zkp' | 'zkm';
  valid: boolean;
  claims?: any;
  error?: string;
}

interface AllVerificationResults {
  vlei?: RootOfTrustVerificationResult;
  eudi?: RootOfTrustVerificationResult;
  eudil?: RootOfTrustVerificationResult;
  zkp?: RootOfTrustVerificationResult;
  zkm?: RootOfTrustVerificationResult;
}

interface VerificationSummary {
  hasAnyTrust: boolean;
  individualVerified: boolean;
  organizationVerified: boolean;
  membershipVerified: boolean;
  details: string[];
}

IndividualTrustService

class IndividualTrustService {
  eudi: EUDIService;
  zkp: ZKPService;

  connectEUDI(config: EUDIVerifierConfig): Promise<void>;
  connectZKP(config: ZKPVerifierConfig): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  verifyEUDI(): Promise<EUDIClaims>;
  verifyZKP(options?: Partial<ZKPVerificationRequest>): Promise<ZKPClaims>;
  getVerificationStatus(): { eudi: boolean; zkp: boolean };
}

OrganizationTrustService

class OrganizationTrustService {
  keria: KERIAService;
  eudi: EUDIService;

  connectKERIA(url: string, bran: string): Promise<KERIAConnection>;
  connectEUDI(config: EUDIVerifierConfig): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  verifyVLEI(): Promise<RootOfTrustInfo>;
  verifyEUDILegal(): Promise<EUDILegalClaims>;
  getVerificationStatus(): { vlei: boolean; eudiLegal: boolean };
}

VerificationStatus

interface VerificationStatus {
  verified: boolean;
  individual?: {
    eudi?: EUDIClaims & { expired?: boolean };
    zkp?: ZKPClaims & { expired?: boolean };
  };
  organization?: {
    vlei?: RootOfTrustInfo & { expired?: boolean };
    eudiLegal?: EUDILegalClaims & { expired?: boolean };
  };
}

Trust Methods (via AuthService)

  • setOrganizationTrust(claims): Promise<void> - Store org trust in KeyInfo
  • clearOrganizationTrust(): Promise<void> - Remove org trust
  • setIndividualTrust(claims): Promise<void> - Store individual trust in KeyInfo
  • clearIndividualTrust(): Promise<void> - Remove individual trust
  • getTrustStatus(): VerificationStatus - Get current verification status
  • publishVLEIToProfile(relayService, orgPubkey?): Promise<boolean>
  • publishEUDIToProfile(relayService): Promise<boolean>
  • publishZKPToProfile(relayService): Promise<boolean>
  • publishEUDILegalToProfile(relayService): Promise<boolean>

vLEI integrates with FROST multisig for organizational authority:

// Enable multisig with vLEI requirement
const result = await authService.enableMultisig({
  threshold: 2,
  totalMembers: 3,
  relays: ['wss://relay.io'],
  organization: {
    lei: '549300TLMP5S...',
    legalName: 'org',
  },
  requireVLEI: true,  // All signers must have valid vLEI
});

// Signers in 'multisig' mode can be verified against vLEI
const signedEvent = await authService.signEvent(event, { mode: 'multisig' });

API Reference

IndividualTrustService

class IndividualTrustService {
  eudi: EUDIService;
  zkp: ZKPService;

  connectEUDI(config: EUDIVerifierConfig): Promise<void>;
  connectZKP(config: ZKPVerifierConfig): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  verifyEUDI(): Promise<EUDIClaims>;
  verifyZKP(options?: Partial<ZKPVerificationRequest>): Promise<ZKPClaims>;
  getVerificationStatus(): { eudi: boolean; zkp: boolean };
}

OrganizationTrustService

class OrganizationTrustService {
  keria: KERIAService;
  eudi: EUDIService;

  connectKERIA(url: string, bran: string): Promise<KERIAConnection>;
  connectEUDI(config: EUDIVerifierConfig): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  verifyVLEI(): Promise<RootOfTrustInfo>;
  verifyEUDILegal(): Promise<EUDILegalClaims>;
  getVerificationStatus(): { vlei: boolean; eudiLegal: boolean };
}

VerificationStatus

interface VerificationStatus {
  verified: boolean;
  individual?: {
    eudi?: EUDIClaims & { expired?: boolean };
    zkp?: ZKPClaims & { expired?: boolean };
  };
  organization?: {
    vlei?: RootOfTrustInfo & { expired?: boolean };
    eudiLegal?: EUDILegalClaims & { expired?: boolean };
  };
}

Root of Trust Types

// EUDI Claims
interface EUDIClaims {
  uniqueIdentifier: string;     // SHA256 hash of holderDID
  signature?: string;           // Signature linking
  isOver18?: boolean;
  nationality?: string;
  verifiedAt: string;  // ISO 8601
  expiresAt?: string;
}

interface ZKPClaims {
  uniqueIdentifier: string;
  proofSaid?: string;
  firstName?: string;
  nationality?: string;
  isOver18?: boolean;
  isEUCitizen?: boolean;
  documentType?: string;
  verifiedAt: string;  // ISO 8601
  expiresAt?: string;
}

interface RootOfTrustInfo {
  aidPrefix: string;
  lei: string;
  legalName: string;
  role: string;
  roleCredentialSaid: string;
  entityCredentialSaid: string;
  verifiedAt: string;  // ISO 8601
  expiresAt?: string;
}

interface EUDILegalClaims {
  holderDID: string;
  legalName: string;
  lei: string;
  legalForm?: string;
  registeredAddress?: string;
  vatNumber?: string;
  taxReference?: string;
  verifiedAt: string;  // ISO 8601
  expiresAt?: string;
}