nappup 1.0.13 → 1.1.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/package.json +1 -1
- package/src/helpers/bech32.js +130 -0
- package/src/helpers/nip19.js +17 -0
- package/src/services/nostr-relays.js +23 -20
- package/src/services/nostr-signer.js +13 -7
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
|
2
|
+
|
|
3
|
+
const ALPHABET_MAP = {}
|
|
4
|
+
for (let z = 0; z < ALPHABET.length; z++) {
|
|
5
|
+
const x = ALPHABET.charAt(z)
|
|
6
|
+
ALPHABET_MAP[x] = z
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function polymod (values) {
|
|
10
|
+
let chk = 1
|
|
11
|
+
for (let p = 0; p < values.length; ++p) {
|
|
12
|
+
const top = chk >> 25
|
|
13
|
+
chk = (chk & 0x1ffffff) << 5 ^ values[p]
|
|
14
|
+
for (let i = 0; i < 5; ++i) {
|
|
15
|
+
if ((top >> i) & 1) {
|
|
16
|
+
chk ^= 0x3b6a57b2 >> i
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return chk
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hrpExpand (hrp) {
|
|
24
|
+
const ret = []
|
|
25
|
+
for (let p = 0; p < hrp.length; ++p) {
|
|
26
|
+
ret.push(hrp.charCodeAt(p) >> 5)
|
|
27
|
+
}
|
|
28
|
+
ret.push(0)
|
|
29
|
+
for (let p = 0; p < hrp.length; ++p) {
|
|
30
|
+
ret.push(hrp.charCodeAt(p) & 31)
|
|
31
|
+
}
|
|
32
|
+
return ret
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function verifyChecksum (hrp, data) {
|
|
36
|
+
return polymod(hrpExpand(hrp).concat(data)) === 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createChecksum (hrp, data) {
|
|
40
|
+
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0])
|
|
41
|
+
const mod = polymod(values) ^ 1
|
|
42
|
+
const ret = []
|
|
43
|
+
for (let p = 0; p < 6; ++p) {
|
|
44
|
+
ret.push((mod >> 5 * (5 - p)) & 31)
|
|
45
|
+
}
|
|
46
|
+
return ret
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function encode (hrp, data) {
|
|
50
|
+
const combined = data.concat(createChecksum(hrp, data))
|
|
51
|
+
let ret = hrp + '1'
|
|
52
|
+
for (let p = 0; p < combined.length; ++p) {
|
|
53
|
+
ret += ALPHABET.charAt(combined[p])
|
|
54
|
+
}
|
|
55
|
+
return ret
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function decode (bechString, limit = 90) {
|
|
59
|
+
let p
|
|
60
|
+
let hasLower = false
|
|
61
|
+
let hasUpper = false
|
|
62
|
+
for (p = 0; p < bechString.length; ++p) {
|
|
63
|
+
if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) {
|
|
67
|
+
hasLower = true
|
|
68
|
+
}
|
|
69
|
+
if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) {
|
|
70
|
+
hasUpper = true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (hasLower && hasUpper) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
bechString = bechString.toLowerCase()
|
|
77
|
+
const pos = bechString.lastIndexOf('1')
|
|
78
|
+
if (pos < 1 || pos + 7 > bechString.length || bechString.length > limit) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
const hrp = bechString.substring(0, pos)
|
|
82
|
+
const data = []
|
|
83
|
+
for (p = pos + 1; p < bechString.length; ++p) {
|
|
84
|
+
const d = ALPHABET_MAP[bechString.charAt(p)]
|
|
85
|
+
if (d === undefined) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
data.push(d)
|
|
89
|
+
}
|
|
90
|
+
if (!verifyChecksum(hrp, data)) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
return { hrp, data: data.slice(0, data.length - 6) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function toWords (data) {
|
|
97
|
+
let value = 0
|
|
98
|
+
let bits = 0
|
|
99
|
+
const maxV = 31
|
|
100
|
+
const result = []
|
|
101
|
+
for (let i = 0; i < data.length; ++i) {
|
|
102
|
+
value = (value << 8) | data[i]
|
|
103
|
+
bits += 8
|
|
104
|
+
while (bits >= 5) {
|
|
105
|
+
bits -= 5
|
|
106
|
+
result.push((value >> bits) & maxV)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (bits > 0) {
|
|
110
|
+
result.push((value << (5 - bits)) & maxV)
|
|
111
|
+
}
|
|
112
|
+
return result
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function fromWords (data) {
|
|
116
|
+
let value = 0
|
|
117
|
+
let bits = 0
|
|
118
|
+
const maxV = 255
|
|
119
|
+
const result = []
|
|
120
|
+
for (let i = 0; i < data.length; ++i) {
|
|
121
|
+
const element = data[i]
|
|
122
|
+
value = (value << 5) | element
|
|
123
|
+
bits += 5
|
|
124
|
+
while (bits >= 8) {
|
|
125
|
+
bits -= 8
|
|
126
|
+
result.push((value >> bits) & maxV)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result
|
|
130
|
+
}
|
package/src/helpers/nip19.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
|
|
2
2
|
import { bytesToBase62, base62ToBytes, BASE62_ALPHABET } from '#helpers/base62.js'
|
|
3
|
+
import { encode as bech32Encode, decode as bech32Decode, toWords, fromWords } from '#helpers/bech32.js'
|
|
3
4
|
import { isNostrAppDTagSafe } from '#helpers/app.js'
|
|
4
5
|
|
|
5
6
|
const MAX_SIZE = 5000
|
|
@@ -58,6 +59,22 @@ export function appDecode (entity) {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
export function nsecEncode (hex) {
|
|
63
|
+
const bytes = base16ToBytes(hex)
|
|
64
|
+
const words = toWords(bytes)
|
|
65
|
+
return bech32Encode('nsec', words)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function nsecDecode (nsec) {
|
|
69
|
+
const decoded = bech32Decode(nsec, MAX_SIZE)
|
|
70
|
+
if (!decoded) throw new Error('Invalid nsec')
|
|
71
|
+
const { hrp, data } = decoded
|
|
72
|
+
if (hrp !== 'nsec') throw new Error('Invalid nsec')
|
|
73
|
+
const bytes = fromWords(data)
|
|
74
|
+
if (bytes.length !== 32) throw new Error('Invalid nsec length')
|
|
75
|
+
return bytesToBase16(new Uint8Array(bytes))
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
function toTlv (tlvConfig) {
|
|
62
79
|
const arrays = []
|
|
63
80
|
tlvConfig
|
|
@@ -16,19 +16,19 @@ export const freeRelays = [
|
|
|
16
16
|
'wss://relay.nostr.band'
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
-
// Interacts with Nostr relays
|
|
19
|
+
// Interacts with Nostr relays
|
|
20
20
|
export class NostrRelays {
|
|
21
21
|
#relays = new Map()
|
|
22
22
|
#relayTimeouts = new Map()
|
|
23
23
|
#timeout = 30000 // 30 seconds
|
|
24
24
|
|
|
25
|
-
// Get a relay connection, creating one if it doesn't exist
|
|
25
|
+
// Get a relay connection, creating one if it doesn't exist
|
|
26
26
|
async #getRelay (url) {
|
|
27
27
|
if (this.#relays.has(url)) {
|
|
28
28
|
clearTimeout(this.#relayTimeouts.get(url))
|
|
29
29
|
this.#relayTimeouts.set(url, maybeUnref(setTimeout(() => this.disconnect(url), this.#timeout)))
|
|
30
30
|
const relay = this.#relays.get(url)
|
|
31
|
-
//
|
|
31
|
+
// Reconnect if needed to avoid SendingOnClosedConnection errors
|
|
32
32
|
await relay.connect()
|
|
33
33
|
return relay
|
|
34
34
|
}
|
|
@@ -43,7 +43,7 @@ export class NostrRelays {
|
|
|
43
43
|
return relay
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Disconnect from a relay
|
|
46
|
+
// Disconnect from a relay
|
|
47
47
|
async disconnect (url) {
|
|
48
48
|
if (this.#relays.has(url)) {
|
|
49
49
|
const relay = this.#relays.get(url)
|
|
@@ -54,7 +54,7 @@ export class NostrRelays {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Disconnect from all relays
|
|
57
|
+
// Disconnect from all relays
|
|
58
58
|
async disconnectAll () {
|
|
59
59
|
for (const url of this.#relays.keys()) {
|
|
60
60
|
await this.disconnect(url)
|
|
@@ -64,17 +64,14 @@ export class NostrRelays {
|
|
|
64
64
|
// Get events from a list of relays
|
|
65
65
|
async getEvents (filter, relays, timeout = 5000) {
|
|
66
66
|
const events = []
|
|
67
|
-
const resolveOrReject = (resolve, reject, err) => {
|
|
68
|
-
err ? reject(err) : resolve()
|
|
69
|
-
}
|
|
70
67
|
const promises = relays.map(async (url) => {
|
|
71
68
|
let sub
|
|
72
69
|
let isClosed = false
|
|
73
70
|
const p = Promise.withResolvers()
|
|
74
71
|
const timer = maybeUnref(setTimeout(() => {
|
|
75
|
-
sub?.close()
|
|
76
72
|
isClosed = true
|
|
77
|
-
|
|
73
|
+
sub?.close()
|
|
74
|
+
p.reject(new Error(`timeout: ${url}`))
|
|
78
75
|
}, timeout))
|
|
79
76
|
try {
|
|
80
77
|
const relay = await this.#getRelay(url)
|
|
@@ -85,7 +82,8 @@ export class NostrRelays {
|
|
|
85
82
|
onclose: err => {
|
|
86
83
|
clearTimeout(timer)
|
|
87
84
|
if (isClosed) return
|
|
88
|
-
|
|
85
|
+
// May have closed normally, without error
|
|
86
|
+
err ? p.reject(err) : p.resolve()
|
|
89
87
|
},
|
|
90
88
|
oneose: () => {
|
|
91
89
|
clearTimeout(timer)
|
|
@@ -94,12 +92,12 @@ export class NostrRelays {
|
|
|
94
92
|
p.resolve()
|
|
95
93
|
}
|
|
96
94
|
})
|
|
97
|
-
|
|
98
|
-
await p.promise
|
|
99
95
|
} catch (err) {
|
|
100
96
|
clearTimeout(timer)
|
|
101
97
|
p.reject(err)
|
|
102
98
|
}
|
|
99
|
+
|
|
100
|
+
return p.promise
|
|
103
101
|
})
|
|
104
102
|
|
|
105
103
|
const results = await Promise.allSettled(promises)
|
|
@@ -112,26 +110,30 @@ export class NostrRelays {
|
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
// Send an event to a list of relays
|
|
113
|
+
// Send an event to a list of relays
|
|
116
114
|
async sendEvent (event, relays, timeout = 3000) {
|
|
117
115
|
const promises = relays.map(async (url) => {
|
|
118
116
|
let timer
|
|
117
|
+
const p = Promise.withResolvers()
|
|
119
118
|
try {
|
|
120
119
|
timer = maybeUnref(setTimeout(() => {
|
|
121
|
-
|
|
120
|
+
p.reject(new Error(`timeout: ${url}`))
|
|
122
121
|
}, timeout))
|
|
122
|
+
|
|
123
123
|
const relay = await this.#getRelay(url)
|
|
124
124
|
await relay.publish(event)
|
|
125
|
+
p.resolve()
|
|
125
126
|
} catch (err) {
|
|
126
|
-
if (err.message?.startsWith('duplicate:')) return
|
|
127
|
+
if (err.message?.startsWith('duplicate:')) return p.resolve()
|
|
127
128
|
if (err.message?.startsWith('mute:')) {
|
|
128
129
|
console.info(`${url} - ${err.message}`)
|
|
129
|
-
return
|
|
130
|
+
return p.resolve()
|
|
130
131
|
}
|
|
131
|
-
|
|
132
|
+
p.reject(err)
|
|
132
133
|
} finally {
|
|
133
134
|
clearTimeout(timer)
|
|
134
135
|
}
|
|
136
|
+
return p.promise
|
|
135
137
|
})
|
|
136
138
|
|
|
137
139
|
const results = await Promise.allSettled(promises)
|
|
@@ -144,6 +146,7 @@ export class NostrRelays {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
+
|
|
150
|
+
// Share same connection
|
|
151
|
+
// Connections aren't authenticated, thus no need to split by authed user
|
|
149
152
|
export default new NostrRelays()
|
|
@@ -7,6 +7,7 @@ import { getConversationKey, encrypt, decrypt } from 'nostr-tools/nip44'
|
|
|
7
7
|
import nostrRelays, { seedRelays, freeRelays } from '#services/nostr-relays.js'
|
|
8
8
|
import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
|
|
9
9
|
import { finalizeEvent } from '#helpers/nip01.js'
|
|
10
|
+
import { nsecDecode, nsecEncode } from '#helpers/nip19.js'
|
|
10
11
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
11
12
|
|
|
12
13
|
const dotenvPath = process.env.DOTENV_CONFIG_PATH ?? `${__dirname}/../../.env`
|
|
@@ -34,21 +35,26 @@ export default class NostrSigner {
|
|
|
34
35
|
this.#secretKey = skBytes
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
static async create (
|
|
38
|
-
if (
|
|
38
|
+
static async create (sk) {
|
|
39
|
+
if (sk) {
|
|
40
|
+
if (sk.startsWith('nsec')) sk = nsecDecode(sk)
|
|
41
|
+
return new this(createToken, base16ToBytes(sk))
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
let skBytes
|
|
41
45
|
let isNewSk = false
|
|
42
46
|
if (process.env.NOSTR_SECRET_KEY) {
|
|
43
|
-
|
|
47
|
+
let envSk = process.env.NOSTR_SECRET_KEY
|
|
48
|
+
if (envSk.startsWith('nsec')) envSk = nsecDecode(envSk)
|
|
49
|
+
skBytes = base16ToBytes(envSk)
|
|
44
50
|
} else {
|
|
45
51
|
isNewSk = true
|
|
46
|
-
|
|
47
|
-
fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${
|
|
48
|
-
skBytes = base16ToBytes(
|
|
52
|
+
sk = generateSecretKey()
|
|
53
|
+
fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${nsecEncode(sk)}\n`)
|
|
54
|
+
skBytes = base16ToBytes(sk)
|
|
49
55
|
}
|
|
50
56
|
const ret = new this(createToken, skBytes)
|
|
51
|
-
if (isNewSk) await ret.#initSk(
|
|
57
|
+
if (isNewSk) await ret.#initSk(sk)
|
|
52
58
|
return ret
|
|
53
59
|
}
|
|
54
60
|
|