ns-auth-sdk 1.11.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
 
@@ -134,20 +117,23 @@ const signedEvent = await authService.signEvent(event);
134
117
  - `hasKeyInfo(): boolean` - Check if key info exists
135
118
  - `clearStoredKeyInfo(): void` - Clear stored key info
136
119
  - `checkPRFSupport(): Promise<boolean>` - Check if PRF is supported
120
+ - `deriveSaltFromUsername(username?: string): Promise<string>` - Derive salt from username (SHA-256)
137
121
 
138
122
  #### Recovery Methods
139
123
 
140
124
  - `addPasswordRecovery(password: string): Promise<KeyInfo>` - Add password recovery to an existing PRF key
141
125
  - `activateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo>` - Recover using password with a new passkey credential ID from a new device
142
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)
143
129
 
144
130
  #### KeyOptions
145
131
 
146
132
  ```typescript
147
133
  interface KeyOptions {
148
- username?: string;
149
- password?: string;
150
- recoveryPassword?: string; // Password for recovery - enables recovery on new device
134
+ username?: string; // Required when PRF unavailable
135
+ password?: string; // Required when PRF unavailable
136
+ recoveryPassword?: string; // Password for recovery (optional)
151
137
  }
152
138
  ```
153
139
 
@@ -158,6 +144,7 @@ interface RecoveryData {
158
144
  recoveryPubkey: string;
159
145
  recoverySalt: string;
160
146
  createdAt?: number;
147
+ signature?: string; // Schnorr signature from recovery key signing the current pubkey
161
148
  }
162
149
  ```
163
150
 
@@ -178,17 +165,7 @@ interface KeyRecovery {
178
165
  recoveryPubkey: string;
179
166
  recoverySalt: string;
180
167
  createdAt?: number;
181
- }
182
- interface KeyRecovery {
183
- recoveryPubkey: string;
184
- recoverySalt: string;
185
- nextSalt?: string;
186
- nextRecoverySalt?: string;
187
- nextPubkey?: string;
188
- nextCredentialId?: string;
189
- rotatedAt?: number;
190
- rotatedBy?: 'passkey' | 'password';
191
- activatedAt?: number;
168
+ signature?: string; // Schnorr signature from recovery key signing the current pubkey
192
169
  }
193
170
 
194
171
  // Sign options
@@ -211,7 +188,7 @@ const keyInfo = await authService.createKey(credentialId, undefined, {
211
188
  });
212
189
 
213
190
  // The recovery data is stored in kind-0 tags:
214
- // ["r", recoveryPubkey, recoverySalt, createdAt]
191
+ // ["r", recoveryPubkey, recoverySalt, createdAt, signature]
215
192
  ```
216
193
 
217
194
  **Recovery on a new device:**
@@ -224,6 +201,23 @@ const newCredentialId = await authService.createPasskey('user@example.com');
224
201
  const keyInfo = await authService.activateWithPassword('my-recovery-password', newCredentialId);
225
202
  ```
226
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
+
227
221
  #### Configuration Options
228
222
 
229
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,12 @@ 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
+ }
395
429
  function parseRecoveryTag(tags) {
396
430
  const recoveryTag = tags.find((tag) => tag[0] === "r");
397
431
  if (!recoveryTag || recoveryTag.length < 3) return null;
@@ -399,7 +433,8 @@ function parseRecoveryTag(tags) {
399
433
  return {
400
434
  recoveryPubkey: recoveryTag[1],
401
435
  recoverySalt: recoveryTag[2],
402
- createdAt: createdAt || void 0
436
+ createdAt: createdAt || void 0,
437
+ signature: recoveryTag[4] || void 0
403
438
  };
404
439
  }
405
440
  function createRecoveryTag(recovery) {
@@ -407,9 +442,33 @@ function createRecoveryTag(recovery) {
407
442
  "r",
408
443
  recovery.recoveryPubkey,
409
444
  recovery.recoverySalt,
410
- recovery.createdAt?.toString() || ""
445
+ recovery.createdAt?.toString() || "",
446
+ recovery.signature || ""
411
447
  ];
412
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
+ }
413
472
  /**
414
473
  * Nosskey - Passkey-Derived Nostr Keys
415
474
  */
@@ -601,6 +660,7 @@ var NosskeyManager = class {
601
660
  if (options.recoveryPassword) keyInfo = await this.addPasswordRecovery(options.recoveryPassword, credentialId);
602
661
  } else {
603
662
  if (!password) throw new Error("Password is required when PRF is not supported");
663
+ if (!options.username) throw new Error("Username is required when PRF is not supported");
604
664
  keyInfo = await this.createPasswordProtectedNostrKey(password, options);
605
665
  }
606
666
  return keyInfo;
@@ -613,10 +673,11 @@ var NosskeyManager = class {
613
673
  if (sk.every((byte) => byte === 0)) throw new Error("Invalid PRF output: all zeros");
614
674
  bytesToHex(sk);
615
675
  const publicKey = (0, applesauce_core_helpers.getPublicKey)(sk);
676
+ const salt = await deriveSaltFromUsername(options.username);
616
677
  const keyInfo = {
617
678
  credentialId: bytesToHex(credentialId || responseId),
618
679
  pubkey: publicKey,
619
- salt: STANDARD_SALT,
680
+ salt,
620
681
  ...options.username && { username: options.username }
621
682
  };
622
683
  if (this.#keyCache.isEnabled() && this.#keyCache.getCacheOptions().cacheOnCreation) this.#keyCache.setKey(keyInfo.credentialId, sk);
@@ -628,11 +689,12 @@ var NosskeyManager = class {
628
689
  * @param options
629
690
  */
630
691
  async createPasswordProtectedNostrKey(password, options = {}) {
631
- const pubkey = await getPublicKeyFromPassword(password, STANDARD_SALT);
692
+ const salt = await deriveSaltFromUsername(options.username);
693
+ const pubkey = await getPublicKeyFromPassword(password, salt);
632
694
  return {
633
695
  credentialId: bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16))),
634
696
  pubkey,
635
- salt: STANDARD_SALT,
697
+ salt,
636
698
  ...options.username && { username: options.username }
637
699
  };
638
700
  }
@@ -716,17 +778,29 @@ var NosskeyManager = class {
716
778
  if (!(currentCredentialId || (keyInfo.credentialId ? hexToBytes(keyInfo.credentialId) : void 0))) throw new Error("Credential ID required");
717
779
  const recoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
718
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);
719
784
  const updatedKeyInfo = {
720
785
  ...keyInfo,
721
786
  recovery: {
722
787
  recoveryPubkey,
723
788
  recoverySalt,
724
- createdAt: Date.now()
789
+ createdAt: Date.now(),
790
+ signature
725
791
  }
726
792
  };
727
793
  this.setCurrentKeyInfo(updatedKeyInfo);
728
794
  return updatedKeyInfo;
729
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
+ }
730
804
  /**
731
805
  * Activate recovery using password
732
806
  * Requires new credential ID from new device
@@ -745,6 +819,9 @@ var NosskeyManager = class {
745
819
  this.#clearKey(newSk);
746
820
  const newRecoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
747
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);
748
825
  const updatedKeyInfo = {
749
826
  credentialId: bytesToHex(newCredentialId),
750
827
  pubkey: newPubkey,
@@ -752,7 +829,8 @@ var NosskeyManager = class {
752
829
  recovery: {
753
830
  recoveryPubkey: newRecoveryPubkey,
754
831
  recoverySalt: newRecoverySalt,
755
- createdAt: Date.now()
832
+ createdAt: Date.now(),
833
+ signature
756
834
  }
757
835
  };
758
836
  this.setCurrentKeyInfo(updatedKeyInfo);
@@ -767,7 +845,8 @@ var NosskeyManager = class {
767
845
  return {
768
846
  recoveryPubkey: keyInfo.recovery.recoveryPubkey,
769
847
  recoverySalt: keyInfo.recovery.recoverySalt,
770
- createdAt: keyInfo.recovery.createdAt
848
+ createdAt: keyInfo.recovery.createdAt,
849
+ signature: keyInfo.recovery.signature
771
850
  };
772
851
  }
773
852
  /**
@@ -1416,15 +1495,18 @@ exports.createPasskey = createPasskey;
1416
1495
  exports.createRecoveryTag = createRecoveryTag;
1417
1496
  exports.deriveAesGcmKey = deriveAesGcmKey;
1418
1497
  exports.deriveNostrPrivateKey = deriveNostrPrivateKey;
1498
+ exports.deriveSaltFromUsername = deriveSaltFromUsername;
1419
1499
  exports.generatePasswordProtectedKey = generatePasswordProtectedKey;
1420
1500
  exports.getPrfSecret = getPrfSecret;
1421
1501
  exports.getPublicKeyFromPassword = getPublicKeyFromPassword;
1502
+ exports.getRecoverySignature = getRecoverySignature;
1422
1503
  exports.hexToBytes = hexToBytes;
1423
1504
  exports.importPublicKeyFromBundle = importPublicKeyFromBundle;
1424
1505
  exports.isPrfSupported = isPrfSupported;
1425
1506
  exports.parseRecoveryTag = parseRecoveryTag;
1426
1507
  exports.registerDummyPasskey = registerDummyPasskey;
1427
1508
  exports.unwrapPasswordProtectedPrivateKey = unwrapPasswordProtectedPrivateKey;
1509
+ exports.verifyRecoverySignature = verifyRecoverySignature;
1428
1510
  var applesauce_core = require("applesauce-core");
1429
1511
  Object.keys(applesauce_core).forEach(function (k) {
1430
1512
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {