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 +40 -46
- package/dist/index.cjs +91 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +7 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +61 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -8
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, {
|