ns-auth-sdk 1.12.7 → 1.14.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 +1089 -1
- package/dist/_esm-C7FRLTj2.cjs +9030 -0
- package/dist/_esm-C7FRLTj2.cjs.map +1 -0
- package/dist/_esm-D_oMW5T5.mjs +9028 -0
- package/dist/_esm-D_oMW5T5.mjs.map +1 -0
- package/dist/base64-js-B8y7dH5k.mjs +93 -0
- package/dist/base64-js-B8y7dH5k.mjs.map +1 -0
- package/dist/base64-js-Tm-kCeud.cjs +98 -0
- package/dist/base64-js-Tm-kCeud.cjs.map +1 -0
- package/dist/ccip-157tdxqP.cjs +7947 -0
- package/dist/ccip-157tdxqP.cjs.map +1 -0
- package/dist/ccip-BEMMdVyd.cjs +4 -0
- package/dist/ccip-BSmCdJ3K.mjs +3 -0
- package/dist/ccip-okqXkslP.mjs +6567 -0
- package/dist/ccip-okqXkslP.mjs.map +1 -0
- package/dist/chains-CTeD2UcS.mjs +33 -0
- package/dist/chains-CTeD2UcS.mjs.map +1 -0
- package/dist/chains-Du-lv_5i.cjs +33 -0
- package/dist/chains-Du-lv_5i.cjs.map +1 -0
- package/dist/chunk-Bnu9O96Y.cjs +60 -0
- package/dist/chunk-CVYhg9ik.mjs +45 -0
- package/dist/cjs-CSUAVztq.cjs +8865 -0
- package/dist/cjs-CSUAVztq.cjs.map +1 -0
- package/dist/cjs-sm5h7qxv.mjs +8860 -0
- package/dist/cjs-sm5h7qxv.mjs.map +1 -0
- package/dist/defineChain-7QG67hYU.cjs +30 -0
- package/dist/defineChain-7QG67hYU.cjs.map +1 -0
- package/dist/defineChain-DBem8ZQY.mjs +24 -0
- package/dist/defineChain-DBem8ZQY.mjs.map +1 -0
- package/dist/dist-BPmSxkxc.cjs +10676 -0
- package/dist/dist-BPmSxkxc.cjs.map +1 -0
- package/dist/dist-BSjH4t6s.cjs +12932 -0
- package/dist/dist-BSjH4t6s.cjs.map +1 -0
- package/dist/dist-C-KjTK4Q.cjs +447 -0
- package/dist/dist-C-KjTK4Q.cjs.map +1 -0
- package/dist/dist-C2h97xM-.mjs +355 -0
- package/dist/dist-C2h97xM-.mjs.map +1 -0
- package/dist/dist-CT7grDWb.mjs +12920 -0
- package/dist/dist-CT7grDWb.mjs.map +1 -0
- package/dist/dist-D7fRmK6G.mjs +10632 -0
- package/dist/dist-D7fRmK6G.mjs.map +1 -0
- package/dist/dist-DlmcyFmM.mjs +16951 -0
- package/dist/dist-DlmcyFmM.mjs.map +1 -0
- package/dist/dist-O0uZr5OF.cjs +17122 -0
- package/dist/dist-O0uZr5OF.cjs.map +1 -0
- package/dist/echo-BB-JgAYZ.cjs +339 -0
- package/dist/echo-BB-JgAYZ.cjs.map +1 -0
- package/dist/echo-Bwy4_Cvh.mjs +316 -0
- package/dist/echo-Bwy4_Cvh.mjs.map +1 -0
- package/dist/echo-D6X2IuNW.cjs +9 -0
- package/dist/echo-DvfG_heb.mjs +8 -0
- package/dist/esm-BcHKCX5i.mjs +569 -0
- package/dist/esm-BcHKCX5i.mjs.map +1 -0
- package/dist/esm-Bh_YwFIz.cjs +599 -0
- package/dist/esm-Bh_YwFIz.cjs.map +1 -0
- package/dist/esm-C0XO9TQm.cjs +30449 -0
- package/dist/esm-C0XO9TQm.cjs.map +1 -0
- package/dist/esm-vMUVj9k4.mjs +30440 -0
- package/dist/esm-vMUVj9k4.mjs.map +1 -0
- package/dist/group-coordination-BlFpBVpn.mjs +49 -0
- package/dist/group-coordination-BlFpBVpn.mjs.map +1 -0
- package/dist/group-coordination-Cf18OjZt.cjs +4 -0
- package/dist/group-coordination-ImuoJEoy.mjs +3 -0
- package/dist/group-coordination-LGIyipaX.cjs +73 -0
- package/dist/group-coordination-LGIyipaX.cjs.map +1 -0
- package/dist/index.cjs +4127 -404
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -96
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1461 -96
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4062 -369
- package/dist/index.mjs.map +1 -1
- package/dist/keyset-CKMQXvsb.mjs +172 -0
- package/dist/keyset-CKMQXvsb.mjs.map +1 -0
- package/dist/keyset-Cxdgu110.cjs +237 -0
- package/dist/keyset-Cxdgu110.cjs.map +1 -0
- package/dist/keyset-DLxpGhdu.cjs +6 -0
- package/dist/keyset-IKjlhvqF.mjs +4 -0
- package/dist/node-3EUJ4ga9.cjs +9 -0
- package/dist/node-C2UpE11T.cjs +444 -0
- package/dist/node-C2UpE11T.cjs.map +1 -0
- package/dist/node-CHdpTQdN.mjs +8679 -0
- package/dist/node-CHdpTQdN.mjs.map +1 -0
- package/dist/node-DQt1CVGl.mjs +6 -0
- package/dist/node-Dy8ww1LG.cjs +8680 -0
- package/dist/node-Dy8ww1LG.cjs.map +1 -0
- package/dist/node-IX55IH6z.mjs +397 -0
- package/dist/node-IX55IH6z.mjs.map +1 -0
- package/dist/nostr-BTOpBN_5.cjs +11 -0
- package/dist/nostr-D4E52XRU.mjs +224 -0
- package/dist/nostr-D4E52XRU.mjs.map +1 -0
- package/dist/nostr-Hv2tsnuI.cjs +272 -0
- package/dist/nostr-Hv2tsnuI.cjs.map +1 -0
- package/dist/nostr-ZwJe_DlZ.mjs +4 -0
- package/dist/pako.esm-DTVnlCJh.cjs +3856 -0
- package/dist/pako.esm-DTVnlCJh.cjs.map +1 -0
- package/dist/pako.esm-ORhkEHM2.mjs +3838 -0
- package/dist/pako.esm-ORhkEHM2.mjs.map +1 -0
- package/dist/peer-B9g3OQ5D.cjs +18 -0
- package/dist/peer-CptDj7zu.mjs +745 -0
- package/dist/peer-CptDj7zu.mjs.map +1 -0
- package/dist/peer-CtqL0yiE.mjs +9 -0
- package/dist/peer-XrPL0O6z.cjs +822 -0
- package/dist/peer-XrPL0O6z.cjs.map +1 -0
- package/dist/ping-BXKREIdI.mjs +7 -0
- package/dist/ping-BtVhKocl.cjs +379 -0
- package/dist/ping-BtVhKocl.cjs.map +1 -0
- package/dist/ping-D5fpMhGC.cjs +11 -0
- package/dist/ping-DOD50kW_.mjs +332 -0
- package/dist/ping-DOD50kW_.mjs.map +1 -0
- package/dist/policy-8HcjulLD.cjs +293 -0
- package/dist/policy-8HcjulLD.cjs.map +1 -0
- package/dist/policy-BA6MEOBY.mjs +5 -0
- package/dist/policy-BFNdXvmM.cjs +11 -0
- package/dist/policy-D_nFHHjo.mjs +228 -0
- package/dist/policy-D_nFHHjo.mjs.map +1 -0
- package/dist/prf-handler-BNiyCQMt.cjs +3 -0
- package/dist/prf-handler-D3EqUNWe.cjs +103 -0
- package/dist/prf-handler-D3EqUNWe.cjs.map +1 -0
- package/dist/prf-handler-Db8CsoIP.mjs +85 -0
- package/dist/prf-handler-Db8CsoIP.mjs.map +1 -0
- package/dist/prf-handler-Dly_WZGn.mjs +3 -0
- package/dist/src-CVfTUJQl.mjs +822 -0
- package/dist/src-CVfTUJQl.mjs.map +1 -0
- package/dist/src-D5S86Xpf.cjs +827 -0
- package/dist/src-D5S86Xpf.cjs.map +1 -0
- package/dist/types-B-TLIS13.cjs +212 -0
- package/dist/types-B-TLIS13.cjs.map +1 -0
- package/dist/types-CTm_FHYD.mjs +111 -0
- package/dist/types-CTm_FHYD.mjs.map +1 -0
- package/dist/validation-1xwWVXqE.mjs +3 -0
- package/dist/validation-DnpurN79.cjs +405 -0
- package/dist/validation-DnpurN79.cjs.map +1 -0
- package/dist/validation-IsAUvsvy.mjs +334 -0
- package/dist/validation-IsAUvsvy.mjs.map +1 -0
- package/dist/validation-yCvZCqIs.cjs +6 -0
- package/dist/wrapper-C5NpyacC.cjs +3552 -0
- package/dist/wrapper-C5NpyacC.cjs.map +1 -0
- package/dist/wrapper-CHq_CV4J.mjs +3552 -0
- package/dist/wrapper-CHq_CV4J.mjs.map +1 -0
- package/dist/zkm.service-B24N_0FB.mjs +115 -0
- package/dist/zkm.service-B24N_0FB.mjs.map +1 -0
- package/dist/zkm.service-BrXyI4BS.mjs +3 -0
- package/dist/zkm.service-BvQ01wSH.cjs +3 -0
- package/dist/zkm.service-DfSPbuOl.cjs +139 -0
- package/dist/zkm.service-DfSPbuOl.cjs.map +1 -0
- package/package.json +13 -4
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: '
|
|
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 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 (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
|
+
|