11import { fromNsec , fromMnemonic , derive , zeroise } from 'nsec-tree'
22import { derivePersona } from 'nsec-tree/persona'
3- import { createBlindProof , createFullProof , verifyProof } from 'nsec-tree/proof'
4- import { finalizeEvent } from 'nostr-tools/pure'
3+ import { createBlindProof , createFullProof } from 'nsec-tree/proof'
4+ import { finalizeEvent , getPublicKey } from 'nostr-tools/pure'
5+ import { decode , npubEncode , nsecEncode } from 'nostr-tools/nip19'
56import type { TreeRoot , Identity , LinkageProof } from 'nsec-tree'
67import type { Event as NostrEvent , EventTemplate } from 'nostr-tools'
78import type { PublicIdentity , SignFn } from './types.js'
@@ -14,6 +15,20 @@ interface CacheEntry {
1415 lastUsed : number
1516}
1617
18+ /** Build an Identity-compatible object from raw key bytes */
19+ function rawIdentity ( privateKey : Uint8Array ) : Identity {
20+ const publicKeyHex = getPublicKey ( privateKey )
21+ const publicKey = Buffer . from ( publicKeyHex , 'hex' )
22+ return {
23+ nsec : nsecEncode ( privateKey ) ,
24+ npub : npubEncode ( publicKeyHex ) ,
25+ privateKey : new Uint8Array ( privateKey ) , // copy so original can be cleaned
26+ publicKey : new Uint8Array ( publicKey ) ,
27+ purpose : 'master' ,
28+ index : 0 ,
29+ }
30+ }
31+
1732export interface ContextOptions {
1833 maxCache ?: number
1934}
@@ -28,24 +43,33 @@ export class IdentityContext {
2843 constructor ( secretKey : string , format : 'nsec' | 'hex' | 'mnemonic' , opts ?: ContextOptions ) {
2944 this . maxCacheSize = opts ?. maxCache ?? 5
3045
46+ // Parse the raw secret key bytes — this IS the user's actual Nostr identity
47+ let rawKeyBytes : Uint8Array
3148 if ( format === 'mnemonic' ) {
49+ // Mnemonic: create tree root, derive a "default" identity as master
3250 this . root = fromMnemonic ( secretKey )
51+ const derived = derive ( this . root , 'master' , 0 )
52+ this . masterEntry = { identity : derived , purpose : 'master' , index : 0 , lastUsed : Date . now ( ) }
53+ this . activeEntry = this . masterEntry
54+ return
3355 } else if ( format === 'hex' ) {
34- const bytes = Buffer . from ( secretKey , 'hex' )
35- this . root = fromNsec ( bytes )
56+ rawKeyBytes = Buffer . from ( secretKey , 'hex' )
3657 } else {
37- this . root = fromNsec ( secretKey )
58+ // nsec bech32
59+ rawKeyBytes = decode ( secretKey ) . data as Uint8Array
3860 }
3961
40- // Derive master identity — kept separate from LRU cache
41- const masterIdentity = derive ( this . root , 'master' , 0 )
62+ // Master identity = the user's actual keypair (their real npub)
4263 this . masterEntry = {
43- identity : masterIdentity ,
64+ identity : rawIdentity ( rawKeyBytes ) ,
4465 purpose : 'master' ,
4566 index : 0 ,
4667 lastUsed : Date . now ( ) ,
4768 }
4869 this . activeEntry = this . masterEntry
70+
71+ // nsec-tree root for child derivation
72+ this . root = fromNsec ( rawKeyBytes )
4973 }
5074
5175 /** Current active identity's npub (bech32) */
@@ -160,15 +184,24 @@ export class IdentityContext {
160184 return result
161185 }
162186
163- /** Create a linkage proof for the active identity */
187+ /** Create a linkage proof for the active identity.
188+ * Only works for derived identities — the master IS the raw key, not a tree child. */
164189 prove ( mode : 'blind' | 'full' = 'blind' ) : LinkageProof {
190+ if ( this . activeEntry === this . masterEntry ) {
191+ throw new Error ( 'Cannot prove master identity — it is the raw key, not a derived child. Switch to a derived identity first.' )
192+ }
165193 const child = this . activeEntry . identity
166194 if ( mode === 'full' ) {
167195 return createFullProof ( this . root , child )
168196 }
169197 return createBlindProof ( this . root , child )
170198 }
171199
200+ /** Get the nsec-tree root's master pubkey (the derivation anchor, distinct from the raw key's pubkey) */
201+ get treeRootPubkey ( ) : string {
202+ return this . root . masterPubkey
203+ }
204+
172205 /** Get the active identity's private key for NIP-17/44/04 crypto operations */
173206 get activePrivateKey ( ) : Uint8Array {
174207 return this . activeEntry . identity . privateKey
0 commit comments