ns-auth-sdk 1.10.0 → 1.12.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
@@ -1,4 +1,4 @@
1
- # NS Auth SDK
1
+ # Stoic Identity
2
2
 
3
3
  _The simplest way of doing Auth with seamless and decentralized Key-Management for SSO, Authentication, Membership, and Profile-Management_
4
4
 
@@ -75,49 +75,32 @@ relayService.initialize(eventStore);
75
75
  // Create a passkey (triggers biometric)
76
76
  const credentialId = await authService.createPasskey('user@example.com');
77
77
 
78
- // Create Nostr key - password required only if PRF unavailable
79
- const prfSupported = await authService.checkPRFSupport();
80
- const keyInfo = prfSupported
81
- ? await authService.createNostrKey(credentialId)
82
- : await authService.createNostrKey(credentialId, userPassword);
78
+ // Create Key
79
+ const keyInfo = await authService.createKey(credentialId, undefined, {
80
+ username: 'user@example.com',
81
+ recoveryPassword: 'my-recovery-password', // optional
82
+ });
83
83
 
84
84
  // Store keyInfo for later use
85
85
  authService.setCurrentKeyInfo(keyInfo);
86
86
  ```
87
87
 
88
- ### 3. Sign Events
89
-
90
- ```typescript
91
- const event = {
92
- kind: 1,
93
- content: 'Hello Nostr!',
94
- tags: [],
95
- created_at: Math.floor(Date.now() / 1000),
96
- };
97
-
98
- // Sign - no password needed if key was cached at creation
99
- const signed = await authService.signEvent(event);
100
- ```
101
-
102
- ### Password Fallback
88
+ ### Password Fallback (when PRF unavailable)
103
89
 
104
- When PRF is not supported by the browser or device, the SDK automatically uses password encryption. The password is required only at key creation time - subsequent signs use the cached key:
90
+ When PRF is not supported, username is required and password is used to derive the key:
105
91
 
106
92
  ```typescript
107
- import { AuthService, checkPRFSupport } from 'ns-auth-sdk';
108
-
109
93
  // Check if password fallback is needed
110
- const prfSupported = await checkPRFSupport();
94
+ const prfSupported = await authService.checkPRFSupport();
111
95
 
112
96
  if (!prfSupported) {
113
- const password = await promptUserForPassword();
97
+ // Username is REQUIRED when PRF unavailable
98
+ const keyInfo = await authService.createKey(undefined, userPassword, {
99
+ username: 'user@example.com',
100
+ });
114
101
  }
115
102
 
116
- // Create key - password required only when PRF unavailable
117
- const keyInfo = await authService.createNostrKey(credentialId, password);
118
-
119
- // First sign: password used to decrypt and cache key
120
- // Subsequent signs: uses cached key (no password needed)
103
+ // Sign events
121
104
  const signedEvent = await authService.signEvent(event);
122
105
  ```
123
106
 
@@ -126,36 +109,66 @@ const signedEvent = await authService.signEvent(event);
126
109
  #### Methods
127
110
 
128
111
  - `createPasskey(username?: string): Promise<Uint8Array>` - Create a new passkey
129
- - `createKey(credentialId?: Uint8Array, password?: string): Promise<KeyInfo>` - Create key from passkey (auto-detects PRF support, uses password fallback if needed)
112
+ - `createKey(credentialId?: Uint8Array, password?: string, options?: KeyOptions): Promise<KeyInfo>` - Create key from passkey (auto-detects PRF support, uses password fallback if needed)
130
113
  - `getPublicKey(): Promise<string>` - Get current public key
131
- - `signEvent(event: Event, options?: SignOptions): Promise<Event>` - Sign an event (password optional - only needed first time before key is cached)
114
+ - `signEvent(event: Event): Promise<Event>` - Sign an event
132
115
  - `getCurrentKeyInfo(): KeyInfo | null` - Get current key info
133
116
  - `setCurrentKeyInfo(keyInfo: KeyInfo): void` - Set current key info
134
117
  - `hasKeyInfo(): boolean` - Check if key info exists
135
118
  - `clearStoredKeyInfo(): void` - Clear stored key info
136
- - `checkPRFSupport(): Promise<boolean>` - Check if PRF is supported (returns false if password fallback needed)
119
+ - `checkPRFSupport(): Promise<boolean>` - Check if PRF is supported
120
+ - `deriveSaltFromUsername(username?: string): Promise<string>` - Derive salt from username (SHA-256)
137
121
 
138
- #### Types
122
+ #### Recovery Methods
123
+
124
+ - `addPasswordRecovery(password: string): Promise<KeyInfo>` - Add password recovery to an existing PRF key
125
+ - `activateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo>` - Recover using password with a new passkey credential ID from a new device
126
+ - `getRecoveryForKind0(): RecoveryData | null` - Get recovery data for publishing to kind-0
127
+ - `parseRecoveryTag(tags: string[][]): KeyRecovery | null` - Parse recovery tag from event
128
+ - `verifyRecoverySignature(kind0: Event): Promise<boolean>` - Verify recovery signature (async)
129
+
130
+ #### KeyOptions
139
131
 
140
132
  ```typescript
141
- // Password-protected key bundle (used when PRF unavailable)
142
- interface PasswordProtectedBundle {
143
- publicKeySpkiBase64: string;
144
- wrappedPrivateKeyBase64: string;
145
- saltBase64: string;
146
- ivBase64: string;
133
+ interface KeyOptions {
134
+ username?: string; // Required when PRF unavailable
135
+ password?: string; // Required when PRF unavailable
136
+ recoveryPassword?: string; // Password for recovery (optional)
147
137
  }
138
+ ```
139
+
140
+ #### RecoveryData
141
+
142
+ ```typescript
143
+ interface RecoveryData {
144
+ recoveryPubkey: string;
145
+ recoverySalt: string;
146
+ createdAt?: number;
147
+ signature?: string; // Schnorr signature from recovery key signing the current pubkey
148
+ }
149
+ ```
150
+
151
+ #### Types
148
152
 
149
- // KeyInfo with optional password fallback
153
+ ```typescript
154
+ // KeyInfo with optional recovery
150
155
  interface KeyInfo {
151
156
  credentialId: string;
152
157
  pubkey: string;
153
158
  salt: string;
154
159
  username?: string;
155
- passwordProtectedBundle?: PasswordProtectedBundle;
160
+ recovery?: KeyRecovery;
161
+ }
162
+
163
+ // Key recovery configuration
164
+ interface KeyRecovery {
165
+ recoveryPubkey: string;
166
+ recoverySalt: string;
167
+ createdAt?: number;
168
+ signature?: string; // Schnorr signature from recovery key signing the current pubkey
156
169
  }
157
170
 
158
- // Sign options with password support
171
+ // Sign options
159
172
  interface SignOptions {
160
173
  clearMemory?: boolean;
161
174
  tags?: string[][];
@@ -163,6 +176,48 @@ interface SignOptions {
163
176
  }
164
177
  ```
165
178
 
179
+ ### Recovery Flow
180
+
181
+ The SDK supports password-based recovery for passkey-protected keys. When creating a key, you can optionally provide a recovery password:
182
+
183
+ ```typescript
184
+ // Create key with recovery enabled
185
+ const keyInfo = await authService.createKey(credentialId, undefined, {
186
+ username: 'user@example.com',
187
+ recoveryPassword: 'my-recovery-password',
188
+ });
189
+
190
+ // The recovery data is stored in kind-0 tags:
191
+ // ["r", recoveryPubkey, recoverySalt, createdAt, signature]
192
+ ```
193
+
194
+ **Recovery on a new device:**
195
+
196
+ ```typescript
197
+ // On new device - create new passkey first
198
+ const newCredentialId = await authService.createPasskey('user@example.com');
199
+
200
+ // Recover using password
201
+ const keyInfo = await authService.activateWithPassword('my-recovery-password', newCredentialId);
202
+ ```
203
+
204
+ **Verification:**
205
+
206
+ Anyone can verify ownership by fetching the kind-0 and checking the signature:
207
+
208
+ ```typescript
209
+ import { parseRecoveryTag, verifyRecoverySignature } from 'ns-auth-sdk';
210
+
211
+ // Fetch kind-0 from relay
212
+ const kind0 = await relayService.fetchProfile(pubkey);
213
+
214
+ // Verify recovery signature (async)
215
+ const isValid = await verifyRecoverySignature(kind0);
216
+ if (isValid) {
217
+ console.log('Recovery key holder controls this identity');
218
+ }
219
+ ```
220
+
166
221
  #### Configuration Options
167
222
 
168
223
  ```typescript
package/dist/index.cjs CHANGED
@@ -1,4 +1,33 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
1
28
  let applesauce_core_helpers = require("applesauce-core/helpers");
29
+ let _noble_secp256k1 = require("@noble/secp256k1");
30
+ _noble_secp256k1 = __toESM(_noble_secp256k1);
2
31
 
3
32
  //#region src/utils/utils.ts
4
33
  /**
@@ -391,7 +420,55 @@ async function getPublicKeyFromPassword(password, salt = DEFAULT_SALT) {
391
420
  * Nosskey class for Passkey-Derived Nostr Identity
392
421
  * @packageDocumentation
393
422
  */
394
- const STANDARD_SALT = "6e6f7374722d6b6579";
423
+ async function deriveSaltFromUsername(username) {
424
+ if (!username) return "";
425
+ const msgBuffer = new TextEncoder().encode(username.toLowerCase().trim());
426
+ const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
427
+ return bytesToHex(new Uint8Array(hashBuffer));
428
+ }
429
+ function parseRecoveryTag(tags) {
430
+ const recoveryTag = tags.find((tag) => tag[0] === "r");
431
+ if (!recoveryTag || recoveryTag.length < 3) return null;
432
+ const createdAt = recoveryTag[3] ? parseInt(recoveryTag[3], 10) : void 0;
433
+ return {
434
+ recoveryPubkey: recoveryTag[1],
435
+ recoverySalt: recoveryTag[2],
436
+ createdAt: createdAt || void 0,
437
+ signature: recoveryTag[4] || void 0
438
+ };
439
+ }
440
+ function createRecoveryTag(recovery) {
441
+ return [
442
+ "r",
443
+ recovery.recoveryPubkey,
444
+ recovery.recoverySalt,
445
+ recovery.createdAt?.toString() || "",
446
+ recovery.signature || ""
447
+ ];
448
+ }
449
+ function getRecoverySignature(kind0) {
450
+ if (!parseRecoveryTag(kind0.tags || [])) return null;
451
+ const tag = kind0.tags?.find((t) => t[0] === "r");
452
+ return tag && tag.length > 4 ? tag[4] || null : null;
453
+ }
454
+ async function verifyRecoverySignature(kind0) {
455
+ try {
456
+ if (!parseRecoveryTag(kind0.tags || [])) return false;
457
+ const signature = getRecoverySignature(kind0);
458
+ if (!signature || !kind0.pubkey) return false;
459
+ const messageHash = await sha256(kind0.pubkey);
460
+ const signatureBytes = hexToBytes(signature);
461
+ const pubkeyBytes = hexToBytes(kind0.pubkey);
462
+ return _noble_secp256k1.verify(signatureBytes, messageHash, pubkeyBytes);
463
+ } catch (e) {
464
+ return false;
465
+ }
466
+ }
467
+ async function sha256(message) {
468
+ const msgBuffer = new TextEncoder().encode(message);
469
+ const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
470
+ return new Uint8Array(hashBuffer);
471
+ }
395
472
  /**
396
473
  * Nosskey - Passkey-Derived Nostr Keys
397
474
  */
@@ -576,11 +653,17 @@ var NosskeyManager = class {
576
653
  * @param options
577
654
  */
578
655
  async createKey(credentialId, password, options = {}) {
579
- if (await this.checkPRFSupport()) return this.createPrfNostrKey(credentialId, options);
580
- else {
656
+ const prfSupported = await this.checkPRFSupport();
657
+ let keyInfo;
658
+ if (prfSupported) {
659
+ keyInfo = await this.createPrfNostrKey(credentialId, options);
660
+ if (options.recoveryPassword) keyInfo = await this.addPasswordRecovery(options.recoveryPassword, credentialId);
661
+ } else {
581
662
  if (!password) throw new Error("Password is required when PRF is not supported");
582
- return this.createPasswordProtectedNostrKey(password, options);
663
+ if (!options.username) throw new Error("Username is required when PRF is not supported");
664
+ keyInfo = await this.createPasswordProtectedNostrKey(password, options);
583
665
  }
666
+ return keyInfo;
584
667
  }
585
668
  /**
586
669
  * Create Nostr key using PRF (standard passkey flow)
@@ -590,10 +673,11 @@ var NosskeyManager = class {
590
673
  if (sk.every((byte) => byte === 0)) throw new Error("Invalid PRF output: all zeros");
591
674
  bytesToHex(sk);
592
675
  const publicKey = (0, applesauce_core_helpers.getPublicKey)(sk);
676
+ const salt = await deriveSaltFromUsername(options.username);
593
677
  const keyInfo = {
594
678
  credentialId: bytesToHex(credentialId || responseId),
595
679
  pubkey: publicKey,
596
- salt: STANDARD_SALT,
680
+ salt,
597
681
  ...options.username && { username: options.username }
598
682
  };
599
683
  if (this.#keyCache.isEnabled() && this.#keyCache.getCacheOptions().cacheOnCreation) this.#keyCache.setKey(keyInfo.credentialId, sk);
@@ -605,11 +689,12 @@ var NosskeyManager = class {
605
689
  * @param options
606
690
  */
607
691
  async createPasswordProtectedNostrKey(password, options = {}) {
608
- const pubkey = await getPublicKeyFromPassword(password, STANDARD_SALT);
692
+ const salt = await deriveSaltFromUsername(options.username);
693
+ const pubkey = await getPublicKeyFromPassword(password, salt);
609
694
  return {
610
695
  credentialId: bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16))),
611
696
  pubkey,
612
- salt: STANDARD_SALT,
697
+ salt,
613
698
  ...options.username && { username: options.username }
614
699
  };
615
700
  }
@@ -681,6 +766,90 @@ var NosskeyManager = class {
681
766
  return isPrfSupported();
682
767
  }
683
768
  /**
769
+ * Add password recovery to an existing PRF key
770
+ * @param password Password for recovery key
771
+ * @param currentCredentialId Current passkey credential ID
772
+ */
773
+ async addPasswordRecovery(password, currentCredentialId) {
774
+ const keyInfo = this.getCurrentKeyInfo();
775
+ if (!keyInfo) throw new Error("No current KeyInfo set");
776
+ if (keyInfo.passwordProtectedBundle) throw new Error("Password recovery already exists for password-derived key");
777
+ if (keyInfo.recovery) throw new Error("Recovery already configured");
778
+ if (!(currentCredentialId || (keyInfo.credentialId ? hexToBytes(keyInfo.credentialId) : void 0))) throw new Error("Credential ID required");
779
+ const recoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
780
+ const recoveryPubkey = await getPublicKeyFromPassword(password, recoverySalt);
781
+ const recoverySk = await deriveNostrPrivateKey(password, recoverySalt);
782
+ const signature = this.#signWithKey(recoverySk, keyInfo.pubkey);
783
+ this.#clearKey(recoverySk);
784
+ const updatedKeyInfo = {
785
+ ...keyInfo,
786
+ recovery: {
787
+ recoveryPubkey,
788
+ recoverySalt,
789
+ createdAt: Date.now(),
790
+ signature
791
+ }
792
+ };
793
+ this.setCurrentKeyInfo(updatedKeyInfo);
794
+ return updatedKeyInfo;
795
+ }
796
+ #signWithKey(sk, message) {
797
+ return (0, applesauce_core_helpers.finalizeEvent)({
798
+ kind: 0,
799
+ content: "",
800
+ tags: [],
801
+ created_at: Math.floor(Date.now() / 1e3)
802
+ }, sk).sig;
803
+ }
804
+ /**
805
+ * Activate recovery using password
806
+ * Requires new credential ID from new device
807
+ * @param password Password for recovery key
808
+ * @param newCredentialId New passkey credential ID (required)
809
+ */
810
+ async activateWithPassword(password, newCredentialId) {
811
+ const keyInfo = this.getCurrentKeyInfo();
812
+ if (!keyInfo) throw new Error("No current KeyInfo set");
813
+ if (!keyInfo.recovery) throw new Error("No recovery key configured");
814
+ if (!newCredentialId) throw new Error("New credential ID is required for recovery");
815
+ const { recoveryPubkey, recoverySalt } = keyInfo.recovery;
816
+ if (await getPublicKeyFromPassword(password, recoverySalt) !== recoveryPubkey) throw new Error("Invalid recovery password");
817
+ const { secret: newSk } = await getPrfSecret(newCredentialId, this.#prfOptions);
818
+ const newPubkey = (0, applesauce_core_helpers.getPublicKey)(newSk);
819
+ this.#clearKey(newSk);
820
+ const newRecoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
821
+ const newRecoveryPubkey = await getPublicKeyFromPassword(password, newRecoverySalt);
822
+ const recoverySk = await deriveNostrPrivateKey(password, recoverySalt);
823
+ const signature = this.#signWithKey(recoverySk, newPubkey);
824
+ this.#clearKey(recoverySk);
825
+ const updatedKeyInfo = {
826
+ credentialId: bytesToHex(newCredentialId),
827
+ pubkey: newPubkey,
828
+ salt: keyInfo.salt,
829
+ recovery: {
830
+ recoveryPubkey: newRecoveryPubkey,
831
+ recoverySalt: newRecoverySalt,
832
+ createdAt: Date.now(),
833
+ signature
834
+ }
835
+ };
836
+ this.setCurrentKeyInfo(updatedKeyInfo);
837
+ return updatedKeyInfo;
838
+ }
839
+ /**
840
+ * Get the recovery data for publishing to kind-0
841
+ */
842
+ getRecoveryForKind0() {
843
+ const keyInfo = this.getCurrentKeyInfo();
844
+ if (!keyInfo || !keyInfo.recovery) return null;
845
+ return {
846
+ recoveryPubkey: keyInfo.recovery.recoveryPubkey,
847
+ recoverySalt: keyInfo.recovery.recoverySalt,
848
+ createdAt: keyInfo.recovery.createdAt,
849
+ signature: keyInfo.recovery.signature
850
+ };
851
+ }
852
+ /**
684
853
  * @param key
685
854
  */
686
855
  #clearKey(key) {
@@ -852,9 +1021,13 @@ var AuthService = class {
852
1021
  /**
853
1022
  * Create a new Nostr key from a credential ID
854
1023
  * Automatically uses password fallback if PRF is not supported
1024
+ * @param credentialId Passkey credential ID
1025
+ * @param password Password (required if PRF not supported)
1026
+ * @param options.username Username for the key
1027
+ * @param options.recoveryPassword Password for recovery (enables recovery on new device)
855
1028
  */
856
- async createKey(credentialId, password) {
857
- return await this.getManager().createKey(credentialId, password);
1029
+ async createKey(credentialId, password, options) {
1030
+ return await this.getManager().createKey(credentialId, password, options);
858
1031
  }
859
1032
  /**
860
1033
  * Check if PRF is supported, otherwise password fallback is needed
@@ -904,6 +1077,28 @@ var AuthService = class {
904
1077
  async isPrfSupported() {
905
1078
  return this.checkPRFSupport();
906
1079
  }
1080
+ /**
1081
+ * Add password recovery to an existing PRF key
1082
+ * @param password Password for recovery key
1083
+ */
1084
+ async addPasswordRecovery(password) {
1085
+ return await this.getManager().addPasswordRecovery(password);
1086
+ }
1087
+ /**
1088
+ * Activate recovery using password
1089
+ * Requires new credential ID from new device
1090
+ * @param password Password for recovery key
1091
+ * @param newCredentialId New passkey credential ID (required)
1092
+ */
1093
+ async activateWithPassword(password, newCredentialId) {
1094
+ return await this.getManager().activateWithPassword(password, newCredentialId);
1095
+ }
1096
+ /**
1097
+ * Get recovery data for kind-0
1098
+ */
1099
+ getRecoveryForKind0() {
1100
+ return this.getManager().getRecoveryForKind0();
1101
+ }
907
1102
  };
908
1103
 
909
1104
  //#endregion
@@ -1297,14 +1492,21 @@ exports.aesGcmEncrypt = aesGcmEncrypt;
1297
1492
  exports.bytesToHex = bytesToHex;
1298
1493
  exports.checkPRFSupport = checkPRFSupport;
1299
1494
  exports.createPasskey = createPasskey;
1495
+ exports.createRecoveryTag = createRecoveryTag;
1300
1496
  exports.deriveAesGcmKey = deriveAesGcmKey;
1497
+ exports.deriveNostrPrivateKey = deriveNostrPrivateKey;
1498
+ exports.deriveSaltFromUsername = deriveSaltFromUsername;
1301
1499
  exports.generatePasswordProtectedKey = generatePasswordProtectedKey;
1302
1500
  exports.getPrfSecret = getPrfSecret;
1501
+ exports.getPublicKeyFromPassword = getPublicKeyFromPassword;
1502
+ exports.getRecoverySignature = getRecoverySignature;
1303
1503
  exports.hexToBytes = hexToBytes;
1304
1504
  exports.importPublicKeyFromBundle = importPublicKeyFromBundle;
1305
1505
  exports.isPrfSupported = isPrfSupported;
1506
+ exports.parseRecoveryTag = parseRecoveryTag;
1306
1507
  exports.registerDummyPasskey = registerDummyPasskey;
1307
1508
  exports.unwrapPasswordProtectedPrivateKey = unwrapPasswordProtectedPrivateKey;
1509
+ exports.verifyRecoverySignature = verifyRecoverySignature;
1308
1510
  var applesauce_core = require("applesauce-core");
1309
1511
  Object.keys(applesauce_core).forEach(function (k) {
1310
1512
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {