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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "GPL-3.0-or-later",
9
- "version": "1.0.14",
9
+ "version": "1.1.0",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -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
+ }
@@ -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
- // reconnect if needed to avoid SendingOnClosedConnection errors
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
- // may have closed normally, without error
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
- // Share same connection.
150
- // Connections aren't authenticated, thus no need to split by authed user.
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 (skHex) {
38
- if (skHex) return new this(createToken, base16ToBytes(skHex))
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
- skBytes = base16ToBytes(process.env.NOSTR_SECRET_KEY)
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
- skHex = generateSecretKey()
47
- fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${skHex}\n`)
48
- skBytes = base16ToBytes(skHex)
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(skHex)
57
+ if (isNewSk) await ret.#initSk(sk)
52
58
  return ret
53
59
  }
54
60