ns-auth-sdk 1.11.0 → 1.12.1

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,14 @@ 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 subtle = globalThis.crypto?.subtle;
427
+ if (!subtle) throw new Error("Web Crypto API not available");
428
+ const hashBuffer = await subtle.digest("SHA-256", msgBuffer);
429
+ return bytesToHex(new Uint8Array(hashBuffer));
430
+ }
395
431
  function parseRecoveryTag(tags) {
396
432
  const recoveryTag = tags.find((tag) => tag[0] === "r");
397
433
  if (!recoveryTag || recoveryTag.length < 3) return null;
@@ -399,7 +435,8 @@ function parseRecoveryTag(tags) {
399
435
  return {
400
436
  recoveryPubkey: recoveryTag[1],
401
437
  recoverySalt: recoveryTag[2],
402
- createdAt: createdAt || void 0
438
+ createdAt: createdAt || void 0,
439
+ signature: recoveryTag[4] || void 0
403
440
  };
404
441
  }
405
442
  function createRecoveryTag(recovery) {
@@ -407,9 +444,35 @@ function createRecoveryTag(recovery) {
407
444
  "r",
408
445
  recovery.recoveryPubkey,
409
446
  recovery.recoverySalt,
410
- recovery.createdAt?.toString() || ""
447
+ recovery.createdAt?.toString() || "",
448
+ recovery.signature || ""
411
449
  ];
412
450
  }
451
+ function getRecoverySignature(kind0) {
452
+ if (!parseRecoveryTag(kind0.tags || [])) return null;
453
+ const tag = kind0.tags?.find((t) => t[0] === "r");
454
+ return tag && tag.length > 4 ? tag[4] || null : null;
455
+ }
456
+ async function verifyRecoverySignature(kind0) {
457
+ try {
458
+ if (!parseRecoveryTag(kind0.tags || [])) return false;
459
+ const signature = getRecoverySignature(kind0);
460
+ if (!signature || !kind0.pubkey) return false;
461
+ const messageHash = await sha256(kind0.pubkey);
462
+ const signatureBytes = hexToBytes(signature);
463
+ const pubkeyBytes = hexToBytes(kind0.pubkey);
464
+ return _noble_secp256k1.verify(signatureBytes, messageHash, pubkeyBytes);
465
+ } catch (e) {
466
+ return false;
467
+ }
468
+ }
469
+ async function sha256(message) {
470
+ const msgBuffer = new TextEncoder().encode(message);
471
+ const subtle = globalThis.crypto?.subtle;
472
+ if (!subtle) throw new Error("Web Crypto API not available");
473
+ const hashBuffer = await subtle.digest("SHA-256", msgBuffer);
474
+ return new Uint8Array(hashBuffer);
475
+ }
413
476
  /**
414
477
  * Nosskey - Passkey-Derived Nostr Keys
415
478
  */
@@ -601,6 +664,7 @@ var NosskeyManager = class {
601
664
  if (options.recoveryPassword) keyInfo = await this.addPasswordRecovery(options.recoveryPassword, credentialId);
602
665
  } else {
603
666
  if (!password) throw new Error("Password is required when PRF is not supported");
667
+ if (!options.username) throw new Error("Username is required when PRF is not supported");
604
668
  keyInfo = await this.createPasswordProtectedNostrKey(password, options);
605
669
  }
606
670
  return keyInfo;
@@ -613,10 +677,11 @@ var NosskeyManager = class {
613
677
  if (sk.every((byte) => byte === 0)) throw new Error("Invalid PRF output: all zeros");
614
678
  bytesToHex(sk);
615
679
  const publicKey = (0, applesauce_core_helpers.getPublicKey)(sk);
680
+ const salt = await deriveSaltFromUsername(options.username);
616
681
  const keyInfo = {
617
682
  credentialId: bytesToHex(credentialId || responseId),
618
683
  pubkey: publicKey,
619
- salt: STANDARD_SALT,
684
+ salt,
620
685
  ...options.username && { username: options.username }
621
686
  };
622
687
  if (this.#keyCache.isEnabled() && this.#keyCache.getCacheOptions().cacheOnCreation) this.#keyCache.setKey(keyInfo.credentialId, sk);
@@ -628,11 +693,12 @@ var NosskeyManager = class {
628
693
  * @param options
629
694
  */
630
695
  async createPasswordProtectedNostrKey(password, options = {}) {
631
- const pubkey = await getPublicKeyFromPassword(password, STANDARD_SALT);
696
+ const salt = await deriveSaltFromUsername(options.username);
697
+ const pubkey = await getPublicKeyFromPassword(password, salt);
632
698
  return {
633
699
  credentialId: bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16))),
634
700
  pubkey,
635
- salt: STANDARD_SALT,
701
+ salt,
636
702
  ...options.username && { username: options.username }
637
703
  };
638
704
  }
@@ -716,17 +782,29 @@ var NosskeyManager = class {
716
782
  if (!(currentCredentialId || (keyInfo.credentialId ? hexToBytes(keyInfo.credentialId) : void 0))) throw new Error("Credential ID required");
717
783
  const recoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
718
784
  const recoveryPubkey = await getPublicKeyFromPassword(password, recoverySalt);
785
+ const recoverySk = await deriveNostrPrivateKey(password, recoverySalt);
786
+ const signature = this.#signWithKey(recoverySk, keyInfo.pubkey);
787
+ this.#clearKey(recoverySk);
719
788
  const updatedKeyInfo = {
720
789
  ...keyInfo,
721
790
  recovery: {
722
791
  recoveryPubkey,
723
792
  recoverySalt,
724
- createdAt: Date.now()
793
+ createdAt: Date.now(),
794
+ signature
725
795
  }
726
796
  };
727
797
  this.setCurrentKeyInfo(updatedKeyInfo);
728
798
  return updatedKeyInfo;
729
799
  }
800
+ #signWithKey(sk, message) {
801
+ return (0, applesauce_core_helpers.finalizeEvent)({
802
+ kind: 0,
803
+ content: "",
804
+ tags: [],
805
+ created_at: Math.floor(Date.now() / 1e3)
806
+ }, sk).sig;
807
+ }
730
808
  /**
731
809
  * Activate recovery using password
732
810
  * Requires new credential ID from new device
@@ -745,6 +823,9 @@ var NosskeyManager = class {
745
823
  this.#clearKey(newSk);
746
824
  const newRecoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
747
825
  const newRecoveryPubkey = await getPublicKeyFromPassword(password, newRecoverySalt);
826
+ const recoverySk = await deriveNostrPrivateKey(password, recoverySalt);
827
+ const signature = this.#signWithKey(recoverySk, newPubkey);
828
+ this.#clearKey(recoverySk);
748
829
  const updatedKeyInfo = {
749
830
  credentialId: bytesToHex(newCredentialId),
750
831
  pubkey: newPubkey,
@@ -752,7 +833,8 @@ var NosskeyManager = class {
752
833
  recovery: {
753
834
  recoveryPubkey: newRecoveryPubkey,
754
835
  recoverySalt: newRecoverySalt,
755
- createdAt: Date.now()
836
+ createdAt: Date.now(),
837
+ signature
756
838
  }
757
839
  };
758
840
  this.setCurrentKeyInfo(updatedKeyInfo);
@@ -767,7 +849,8 @@ var NosskeyManager = class {
767
849
  return {
768
850
  recoveryPubkey: keyInfo.recovery.recoveryPubkey,
769
851
  recoverySalt: keyInfo.recovery.recoverySalt,
770
- createdAt: keyInfo.recovery.createdAt
852
+ createdAt: keyInfo.recovery.createdAt,
853
+ signature: keyInfo.recovery.signature
771
854
  };
772
855
  }
773
856
  /**
@@ -786,6 +869,11 @@ var NosskeyManager = class {
786
869
  */
