nappup 1.0.14 → 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 +10 -9
- 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)
|
|
@@ -82,7 +82,7 @@ export class NostrRelays {
|
|
|
82
82
|
onclose: err => {
|
|
83
83
|
clearTimeout(timer)
|
|
84
84
|
if (isClosed) return
|
|
85
|
-
//
|
|
85
|
+
// May have closed normally, without error
|
|
86
86
|
err ? p.reject(err) : p.resolve()
|
|
87
87
|
},
|
|
88
88
|
oneose: () => {
|
|
@@ -110,7 +110,7 @@ export class NostrRelays {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// Send an event to a list of relays
|
|
113
|
+
// Send an event to a list of relays
|
|
114
114
|
async sendEvent (event, relays, timeout = 3000) {
|
|
115
115
|
const promises = relays.map(async (url) => {
|
|
116
116
|
let timer
|
|
@@ -146,6 +146,7 @@ export class NostrRelays {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
//
|
|
149
|
+
|
|
150
|
+
// Share same connection
|
|
151
|
+
// Connections aren't authenticated, thus no need to split by authed user
|
|
151
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
|
|