ns-auth-sdk 1.12.6 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,7 +55,7 @@ import { EventStore } from 'ns-auth-sdk';
55
55
  const authService = new AuthService({
56
56
  rpId: 'your-domain.com',
57
57
  rpName: 'Your App Name',
58
- storageKey: 'nsauth_keyinfo',
58
+ storageKey: 'identity',
59
59
  cacheOnCreation: true, // Enable early caching (default: true)
60
60
  });
61
61
 
@@ -124,9 +124,15 @@ const signedEvent = await authService.signEvent(event);
124
124
  - `addPasswordRecovery(password: string): Promise<KeyInfo>` - Add password recovery to an existing PRF key
125
125
  - `activateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo>` - Recover using password with a new passkey credential ID from a new device
126
126
  - `getRecoveryForKind0(): RecoveryData | null` - Get recovery data for publishing to kind-0
127
+ - `publishRecoveryToKind0(relayService: RelayService, content?: string): Promise<boolean>` - Publish recovery data to a kind-0 event, preserving existing content and tags
127
128
  - `parseRecoveryTag(tags: string[][]): KeyRecovery | null` - Parse recovery tag from event
128
129
  - `verifyRecoverySignature(kind0: Event): Promise<boolean>` - Verify recovery signature (async)
129
130
 
131
+ #### RelayService Methods
132
+
133
+ - `fetchKind0Event(pubkey: string): Promise<{ content: string; tags: string[][] } | null>` - Fetch the full kind-0 event (content + tags)
134
+ - `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
135
+
130
136
  #### KeyOptions
131
137
 
132
138
  ```typescript
