nsec.signer.ts raw
1 import { ISigner, TDraftEvent } from '@/types'
2 import * as utils from '@noble/curves/abstract/utils'
3 import { bech32 } from '@scure/base'
4 import { finalizeEvent, getPublicKey as nGetPublicKey, nip04 } from 'nostr-tools'
5 import * as nip44 from 'nostr-tools/nip44'
6
7 /**
8 * Convert nsec (bech32) to hex string
9 */
10 function nsecToHex(nsec: string): string {
11 const { prefix, words } = bech32.decode(nsec as `${string}1${string}`, 5000)
12 if (prefix !== 'nsec') {
13 throw new Error('Invalid nsec prefix')
14 }
15 const data = new Uint8Array(bech32.fromWords(words))
16 return utils.bytesToHex(data)
17 }
18
19 /**
20 * Normalize a private key to hex format.
21 * Accepts nsec (bech32) or hex string.
22 */
23 function normalizeToHex(privateKey: string): string {
24 if (privateKey.startsWith('nsec')) {
25 return nsecToHex(privateKey)
26 }
27 return privateKey
28 }
29
30 /**
31 * Validate that a hex key is exactly 64 characters.
32 */
33 function validateHexKey(hex: string): void {
34 if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
35 throw new Error('Private key must be 64 hex characters')
36 }
37 }
38
39 export class NsecSigner implements ISigner {
40 private privkey: Uint8Array | null = null
41 private pubkey: string | null = null
42
43 login(nsecOrPrivkey: string | Uint8Array) {
44 let privkey: Uint8Array
45
46 if (typeof nsecOrPrivkey === 'string') {
47 try {
48 const hex = normalizeToHex(nsecOrPrivkey)
49 validateHexKey(hex)
50 privkey = utils.hexToBytes(hex)
51 } catch (error) {
52 throw new Error(
53 `Invalid private key: ${error instanceof Error ? error.message : 'unknown error'}`
54 )
55 }
56 } else {
57 privkey = nsecOrPrivkey
58 }
59
60 this.privkey = privkey
61 this.pubkey = nGetPublicKey(privkey)
62 return this.pubkey
63 }
64
65 async getPublicKey() {
66 if (!this.pubkey) {
67 throw new Error('Not logged in')
68 }
69 return this.pubkey
70 }
71
72 async signEvent(draftEvent: TDraftEvent) {
73 if (!this.privkey) {
74 throw new Error('Not logged in')
75 }
76
77 return finalizeEvent(draftEvent, this.privkey)
78 }
79
80 async nip04Encrypt(pubkey: string, plainText: string) {
81 if (!this.privkey) {
82 throw new Error('Not logged in')
83 }
84 return nip04.encrypt(this.privkey, pubkey, plainText)
85 }
86
87 async nip04Decrypt(pubkey: string, cipherText: string) {
88 if (!this.privkey) {
89 throw new Error('Not logged in')
90 }
91 return nip04.decrypt(this.privkey, pubkey, cipherText)
92 }
93
94 async nip44Encrypt(pubkey: string, plainText: string) {
95 if (!this.privkey) {
96 throw new Error('Not logged in')
97 }
98 const conversationKey = nip44.v2.utils.getConversationKey(this.privkey, pubkey)
99 return nip44.v2.encrypt(plainText, conversationKey)
100 }
101
102 async nip44Decrypt(pubkey: string, cipherText: string) {
103 if (!this.privkey) {
104 throw new Error('Not logged in')
105 }
106 const conversationKey = nip44.v2.utils.getConversationKey(this.privkey, pubkey)
107 return nip44.v2.decrypt(cipherText, conversationKey)
108 }
109 }
110