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-sdkQuick 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 passkeycreateKey(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 keysignEvent(event: Event): Promise<Event>- Sign an eventgetCurrentKeyInfo(): KeyInfo | null- Get current key infosetCurrentKeyInfo(keyInfo: KeyInfo): void- Set current key infohasKeyInfo(): boolean- Check if key info existsclearStoredKeyInfo(): void- Clear stored key infocheckPRFSupport(): Promise<boolean>- Check if PRF is supportedderiveSaltFromUsername(username?: string): Promise<string>- Derive salt from username (SHA-256)
Recovery Methods
addPasswordRecovery(password: string): Promise<KeyInfo>- Add password recovery to an existing PRF keyactivateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo>- Recover using password with a new passkey credential ID from a new devicegetRecoveryForKind0(): RecoveryData | null- Get recovery data for publishing to kind-0publishRecoveryToKind0(relayService: RelayService, content?: string): Promise<boolean>- Publish recovery data to a kind-0 event, preserving existing content and tagsparseRecoveryTag(tags: string[][]): KeyRecovery | null- Parse recovery tag from eventverifyRecoverySignature(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:
- Host the identity provider on a single origin (e.g.,
identity.org.com) - All sites embed it as an iframe:
<iframe src="https://identity.org.com/auth"> - The iframe calls
document.requestStorageAccess()to access its first-party storage - The passkey uses the shared
rpId(identity.org.com), so all sites derive the same key - KeyInfo in
localStorageis 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 cachingtimeoutMs: Cache timeout in milliseconds (default: 30 minutes)cacheOnCreation: Whentrue, caches the key immediately aftercreateKey()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 EventStoregetRelays(): string[]- Get current relay URLssetRelays(urls: string[]): void- Set relay URLspublishEvent(event: Event, timeoutMs?: number): Promise<boolean>- Publish eventfetchProfile(pubkey: string): Promise<ProfileMetadata | null>- Fetch profilefetchProfileRoleTag(pubkey: string): Promise<string | null>- Fetch role tagfetchFollowList(pubkey: string): Promise<FollowEntry[]>- Fetch follow listfetchMultipleProfiles(pubkeys: string[]): Promise<Map<string, ProfileMetadata>>- Fetch multiple profilesqueryProfiles(pubkeys?: string[], limit?: number): Promise<Map<string, ProfileMetadata>>- Query profilespublishFollowList(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
- Admin creates an identity and enables multisig — the passkey-derived secret is split into N shares
- Admin distributes share credentials to members (via QR code, encrypted message, etc.)
- Members import their share into their own identity
- 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-coreSetup 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 CImport 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 storedSigning
// 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 configisMultisig(): boolean— Check if multisig is enabled on the current identitygetMultisigConfig(): MultisigConfig | null— Get the public multisig configurationgetPeerStatus(): Promise<PeerInfo[]>— Check which group members are onlinerotateShares(newThreshold?: number, newTotal?: number): Promise<string[]>— Generate a new keyset, invalidating old sharesimportMultisigShare(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 invalidGroup 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-tsQuick 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 = 2Or 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 messagesUnread 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 backendsstart(): Promise<void>— Start the full pipeline: ingestion, invite sync, key package tracking (required for receiving messages)stop(): void— Stop all pipelines and clean up subscriptionscreateGroup(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 packagesendMessage(group: Group, content: string, kind?: number): Promise<void>— Send an encrypted messagesendShareToMember(group: Group, memberPubkey: string, shareCredential: string, groupCredential: string, relays: string[]): Promise<void>— Send a share credential to a memberonMessage(handler: (msg: GroupMessage) => void): () => void— Subscribe to all group messages (returns unsubscribe function)parseGroupMessage(message: GroupMessage): ShareCredentialMessage | SigningRequest | null— Parse structured messagesgetGroups(): Promise<Group[]>— List all loaded groupsleaveGroup(groupId: Uint8Array | string): Promise<void>— Leave a groupdestroyGroup(groupId: Uint8Array | string): Promise<void>— Destroy a group and purge local dataclose(): void— Alias for stop()markGroupSeen(groupIdHex: string, seenAt?: number): Promise<void>— Mark a group as readprocessInvites(): Promise<void>— Decrypt pending gift wraps and auto-join groups from Welcome messagesgetInviteReader(): InviteReader— Get the underlying InviteReader for advanced invite handlingunreadGroupIds$: 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 messageparseSigningRequest(message: GroupMessage): SigningRequest | null— Parse a signing requestcreateShareMessage(shareCredential: string, groupCredential: string, relays: string[]): string— Create a share credential messageparseShareMessage(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 + ZKPassportOrganizationTrustService- 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 # vLEIQuick 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
- Identity Creation: Generate a private key + commitment (Poseidon hash)
- Group Creation: Admin creates group with their commitment as initial root
- Add Members: New members' commitments update the Merkle tree root
- Publish Root: Only the root is published to kind-0 (not members)
- Proof Generation: Members generate ZK proofs proving membership without revealing identity
- 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 areRoot 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:
- Extract
uniqueIdentifier(SHA256 of holderDID) andsignaturefrom tag - Reconstruct message:
EUDI:${uniqueIdentifier}:${verifiedAt} - 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 KeyInfoclearOrganizationTrust(): Promise<void>- Remove org trustsetIndividualTrust(claims): Promise<void>- Store individual trust in KeyInfoclearIndividualTrust(): Promise<void>- Remove individual trustgetTrustStatus(): VerificationStatus- Get current verification statuspublishVLEIToProfile(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;
}