@@ -191,6 +197,29 @@ const keyInfo = await authService.createKey(credentialId, undefined, {
191
197
  // ["r", recoveryPubkey, recoverySalt, createdAt, signature]
192
198
  ```
193
199
 
200
+ **Publishing recovery data to relays:**
201
+
202
+ 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:
203
+
204
+ ```typescript
205
+ import { RelayService } from 'ns-auth-sdk';
206
+
207
+ const relayService = new RelayService({
208
+ relayUrls: ['wss://relay.io'],
209
+ });
210
+ relayService.initialize(eventStore);
211
+
212
+ // Publish recovery data — existing kind-0 content and tags are preserved
213
+ await authService.publishRecoveryToKind0(relayService);
214
+
215
+ // Or provide custom content instead of fetching the existing kind-0
216
+ await authService.publishRecoveryToKind0(relayService, JSON.stringify({
217
+ name: 'My Name',
218
+ about: 'My bio',
219
+ picture: 'https://example.com/avatar.jpg',
220
+ }));
221
+ ```
222
+
194
223
  **Recovery on a new device:**
195
224
 
196
225
  ```typescript
@@ -218,6 +247,41 @@ if (isValid) {
218
247
  }
219
248
  ```
220
249
 
250
+ ### Cross-Origin Identity
251
+
252
+ 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](https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API):
253
+
254
+ ```typescript
255
+ // Identity provider hosted at identity.example.com
256
+ // All sites embed: <iframe src="https://identity.example.com/auth">
257
+
258
+ // Inside the iframe, request storage access:
259
+ if (document.hasStorageAccess) {
260
+ await document.requestStorageAccess();
261
+ }
262
+
263
+ // Now localStorage is shared across all sites embedding the iframe
264
+ const authService = new AuthService({
265
+ rpId: 'identity.org.com', // Shared rpId for all sites
266
+ rpName: 'org',
267
+ });
268
+
269
+ // Create or load identity — same keyInfo across all embedding sites
270
+ const keyInfo = authService.getCurrentKeyInfo();
271
+ ```
272
+
273
+ **How it works:**
274
+
275
+ 1. Host the identity provider on a single origin (e.g., `identity.org.com`)
276
+ 2. All sites embed it as an iframe: `<iframe src="https://identity.org.com/auth">`
277
+ 3. The iframe calls `document.requestStorageAccess()` to access its first-party storage
278
+ 4. The passkey uses the shared `rpId` (`identity.org.com`), so all sites derive the same key
279
+ 5. KeyInfo in `localStorage` is shared across all embedding sites
280
+
281
+ **Browser support:** Storage Access API is supported in all major browsers (Chrome, Firefox, Safari, Edge).
282
+
283
+ **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.
284
+
221
285
  #### Configuration Options
222
286
 
223
287
  ```typescript
@@ -309,3 +373,1027 @@ const event = finalizeEvent({
309
373
  - Add rate limiting or debouncing around profile queries and event publishing in the host app or API layer.
310
374
  - Avoid surfacing raw error details to end users; log detailed errors in secure logs.
311
375
 
376
+ ## Multisig (Threshold Signing)
377
+
378
+ The SDK supports M-of-N threshold signing via [FROST](https://eprint.iacr.org/2020/852) (Flexible Round-Optimized Schnorr Threshold signatures). This enables organizations to require multiple members to collaborate on signing — no single person can act alone.
379
+
380
+ The resulting signatures are standard BIP-340 Schnorr signatures, indistinguishable from single-signer signatures. Your `pubkey` never changes.
381
+
382
+ ### How It Works
383
+
384
+ 1. **Admin** creates an identity and enables multisig — the passkey-derived secret is split into N shares
385
+ 2. **Admin distributes** share credentials to members (via QR code, encrypted message, etc.)
386
+ 3. **Members** import their share into their own identity
387
+ 4. **Signing** requires M-of-N members to coordinate via Nostr relays
388
+
389
+ Share credentials are encrypted with the identity's secret (PRF or password-derived) before storage in localStorage. The plaintext share is never persisted.
390
+
391
+ ### Installation
392
+
393
+ Multisig requires peer dependencies:
394
+
395
+ ```bash
396
+ npm install ns-auth-sdk @frostr/bifrost @frostr/igloo-core
397
+ ```
398
+
399
+ ### Setup Flow (Org Admin)
400
+
401
+ ```typescript
402
+ import { AuthService } from 'ns-auth-sdk';
403
+
404
+ const adminAuth = new AuthService({
405
+ rpId: 'org.com',
406
+ rpName: 'org',
407
+ });
408
+
409
+ // 1. Create the org identity
410
+ const credentialId = await adminAuth.createPasskey('alice@org.com');
411
+ const keyInfo = await adminAuth.createKey(credentialId);
412
+ adminAuth.setCurrentKeyInfo(keyInfo);
413
+
414
+ // 2. Enable multisig: 2-of-3 threshold
415
+ const { groupCredential, shareCredential } = await adminAuth.enableMultisig({
416
+ threshold: 2,
417
+ totalMembers: 3,
418
+ relays: ['wss://relay.io'],
419
+ });
420
+
421
+ // 3. Distribute shares to members
422
+ // allShareCredentials[0] → admin (stored encrypted in their localStorage)
423
+ // allShareCredentials[1] → member B
424
+ // allShareCredentials[2] → member C
425
+ ```
426
+
427
+ ### Import Flow (Members)
428
+
429
+ ```typescript
430
+ const memberAuth = new AuthService({
431
+ rpId: 'org.com',
432
+ rpName: 'org',
433
+ });
434
+
435
+ // Member creates their own passkey (for local auth)
436
+ const credentialId = await memberAuth.createPasskey('member@org.com');
437
+ const keyInfo = await memberAuth.createKey(credentialId);
438
+ memberAuth.setCurrentKeyInfo(keyInfo);
439
+
440
+ // Member imports their MultiSig share (received from admin)
441
+ await memberAuth.importMultisigShare({
442
+ groupCredential: 'bfgroup1...',
443
+ shareCredential: 'bfshare1...',
444
+ relays: ['wss://relay.io'],
445
+ });
446
+
447
+ // The share is encrypted with the member's passkey-derived secret and stored
448
+ ```
449
+
450
+ ### Signing
451
+
452
+ ```typescript
453
+ // Default: auto-detects mode (multisig if configured, individual otherwise)
454
+ const signed = await memberAuth.signEvent(event);
455
+
456
+ // Force individual signing (personal actions, no peer coordination needed)
457
+ const personal = await memberAuth.signEvent(event, { mode: 'individual' });
458
+
459
+ // Force multisig (org actions, requires threshold approval)
460
+ const org = await memberAuth.signEvent(event, { mode: 'multisig' });
461
+
462
+ // Password-protected identity: provide password to decrypt the share
463
+ const signed = await memberAuth.signEvent(event, {
464
+ mode: 'multisig',
465
+ password: 'my-password',
466
+ });
467
+ ```
468
+
469
+ ### Multisig API Reference
470
+
471
+ #### AuthService Methods
472
+
473
+ - `enableMultisig(options: MultisigSetupOptions): Promise<MultisigSetupResult>` — Split the identity's secret into shares, encrypt the local share, and store config
474
+ - `isMultisig(): boolean` — Check if multisig is enabled on the current identity
475
+ - `getMultisigConfig(): MultisigConfig | null` — Get the public multisig configuration
476
+ - `getPeerStatus(): Promise<PeerInfo[]>` — Check which group members are online
477
+ - `rotateShares(newThreshold?: number, newTotal?: number): Promise<string[]>` — Generate a new keyset, invalidating old shares
478
+ - `importMultisigShare(options: MultisigImportOptions): Promise<KeyInfo>` — Import a share credential into an existing identity
479
+
480
+ #### Types
481
+
482
+ ```typescript
483
+ interface MultisigSetupOptions {
484
+ threshold: number; // M: minimum signers required
485
+ totalMembers: number; // N: total shares
486
+ relays: string[]; // Nostr relays for coordination
487
+ members?: MultisigMember[]; // Optional: members to invite via Welcome messages
488
+ groupManager?: GroupManager; // Optional: required if members are provided
489
+ }
490
+
491
+ interface MultisigMember {
492
+ pubkey: string; // Member's Nostr pubkey
493
+ keyPackageEvent?: any; // Member's key package event (kind 443)
494
+ }
495
+
496
+ interface MultisigSetupResult {
497
+ keyInfo: KeyInfo;
498
+ groupCredential: string; // bfgroup1...
499
+ shareCredential: string; // bfshare1... (this member's share)
500
+ membersInvited: number; // number of members invited via Welcome messages
501
+ }
502
+
503
+ interface MultisigImportOptions {
504
+ groupCredential: string;
505
+ shareCredential: string;
506
+ relays: string[];
507
+ }
508
+
509
+ interface MultisigConfig {
510
+ threshold: number;
511
+ totalMembers: number;
512
+ relays: string[];
513
+ groupCredential: string;
514
+ shareCredential: string;
515
+ shareIndex: number;
516
+ }
517
+
518
+ interface PeerInfo {
519
+ pubkey: string;
520
+ status: 'online' | 'offline' | 'unknown';
521
+ latency?: number;
522
+ }
523
+
524
+ type SigningMode = 'individual' | 'multisig';
525
+
526
+ interface SignOptions {
527
+ mode?: SigningMode;
528
+ clearMemory?: boolean;
529
+ tags?: string[][];
530
+ password?: string;
531
+ }
532
+ ```
533
+
534
+ ### Share Rotation
535
+
536
+ If a share is compromised or a member leaves, rotate the shares:
537
+
538
+ ```typescript
539
+ // Generate new shares (same threshold and member count)
540
+ const newShares = await adminAuth.rotateShares();
541
+
542
+ // Or change the threshold
543
+ const newShares = await adminAuth.rotateShares(3, 5); // 3-of-5
544
+
545
+ // Distribute new shares to members — old shares are now invalid
546
+ ```
547
+
548
+ ## Group Messaging
549
+
550
+ The SDK integrates an end-to-end encrypted group messaging protocol. This provides the coordination layer for multisig groups and general encrypted communication.
551
+
552
+ ### Architecture
553
+
554
+ ```
555
+ Organization
556
+
557
+ MultiSig (2-of-3 directors) Group (all employees)
558
+ ├── Alice (director) ◄──────────────► ├── Alice (director)
559
+ ├── Bob (director) ◄──────────────► ├── Bob (director)
560
+ └── Carol (director) ◄──────────────► ├── Carol (director)
561
+ ├── Dave (engineer)
562
+ └── Eve (designer)
563
+ ```
564
+
565
+ - **MultiSig** = signing authority (directors only, threshold-based)
566
+ - **Group** = encrypted communication (everyone, read/write)
567
+
568
+ ### Installation
569
+
570
+ ```bash
571
+ npm install ns-auth-sdk @internet-privacy/marmot-ts
572
+ ```
573
+
574
+ ### Quick Start
575
+
576
+ ```typescript
577
+ import { AuthService, GroupManager, RelayService, EventStore } from 'ns-auth-sdk';
578
+
579
+ // 1. Initialize services
580
+ const authService = new AuthService({ rpId: 'org.com', rpName: 'org' });
581
+ const relayService = new RelayService({ relayUrls: ['wss://relay.io'] });
582
+ const eventStore = new EventStore(/* config */);
583
+ relayService.initialize(eventStore);
584
+
585
+ // 2. Create identity
586
+ const credentialId = await authService.createPasskey('alice@org.com');
587
+ await authService.createKey(credentialId);
588
+
589
+ // 3. Initialize group manager (relays are required)
590
+ const groupManager = new GroupManager({
591
+ signer: authService,
592
+ network: relayService,
593
+ relays: ['wss://relay.io'],
594
+ });
595
+ await groupManager.initialize();
596
+
597
+ // 4. Start the event ingestion pipeline (required for receiving messages)
598
+ await groupManager.start();
599
+
600
+ // 5. Subscribe to incoming messages
601
+ groupManager.onMessage((message) => {
602
+ console.log(`${message.senderPubkey}: ${message.content}`);
603
+ });
604
+
605
+ // 6. Create a group
606
+ const group = await groupManager.createGroup('org', {
607
+ description: 'Company-wide encrypted communication',
608
+ adminPubkeys: ['alice-pubkey-hex'],
609
+ relays: ['wss://relay.io'],
610
+ });
611
+
612
+ // 7. Invite a member
613
+ await groupManager.inviteMember(group, 'bob-pubkey-hex');
614
+
615
+ // 8. Send a message
616
+ await groupManager.sendMessage(group, 'Hello team!');
617
+ ```
618
+
619
+ ### Share Distribution via Welcome Messages
620
+
621
+ 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:
622
+
623
+ You can use the `members` and `groupManager` options to automatically distribute shares via Welcome messages:
624
+
625
+ ```typescript
626
+ // Admin enables multisig with Welcome-based share distribution
627
+ const { groupCredential, membersInvited } = await adminAuth.enableMultisig({
628
+ threshold: 2,
629
+ totalMembers: 3,
630
+ relays: ['wss://relay.io'],
631
+ members: [
632
+ { pubkey: 'bob-pubkey-hex', keyPackageEvent: bobKeyPackage },
633
+ { pubkey: 'carol-pubkey-hex', keyPackageEvent: carolKeyPackage },
634
+ ],
635
+ groupManager,
636
+ });
637
+
638
+ // Members automatically receive their shares via Welcome messages
639
+ // membersInvited = 2
640
+ ```
641
+
642
+ Or manually send shares after creating a group:
643
+
644
+ ```typescript
645
+ // Admin creates a group
646
+ const group = await groupManager.createGroup('org Directors', {
647
+ adminPubkeys: [adminPubkey],
648
+ relays: ['wss://relay.io'],
649
+ });
650
+
651
+ // Manually invite member and send their share via encrypted Welcome
652
+ await groupManager.inviteMember(group, 'bob-pubkey-hex');
653
+ await groupManager.sendShareToMember(
654
+ group,
655
+ 'bob-pubkey-hex',
656
+ shareCredential, // Use shareCredential from enableMultisig result
657
+ groupCredential,
658
+ ['wss://relay.io']
659
+ );
660
+
661
+ // Bob receives the Welcome message, decrypts it, and imports the share
662
+ const welcomeRumor = /* decrypted NIP-59 gift wrap */;
663
+ const joinedGroup = await groupManager.joinGroupFromWelcome(welcomeRumor);
664
+
665
+ // Parse the share message from the group
666
+ groupManager.onMessage((message) => {
667
+ const share = groupManager.parseGroupMessage(message);
668
+ if (share?.type === 'share-credential') {
669
+ memberAuth.importMultisigShare({
670
+ groupCredential: share.groupCredential,
671
+ shareCredential: share.shareCredential,
672
+ relays: share.relays,
673
+ });
674
+ }
675
+ });
676
+ ```
677
+
678
+ ### Coordination Helpers
679
+
680
+ The SDK provides utilities for structured group messages:
681
+
682
+ ```typescript
683
+ import { createSigningRequest, parseSigningRequest, createShareMessage, parseShareMessage } from 'ns-auth-sdk';
684
+
685
+ // Send a signing request to the group
686
+ const request = createSigningRequest('doc-42', 'Update Document');
687
+ await groupManager.sendMessage(group, request);
688
+
689
+ // Parse incoming messages
690
+ groupManager.onMessage((message) => {
691
+ const signingRequest = groupManager.parseGroupMessage(message);
692
+ if (signingRequest?.type === 'signing-request') {
693
+ console.log(`Signing requested: ${signingRequest.description}`);
694
+ // Trigger MultiSig signing flow
695
+ }
696
+
697
+ if (signingRequest?.type === 'share-credential') {
698
+ console.log('Share credential received');
699
+ // Import the share
700
+ }
701
+ });
702
+ ```
703
+
704
+ ### Invite Processing
705
+
706
+ 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()`:
707
+
708
+ ```typescript
709
+ // Start begins automatic invite sync from relays
710
+ await groupManager.start();
711
+
712
+ // After user interaction (e.g., clicking "Check Invites"), decrypt and process
713
+ await groupManager.processInvites();
714
+ // This auto-joins groups from Welcome messages
715
+ ```
716
+
717
+ ### Unread Management
718
+
719
+ The `GroupManager` tracks unread state per group, persisted to IndexedDB:
720
+
721
+ ```typescript
722
+ // React to unread changes
723
+ groupManager.unreadGroupIds$.subscribe((unreadIds) => {
724
+ console.log('Unread groups:', unreadIds);
725
+ });
726
+
727
+ // Mark a group as read when user opens it
728
+ await groupManager.markGroupSeen(groupIdHex);
729
+ ```
730
+
731
+ ### Group Messaging API Reference
732
+
733
+ #### GroupManager Methods
734
+
735
+ - `constructor(options: { signer: any; network: any; relays: string[]; dbName?: string })` — Create a GroupManager (relays are required, no fallback)
736
+ - `initialize(): Promise<void>` — Initialize with IndexedDB storage backends
737
+ - `start(): Promise<void>` — Start the full pipeline: ingestion, invite sync, key package tracking (required for receiving messages)
738
+ - `stop(): void` — Stop all pipelines and clean up subscriptions
739
+ - `createGroup(name: string, options?: CreateGroupOptions): Promise<Group>` — Create a new MLS group (auto-calls selfUpdate for forward secrecy)
740
+ - `joinGroupFromWelcome(welcomeRumor: Event): Promise<Group>` — Join a group from a Welcome message (auto-calls selfUpdate)
741
+ - `inviteMember(group: Group, memberPubkey: string): Promise<void>` — Invite a member via key package
742
+ - `sendMessage(group: Group, content: string, kind?: number): Promise<void>` — Send an encrypted message
743
+ - `sendShareToMember(group: Group, memberPubkey: string, shareCredential: string, groupCredential: string, relays: string[]): Promise<void>` — Send a share credential to a member
744
+ - `onMessage(handler: (msg: GroupMessage) => void): () => void` — Subscribe to all group messages (returns unsubscribe function)
745
+ - `parseGroupMessage(message: GroupMessage): ShareCredentialMessage | SigningRequest | null` — Parse structured messages
746
+ - `getGroups(): Promise<Group[]>` — List all loaded groups
747
+ - `leaveGroup(groupId: Uint8Array | string): Promise<void>` — Leave a group
748
+ - `destroyGroup(groupId: Uint8Array | string): Promise<void>` — Destroy a group and purge local data
749
+ - `close(): void` — Alias for stop()
750
+ - `markGroupSeen(groupIdHex: string, seenAt?: number): Promise<void>` — Mark a group as read
751
+ - `processInvites(): Promise<void>` — Decrypt pending gift wraps and auto-join groups from Welcome messages
752
+ - `getInviteReader(): InviteReader` — Get the underlying InviteReader for advanced invite handling
753
+ - `unreadGroupIds$: BehaviorSubject<string[]>` — Observable of unread group IDs (persisted across page reloads)
754
+
755
+ #### Default Storage Backends
756
+
757
+ - `IndexedDBGroupStateBackend` — Default IndexedDB backend for group state (stores MLS group bytes)
758
+ - `IndexedDBKeyPackageStore` — Default IndexedDB backend for key packages (stores kind-443 key material)
759
+ - `IndexedDBKeyValueBackend` — Generic key-value store backend for InviteStore and other needs
760
+
761
+ All use a unified IndexedDB database (default: `group`) with no external dependencies. Custom backends can be provided via the `GroupManager` constructor.
762
+
763
+ #### Coordination Helpers
764
+
765
+ - `createSigningRequest(eventId: string, description: string): string` — Create a signing request message
766
+ - `parseSigningRequest(message: GroupMessage): SigningRequest | null` — Parse a signing request
767
+ - `createShareMessage(shareCredential: string, groupCredential: string, relays: string[]): string` — Create a share credential message
768
+ - `parseShareMessage(message: GroupMessage): ShareCredentialMessage | null` — Parse a share credential message
769
+
770
+ ## Root of Trust
771
+
772
+ The SDK supports optional "root of trust" verification through multiple credential types. This enables cryptographic verification of identity - both individual and organizational.
773
+
774
+ ### Architecture
775
+
776
+ ```
777
+ VerificationStatus
778
+ ├── verified: boolean // true = any trust established
779
+ ├── individual: {
780
+ │ ├── eudi?: EUDIClaims // EUDI Wallet PID
781
+ │ └── zkp?: ZKPClaims // ZKPassport (zero-knowledge)
782
+ └── organization: {
783
+ ├── vlei?: RootOfTrustInfo // vLEI credentials
784
+ └── eudiLegal?: EUDILegalClaims // EUDI Legal PID
785
+ }
786
+ ```
787
+
788
+ Two separate services handle verification:
789
+ - `IndividualTrustService` - EUDI PID + ZKPassport
790
+ - `OrganizationTrustService` - vLEI + EUDI Legal PID
791
+
792
+ ### Installation
793
+
794
+ Root of trust is optional. Install only what you need:
795
+
796
+ ```bash
797
+ # Individual verification
798
+ npm install ns-auth-sdk jose # EUDI
799
+ npm install ns-auth-sdk @zkpassport/sdk # ZKPassport
800
+
801
+ # Organization verification
802
+ npm install ns-auth-sdk @global-vlei/signify-ts # vLEI
803
+ ```
804
+
805
+ ### Quick Start
806
+
807
+ ```typescript
808
+ import {
809
+ IndividualTrustService,
810
+ OrganizationTrustService,
811
+ RelayService
812
+ } from 'ns-auth-sdk';
813
+
814
+ // Create services
815
+ const individualTrust = new IndividualTrustService();
816
+ const organizationTrust = new OrganizationTrustService();
817
+ const relayService = new RelayService({ relayUrls: ['wss://relay.io'] });
818
+ ```
819
+
820
+ ## Individual Trust (EUDI + ZKPassport)
821
+
822
+ ### EUDI Wallet (PID)
823
+
824
+ EUDI provides cryptographic verification of individual identity through the European Digital Identity Wallet.
825
+
826
+ ```typescript
827
+ import { IndividualTrustService } from 'ns-auth-sdk';
828
+
829
+ const individualTrust = new IndividualTrustService();
830
+
831
+ // Connect to EUDI verifier (e.g., walt.id)
832
+ await individualTrust.connectEUDI({
833
+ url: 'https://verifier.example.com',
834
+ clientId: 'your-client-id',
835
+ });
836
+
837
+ // Verify EUDI credentials
838
+ const claims = await individualTrust.verifyEUDI();
839
+ // { holderDID, givenName, familyName, dateOfBirth, nationality, verifiedAt, expiresAt? }
840
+ ```
841
+
842
+ ### ZKPassport (Zero-Knowledge)
843
+
844
+ ZKPassport provides zero-knowledge proofs without revealing underlying data.
845
+
846
+ ```typescript
847
+ // Connect to ZKPassport
848
+ await individualTrust.connectZKP({ appId: 'your-app-id' });
849
+
850
+ // Verify with zero-knowledge (supports ALL countries)
851
+ const claims = await individualTrust.verifyZKP({
852
+ scope: 'global-adult',
853
+ purpose: 'Age verification',
854
+ });
855
+ // Returns: { uniqueIdentifier, proofSaid?, firstName?, nationality?, verifiedAt, expiresAt? }
856
+ ```
857
+
858
+ ### Storing Individual Trust
859
+
860
+ ```typescript
861
+ import { AuthService } from 'ns-auth-sdk';
862
+
863
+ const authService = new AuthService();
864
+
865
+ // Store EUDI
866
+ await authService.setIndividualTrust({ eudi: eudiClaims });
867
+
868
+ // Store ZKP
869
+ await authService.setIndividualTrust({ zkp: zkpClaims });
870
+
871
+ // Store both
872
+ await authService.setIndividualTrust({ eudi: eudiClaims, zkp: zkpClaims });
873
+
874
+ // Check verification status
875
+ const status = authService.getTrustStatus();
876
+ // { verified: true, individual: { eudi?, zkp? }, organization? }
877
+ ```
878
+
879
+ ### Publishing to Profile
880
+
881
+ ```typescript
882
+ // Publish EUDI to your kind-0
883
+ await authService.publishEUDIToProfile(relayService);
884
+
885
+ // Publish ZKP to your kind-0
886
+ await authService.publishZKPToProfile(relayService);
887
+
888
+ // Fetch from any pubkey
889
+ const eudiTags = await relayService.fetchEUDITags(pubkey);
890
+ const zkpTags = await relayService.fetchZKPTags(pubkey);
891
+ ```
892
+
893
+ ## Organization Trust (vLEI + EUDI Legal PID)
894
+
895
+ ### vLEI
896
+
897
+ vLEI provides cryptographic verification of organizational identity through ACDC credentials.
898
+
899
+ ```typescript
900
+ import { OrganizationTrustService } from 'ns-auth-sdk';
901
+
902
+ const orgTrust = new OrganizationTrustService();
903
+
904
+ // Connect to KERIA agent
905
+ await orgTrust.connectKERIA('https://keria.example.com', 'yourbran');
906
+
907
+ // Verify vLEI credentials
908
+ const vleiClaims = await orgTrust.verifyVLEI();
909
+ // { aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt? }
910
+ ```
911
+
912
+ ### EUDI Legal PID
913
+
914
+ EUDI also supports Legal Person Identification Data (LPID) for organizations via OpenID4VCI.
915
+
916
+ ```typescript
917
+ // Connect to EUDI verifier for Legal PID
918
+ await orgTrust.connectEUDI({
919
+ url: 'https://verifier.example.com',
920
+ clientId: 'your-client-id',
921
+ });
922
+
923
+ // Verify Legal PID
924
+ const legalClaims = await orgTrust.verifyEUDILegal();
925
+ // { holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt? }
926
+ ```
927
+
928
+ ### Storing Organization Trust
929
+
930
+ ```typescript
931
+ import { AuthService } from 'ns-auth-sdk';
932
+
933
+ const authService = new AuthService();
934
+
935
+ // Store vLEI
936
+ await authService.setOrganizationTrust(vleiClaims);
937
+
938
+ // Store EUDI Legal PID
939
+ await authService.setOrganizationTrust(legalClaims);
940
+
941
+ // Check verification status
942
+ const status = authService.getTrustStatus();
943
+ // { verified: true, individual?, organization: { vlei?, eudiLegal? } }
944
+ ```
945
+
946
+ ### Publishing to Profile
947
+
948
+ ```typescript
949
+ // Publish vLEI to organization's kind-0
950
+ await authService.publishVLEIToProfile(relayService);
951
+
952
+ // Publish EUDI Legal to organization's kind-0
953
+ await authService.publishEUDILegalToProfile(relayService);
954
+
955
+ // Fetch from any org pubkey
956
+ const vleiTags = await relayService.fetchVLEITags(pubkey);
957
+ const eudiLegalTags = await relayService.fetchEUDILegalTags(pubkey);
958
+ ```
959
+
960
+ ### FROST Multisig + Organization Trust
961
+
962
+ Organization trust integrates with FROST multisig for organizational authority:
963
+
964
+ ```typescript
965
+ // Enable multisig requiring organization verification
966
+ const result = await authService.enableMultisig({
967
+ threshold: 2,
968
+ totalMembers: 3,
969
+ relays: ['wss://relay.io'],
970
+ organization: {
971
+ lei: '549300TLMP5S...',
972
+ legalName: 'org',
973
+ },
974
+ requireVLEI: true, // All signers must have valid org trust
975
+ });
976
+ ```
977
+
978
+ ## Kind-0 Tag Formats
979
+
980
+ All tags use generic list format (not JSON):
981
+
982
+ ```typescript
983
+ // EUDI (individual)
984
+ ['eudi', holderDID, givenName, familyName, dateOfBirth, nationality, verifiedAt, expiresAt?]
985
+
986
+ // ZKPassport (individual)
987
+ ['zkp', uniqueIdentifier, proofSaid?, firstName?, nationality?, expiresAt?]
988
+
989
+ // vLEI (organization)
990
+ ['vlei', aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt?]
991
+
992
+ // EUDI Legal PID (organization)
993
+ ['eudil', holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt?]
994
+
995
+ // ZK Membership
996
+ ['zkm', groupId, root, createdAt, expiresAt?]
997
+ ```
998
+
999
+ ## ZK Membership
1000
+
1001
+ Anonymous group membership where only the group root (Merkle tree root) is published to kind-0, not individual members.
1002
+
1003
+ ### How It Works
1004
+
1005
+ 1. **Identity Creation**: Generate a private key + commitment (Poseidon hash)
1006
+ 2. **Group Creation**: Admin creates group with their commitment as initial root
1007
+ 3. **Add Members**: New members' commitments update the Merkle tree root
1008
+ 4. **Publish Root**: Only the root is published to kind-0 (not members)
1009
+ 5. **Proof Generation**: Members generate ZK proofs proving membership without revealing identity
1010
+ 6. **Nullifiers**: Each proof includes a nullifier to prevent double-signing
1011
+
1012
+ ### Quick Start
1013
+
1014
+ ```typescript
1015
+ import { ZKMService } from 'ns-auth-sdk';
1016
+
1017
+ const zkmService = new ZKMService();
1018
+
1019
+ // Generate your identity (private key + commitment)
1020
+ const identity = await zkmService.generateIdentity();
1021
+ // { privateKey: '...', commitment: '0xabc123...' }
1022
+
1023
+ // Create a group (admin is first member)
1024
+ const group = await zkmService.createGroup('my-group');
1025
+ // { id: 'my-group', root: '0xdef456...', members: [...] }
1026
+
1027
+ // Add members (updates root)
1028
+ const newMember = await zkmService.generateIdentity();
1029
+ const updatedGroup = await zkmService.addMember(group, newMember);
1030
+
1031
+ // Publish group root to kind-0 (only root, no members!)
1032
+ await relayService.publishZKMGroupToKind0(pubkey, {
1033
+ groupId: group.id,
1034
+ root: updatedGroup.root
1035
+ }, signEvent);
1036
+
1037
+ // Generate anonymous membership proof
1038
+ const proof = await zkmService.generateProof(group, 'scope-1');
1039
+ // { groupId, root, proof, nullifier, scope, issuedAt }
1040
+
1041
+ // Verify proof against known root
1042
+ const isValid = await zkmService.verifyProof(proof, knownRoot);
1043
+ ```
1044
+
1045
+ ### Tag Format
1046
+
1047
+ ```
1048
+ ['zkm', groupId, root, createdAt, expiresAt?]
1049
+ ```
1050
+
1051
+ Only the group root is published - members remain private.
1052
+
1053
+ ### Verification
1054
+
1055
+ ```typescript
1056
+ import { verifyZKMTag, verifyAllRootOfTrusts } from 'ns-auth-sdk';
1057
+
1058
+ const result = verifyZKMTag(kind0Event);
1059
+ // Only verifies root exists and is valid
1060
+ // Does NOT reveal which member you are
1061
+ ```
1062
+
1063
+ ## Root of Trust Verification
1064
+
1065
+ 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.
1066
+
1067
+ ### Quick Start
1068
+
1069
+ ```typescript
1070
+ import {
1071
+ verifyAllRootOfTrusts,
1072
+ hasAnyVerifiedRootOfTrust,
1073
+ getVerificationSummary
1074
+ } from 'ns-auth-sdk';
1075
+
1076
+ // Fetch kind-0 from relay
1077
+ const kind0 = await relayService.fetchKind0Event(pubkey);
1078
+
1079
+ // Verify all root of trust credentials
1080
+ const results = await verifyAllRootOfTrusts(kind0);
1081
+
1082
+ // Check if any trust exists
1083
+ const hasTrust = await hasAnyVerifiedRootOfTrust(kind0);
1084
+
1085
+ // Get human-readable summary
1086
+ const summary = await getVerificationSummary(kind0);
1087
+ console.log(summary);
1088
+ // {
1089
+ // hasAnyTrust: true,
1090
+ // individualVerified: true,
1091
+ // organizationVerified: false,
1092
+ // membershipVerified: true,
1093
+ // details: ['EUDI verified', 'ZKPassport verified', 'ZK Membership verified']
1094
+ // }
1095
+ ```
1096
+
1097
+ ### Individual Verification
1098
+
1099
+ #### EUDI (with Nostr key linkage)
1100
+
1101
+ EUDI verification includes signature verification to prove the EUDI holder also controls this Nostr key:
1102
+
1103
+ ```typescript
1104
+ import { verifyEUDITag } from 'ns-auth-sdk';
1105
+
1106
+ const result = await verifyEUDITag(kind0Event);
1107
+
1108
+ if (result.valid) {
1109
+ console.log('EUDI verified!', result.claims);
1110
+ // { uniqueIdentifier, signature, isOver18, nationality?, verifiedAt, expiresAt? }
1111
+ }
1112
+ ```
1113
+
1114
+ **Verification steps:**
1115
+ 1. Extract `uniqueIdentifier` (SHA256 of holderDID) and `signature` from tag
1116
+ 2. Reconstruct message: `EUDI:${uniqueIdentifier}:${verifiedAt}`
1117
+ 3. Verify signature matches the pubkey in the kind-0 event
1118
+
1119
+ #### ZKPassport (zero-knowledge)
1120
+
1121
+ ```typescript
1122
+ import { verifyZKPTag } from 'ns-auth-sdk';
1123
+
1124
+ const result = verifyZKPTag(kind0Event);
1125
+
1126
+ if (result.valid) {
1127
+ console.log('ZKPassport verified!', result.claims);
1128
+ // { uniqueIdentifier, proofSaid?, firstName?, nationality?, verifiedAt, expiresAt? }
1129
+ }
1130
+ ```
1131
+
1132
+ ### Organization Verification
1133
+
1134
+ #### vLEI
1135
+
1136
+ ```typescript
1137
+ import { verifyVLEITag } from 'ns-auth-sdk';
1138
+
1139
+ const result = verifyVLEITag(kind0Event);
1140
+
1141
+ if (result.valid) {
1142
+ console.log('vLEI verified!', result.claims);
1143
+ // { aidPrefix, lei, legalName, role, roleCredentialSaid, entityCredentialSaid, verifiedAt, expiresAt? }
1144
+ }
1145
+ ```
1146
+
1147
+ #### EUDI Legal PID
1148
+
1149
+ ```typescript
1150
+ import { verifyEUDILegalTag } from 'ns-auth-sdk';
1151
+
1152
+ const result = await verifyEUDILegalTag(kind0Event);
1153
+
1154
+ if (result.valid) {
1155
+ console.log('EUDI Legal verified!', result.claims);
1156
+ // { holderDID, legalName, lei, legalForm?, registeredAddress?, vatNumber?, taxReference?, verifiedAt, expiresAt? }
1157
+ }
1158
+ ```
1159
+
1160
+ ### Group Membership Verification
1161
+
1162
+ #### ZK Membership (Semaphore-style)
1163
+
1164
+ ```typescript
1165
+ import { verifyZKMTag } from 'ns-auth-sdk';
1166
+
1167
+ const result = verifyZKMTag(kind0Event);
1168
+
1169
+ if (result.valid) {
1170
+ console.log('ZK Membership verified!', result.claims);
1171
+ // { groupId, root, createdAt, expiresAt? }
1172
+ // Note: Only root is verified, member identity is NOT revealed
1173
+ }
1174
+ ```
1175
+
1176
+ ### API Reference
1177
+
1178
+ ```typescript
1179
+ // Verify individual root of trust types
1180
+ verifyVLEITag(kind0Event): RootOfTrustVerificationResult
1181
+ verifyEUDITag(kind0Event): Promise<RootOfTrustVerificationResult>
1182
+ verifyEUDILegalTag(kind0Event): Promise<RootOfTrustVerificationResult>
1183
+ verifyZKPTag(kind0Event): RootOfTrustVerificationResult
1184
+ verifyZKMTag(kind0Event): RootOfTrustVerificationResult
1185
+
1186
+ // Verify all at once
1187
+ verifyAllRootOfTrusts(kind0Event): Promise<AllVerificationResults>
1188
+
1189
+ // Quick checks
1190
+ hasAnyVerifiedRootOfTrust(kind0Event): Promise<boolean>
1191
+ getVerificationSummary(kind0Event): Promise<VerificationSummary>
1192
+
1193
+ // Types
1194
+ interface RootOfTrustVerificationResult {
1195
+ type: 'vlei' | 'eudi' | 'eudil' | 'zkp' | 'zkm';
1196
+ valid: boolean;
1197
+ claims?: any;
1198
+ error?: string;
1199
+ }
1200
+
1201
+ interface AllVerificationResults {
1202
+ vlei?: RootOfTrustVerificationResult;
1203
+ eudi?: RootOfTrustVerificationResult;
1204
+ eudil?: RootOfTrustVerificationResult;
1205
+ zkp?: RootOfTrustVerificationResult;
1206
+ zkm?: RootOfTrustVerificationResult;
1207
+ }
1208
+
1209
+ interface VerificationSummary {
1210
+ hasAnyTrust: boolean;
1211
+ individualVerified: boolean;
1212
+ organizationVerified: boolean;
1213
+ membershipVerified: boolean;
1214
+ details: string[];
1215
+ }
1216
+ ```
1217
+
1218
+ ### IndividualTrustService
1219
+
1220
+ ```typescript
1221
+ class IndividualTrustService {
1222
+ eudi: EUDIService;
1223
+ zkp: ZKPService;
1224
+
1225
+ connectEUDI(config: EUDIVerifierConfig): Promise<void>;
1226
+ connectZKP(config: ZKPVerifierConfig): Promise<void>;
1227
+ disconnect(): Promise<void>;
1228
+ isConnected(): boolean;
1229
+ verifyEUDI(): Promise<EUDIClaims>;
1230
+ verifyZKP(options?: Partial<ZKPVerificationRequest>): Promise<ZKPClaims>;
1231
+ getVerificationStatus(): { eudi: boolean; zkp: boolean };
1232
+ }
1233
+ ```
1234
+
1235
+ ### OrganizationTrustService
1236
+
1237
+ ```typescript
1238
+ class OrganizationTrustService {
1239
+ keria: KERIAService;
1240
+ eudi: EUDIService;
1241
+
1242
+ connectKERIA(url: string, bran: string): Promise<KERIAConnection>;
1243
+ connectEUDI(config: EUDIVerifierConfig): Promise<void>;
1244
+ disconnect(): Promise<void>;
1245
+ isConnected(): boolean;
1246
+ verifyVLEI(): Promise<RootOfTrustInfo>;
1247
+ verifyEUDILegal(): Promise<EUDILegalClaims>;
1248
+ getVerificationStatus(): { vlei: boolean; eudiLegal: boolean };
1249
+ }
1250
+ ```
1251
+
1252
+ ### VerificationStatus
1253
+
1254
+ ```typescript
1255
+ interface VerificationStatus {
1256
+ verified: boolean;
1257
+ individual?: {
1258
+ eudi?: EUDIClaims & { expired?: boolean };
1259
+ zkp?: ZKPClaims & { expired?: boolean };
1260
+ };
1261
+ organization?: {
1262
+ vlei?: RootOfTrustInfo & { expired?: boolean };
1263
+ eudiLegal?: EUDILegalClaims & { expired?: boolean };
1264
+ };
1265
+ }
1266
+ ```
1267
+
1268
+ ### Trust Methods (via AuthService)
1269
+
1270
+ - `setOrganizationTrust(claims): Promise<void>` - Store org trust in KeyInfo
1271
+ - `clearOrganizationTrust(): Promise<void>` - Remove org trust
1272
+ - `setIndividualTrust(claims): Promise<void>` - Store individual trust in KeyInfo
1273
+ - `clearIndividualTrust(): Promise<void>` - Remove individual trust
1274
+ - `getTrustStatus(): VerificationStatus` - Get current verification status
1275
+ - `publishVLEIToProfile(relayService, orgPubkey?): Promise<boolean>`
1276
+ - `publishEUDIToProfile(relayService): Promise<boolean>`
1277
+ - `publishZKPToProfile(relayService): Promise<boolean>`
1278
+ - `publishEUDILegalToProfile(relayService): Promise<boolean>`
1279
+
1280
+ vLEI integrates with FROST multisig for organizational authority:
1281
+
1282
+ ```typescript
1283
+ // Enable multisig with vLEI requirement
1284
+ const result = await authService.enableMultisig({
1285
+ threshold: 2,
1286
+ totalMembers: 3,
1287
+ relays: ['wss://relay.io'],
1288
+ organization: {
1289
+ lei: '549300TLMP5S...',
1290
+ legalName: 'org',
1291
+ },
1292
+ requireVLEI: true, // All signers must have valid vLEI
1293
+ });
1294
+
1295
+ // Signers in 'multisig' mode can be verified against vLEI
1296
+ const signedEvent = await authService.signEvent(event, { mode: 'multisig' });
1297
+ ```
1298
+
1299
+ ## API Reference
1300
+
1301
+ ### IndividualTrustService
1302
+
1303
+ ```typescript
1304
+ class IndividualTrustService {
1305
+ eudi: EUDIService;
1306
+ zkp: ZKPService;
1307
+
1308
+ connectEUDI(config: EUDIVerifierConfig): Promise<void>;
1309
+ connectZKP(config: ZKPVerifierConfig): Promise<void>;
1310
+ disconnect(): Promise<void>;
1311
+ isConnected(): boolean;
1312
+ verifyEUDI(): Promise<EUDIClaims>;
1313
+ verifyZKP(options?: Partial<ZKPVerificationRequest>): Promise<ZKPClaims>;
1314
+ getVerificationStatus(): { eudi: boolean; zkp: boolean };
1315
+ }
1316
+ ```
1317
+
1318
+ ### OrganizationTrustService
1319
+
1320
+ ```typescript
1321
+ class OrganizationTrustService {
1322
+ keria: KERIAService;
1323
+ eudi: EUDIService;
1324
+
1325
+ connectKERIA(url: string, bran: string): Promise<KERIAConnection>;
1326
+ connectEUDI(config: EUDIVerifierConfig): Promise<void>;
1327
+ disconnect(): Promise<void>;
1328
+ isConnected(): boolean;
1329
+ verifyVLEI(): Promise<RootOfTrustInfo>;
1330
+ verifyEUDILegal(): Promise<EUDILegalClaims>;
1331
+ getVerificationStatus(): { vlei: boolean; eudiLegal: boolean };
1332
+ }
1333
+ ```
1334
+
1335
+ ### VerificationStatus
1336
+
1337
+ ```typescript
1338
+ interface VerificationStatus {
1339
+ verified: boolean;
1340
+ individual?: {
1341
+ eudi?: EUDIClaims & { expired?: boolean };
1342
+ zkp?: ZKPClaims & { expired?: boolean };
1343
+ };
1344
+ organization?: {
1345
+ vlei?: RootOfTrustInfo & { expired?: boolean };
1346
+ eudiLegal?: EUDILegalClaims & { expired?: boolean };
1347
+ };
1348
+ }
1349
+ ```
1350
+
1351
+ ### Root of Trust Types
1352
+
1353
+ ```typescript
1354
+ // EUDI Claims
1355
+ interface EUDIClaims {
1356
+ uniqueIdentifier: string; // SHA256 hash of holderDID
1357
+ signature?: string; // Signature linking
1358
+ isOver18?: boolean;
1359
+ nationality?: string;
1360
+ verifiedAt: string; // ISO 8601
1361
+ expiresAt?: string;
1362
+ }
1363
+
1364
+ interface ZKPClaims {
1365
+ uniqueIdentifier: string;
1366
+ proofSaid?: string;
1367
+ firstName?: string;
1368
+ nationality?: string;
1369
+ isOver18?: boolean;
1370
+ isEUCitizen?: boolean;
1371
+ documentType?: string;
1372
+ verifiedAt: string; // ISO 8601
1373
+ expiresAt?: string;
1374
+ }
1375
+
1376
+ interface RootOfTrustInfo {
1377
+ aidPrefix: string;
1378
+ lei: string;
1379
+ legalName: string;
1380
+ role: string;
1381
+ roleCredentialSaid: string;
1382
+ entityCredentialSaid: string;
1383
+ verifiedAt: string; // ISO 8601
1384
+ expiresAt?: string;
1385
+ }
1386
+
1387
+ interface EUDILegalClaims {
1388
+ holderDID: string;
1389
+ legalName: string;
1390
+ lei: string;
1391
+ legalForm?: string;
1392
+ registeredAddress?: string;
1393
+ vatNumber?: string;
1394
+ taxReference?: string;
1395
+ verifiedAt: string; // ISO 8601
1396
+ expiresAt?: string;
1397
+ }
1398
+ ```
1399
+