787
870
  const INFO_BYTES = new TextEncoder().encode("nostr-pwk");
788
871
  const AES_LENGTH = 256;
872
+ function getSubtle() {
873
+ const subtle = globalThis.crypto?.subtle;
874
+ if (!subtle) throw new Error("Web Crypto API not available");
875
+ return subtle;
876
+ }
789
877
  /**
790
878
  * PRF AES-GCM
791
879
  * @param secret PRF
@@ -793,8 +881,9 @@ const AES_LENGTH = 256;
793
881
  * @returns AES-GCM
794
882
  */
795
883
  async function deriveAesGcmKey(secret, salt) {
796
- const keyMaterial = await crypto.subtle.importKey("raw", secret, "HKDF", false, ["deriveKey"]);
797
- return crypto.subtle.deriveKey({
884
+ const subtle = getSubtle();
885
+ const keyMaterial = await subtle.importKey("raw", secret, "HKDF", false, ["deriveKey"]);
886
+ return subtle.deriveKey({
798
887
  name: "HKDF",
799
888
  hash: "SHA-256",
800
889
  salt,
@@ -812,7 +901,7 @@ async function deriveAesGcmKey(secret, salt) {
812
901
  * @returns
813
902
  */
814
903
  async function aesGcmEncrypt(key, iv, plaintext) {
815
- const buf = await crypto.subtle.encrypt({
904
+ const buf = await getSubtle().encrypt({
816
905
  name: "AES-GCM",
817
906
  iv
818
907
  }, key, plaintext);
@@ -831,7 +920,7 @@ async function aesGcmEncrypt(key, iv, plaintext) {
831
920
  * @returns
832
921
  */
833
922
  async function aesGcmDecrypt(key, iv, ct, tag) {
834
- const buf = await crypto.subtle.decrypt({
923
+ const buf = await getSubtle().decrypt({
835
924
  name: "AES-GCM",
836
925
  iv
837
926
  }, key, new Uint8Array([...ct, ...tag]));
@@ -1416,15 +1505,18 @@ exports.createPasskey = createPasskey;
1416
1505
  exports.createRecoveryTag = createRecoveryTag;
1417
1506
  exports.deriveAesGcmKey = deriveAesGcmKey;
1418
1507
  exports.deriveNostrPrivateKey = deriveNostrPrivateKey;
1508
+ exports.deriveSaltFromUsername = deriveSaltFromUsername;
1419
1509
  exports.generatePasswordProtectedKey = generatePasswordProtectedKey;
1420
1510
  exports.getPrfSecret = getPrfSecret;
1421
1511
  exports.getPublicKeyFromPassword = getPublicKeyFromPassword;
1512
+ exports.getRecoverySignature = getRecoverySignature;
1422
1513
  exports.hexToBytes = hexToBytes;
1423
1514
  exports.importPublicKeyFromBundle = importPublicKeyFromBundle;
1424
1515
  exports.isPrfSupported = isPrfSupported;
1425
1516
  exports.parseRecoveryTag = parseRecoveryTag;
1426
1517
  exports.registerDummyPasskey = registerDummyPasskey;
1427
1518
  exports.unwrapPasswordProtectedPrivateKey = unwrapPasswordProtectedPrivateKey;
1519
+ exports.verifyRecoverySignature = verifyRecoverySignature;
1428
1520
  var applesauce_core = require("applesauce-core");
1429
1521
  Object.keys(applesauce_core).forEach(function (k) {
1430
1522
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {