nappup 2.1.0 → 2.2.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": "MIT",
9
- "version": "2.1.0",
9
+ "version": "2.2.0",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -22,10 +22,9 @@
22
22
  "@noble/hashes": "^2.0.0",
23
23
  "dotenv": "^17.2.0",
24
24
  "file-type": "^21.0.0",
25
- "libp2r2p": "^0.3.0",
25
+ "libp2r2p": "^0.9.0",
26
26
  "mime-types": "^3.0.1",
27
- "nmmr": "^2.0.0",
28
- "nostr-tools": "^2.23.3"
27
+ "nmmr": "^2.0.0"
29
28
  },
30
29
  "devDependencies": {
31
30
  "eslint": "^9.22.0",
@@ -1,4 +1,4 @@
1
- import { getPublicKey } from 'nostr-tools/pure'
1
+ import { getPublicKey } from 'libp2r2p/key'
2
2
  import { base16ToBytes } from 'libp2r2p/base16'
3
3
  import { base64UrlToBytes, bytesToBase64Url } from 'libp2r2p/base64'
4
4
 
@@ -1,4 +1,4 @@
1
- import { generateSecretKey } from 'nostr-tools/pure'
1
+ import { generateSecretKey } from 'libp2r2p/key'
2
2
  import { bytesToBase16 } from 'libp2r2p/base16'
3
3
  import { toBunkerUrl } from 'libp2r2p/nip46'
4
4
  import {
@@ -1,4 +1,4 @@
1
- import nostrRelays, { nappRelays } from '#services/nostr-relays.js'
1
+ import nostrRelays, { nappRelays, sendEventReport } from '#services/nostr-relays.js'
2
2
  import NMMR from 'nmmr'
3
3
  import { decode as base93Decode, encode as base93Encode } from 'libp2r2p/base93'
4
4
  import { stringifyEvent } from '#helpers/event.js'
@@ -137,7 +137,7 @@ export async function throttledSendEvent (event, relays, {
137
137
  if (pause && leadingPause) await new Promise(resolve => setTimeout(resolve, pause))
138
138
  if (retries > 0) log(`Retrying upload to ${relays.length} relays: ${relays.join(', ')}`)
139
139
 
140
- const { errors } = (await nostrRelays.sendEvent(event, relays, 15000))
140
+ const { errors } = await sendEventReport(event, relays, { timeout: 15000, timeoutUntilFirstFulfillment: null })
141
141
  if (errors.length === 0) {
142
142
  if (pause && trailingPause) await new Promise(resolve => setTimeout(resolve, pause))
143
143
  return { pause }
@@ -147,8 +147,7 @@ export async function throttledSendEvent (event, relays, {
147
147
  errors.reduce((r, v) => {
148
148
  const message = v.reason?.message ?? ''
149
149
  if (message.startsWith('rate-limited:')) r[0].push(v)
150
- // https://github.com/nbd-wtf/nostr-tools/blob/28f7553187d201088c8a1009365db4ecbe03e568/abstract-relay.ts#L311
151
- else if (message === 'publish timed out') r[1].push(v)
150
+ else if (message === 'PUBLISH_TIMEOUT') r[1].push(v)
152
151
  else r[2].push(v)
153
152
  return r
154
153
  }, [[], [], []])
@@ -158,7 +157,7 @@ export async function throttledSendEvent (event, relays, {
158
157
  const timedOutRelays = maybeUnretryableErrors.map(v => v.relay)
159
158
  log(`${maybeUnretryableErrors.length} timeout errors, retrying once after ${pause}ms:\n${maybeUnretryableErrors.map(v => `${v.relay}: ${v.reason.message}`).join('; ')}`)
160
159
  if (pause) await new Promise(resolve => setTimeout(resolve, pause))
161
- const { errors: timeoutRetryErrors } = await nostrRelays.sendEvent(event, timedOutRelays, 15000)
160
+ const { errors: timeoutRetryErrors } = await sendEventReport(event, timedOutRelays, { timeout: 15000, timeoutUntilFirstFulfillment: null })
162
161
  unretryableErrors.push(...timeoutRetryErrors)
163
162
  }
164
163
 
@@ -204,7 +203,7 @@ export async function getPreviousChunks (dTagValues, relays, signer) {
204
203
  authors: [pubkey],
205
204
  '#d': batch,
206
205
  limit: batch.length
207
- }, targetRelays)).result
206
+ }, targetRelays, { timeoutAfterFirstEose: null })).result
208
207
 
209
208
  for (const event of storedEvents) {
210
209
  const dTag = event.tags?.find(tag => tag[0] === 'd')?.[1]
@@ -1,5 +1,4 @@
1
- import { Relay } from 'nostr-tools/relay'
2
- import { maybeUnref } from '#helpers/timer.js'
1
+ import { relayPool } from 'libp2r2p/relay'
3
2
 
4
3
  export const seedRelays = [
5
4
  'wss://relay.44billion.net',
@@ -14,145 +13,13 @@ export const freeRelays = [
14
13
  'wss://nos.lol',
15
14
  'wss://relay.damus.io'
16
15
  ]
17
- export const nappRelays = [
18
- 'wss://relay.44billion.net'
19
- ]
20
-
21
- // Interacts with Nostr relays
22
- export class NostrRelays {
23
- #relays = new Map()
24
- #relayTimeouts = new Map()
25
- #timeout = 30000 // 30 seconds
26
-
27
- // Get a relay connection, creating one if it doesn't exist
28
- async #getRelay (url) {
29
- if (this.#relays.has(url)) {
30
- clearTimeout(this.#relayTimeouts.get(url))
31
- this.#relayTimeouts.set(url, maybeUnref(setTimeout(() => this.disconnect(url), this.#timeout)))
32
- const relay = this.#relays.get(url)
33
- // Reconnect if needed to avoid SendingOnClosedConnection errors
34
- await relay.connect()
35
- return relay
36
- }
37
-
38
- const relay = new Relay(url)
39
- this.#relays.set(url, relay)
40
-
41
- await relay.connect()
42
-
43
- this.#relayTimeouts.set(url, maybeUnref(setTimeout(() => this.disconnect(url), this.#timeout)))
44
-
45
- return relay
46
- }
47
-
48
- // Disconnect from a relay
49
- async disconnect (url) {
50
- if (this.#relays.has(url)) {
51
- const relay = this.#relays.get(url)
52
- if (relay.ws.readyState < 2) await relay.close()?.catch(console.log)
53
- this.#relays.delete(url)
54
- clearTimeout(this.#relayTimeouts.get(url))
55
- this.#relayTimeouts.delete(url)
56
- }
57
- }
58
-
59
- // Disconnect from all relays
60
- async disconnectAll () {
61
- for (const url of this.#relays.keys()) {
62
- await this.disconnect(url)
63
- }
64
- }
65
-
66
- // Get events from a list of relays
67
- async getEvents (filter, relays, timeout = 5000) {
68
- const events = []
69
- const promises = relays.map(async (url) => {
70
- let sub
71
- const p = Promise.withResolvers()
72
- const t = Promise.withResolvers()
73
- const timer = maybeUnref(setTimeout(() => {
74
- sub?.close()
75
- t.reject(new Error(`timeout: ${url}`))
76
- }, timeout))
77
-
78
- ;(async () => {
79
- try {
80
- const relay = await this.#getRelay(url)
81
- sub = relay.subscribe([filter], {
82
- onevent: (event) => {
83
- event.meta = { relay: url }
84
- events.push(event)
85
- },
86
- onclose: err => err ? p.reject(err) : p.resolve(),
87
- oneose: () => { sub.close(); p.resolve() }
88
- })
89
- } catch (err) {
90
- p.reject(err)
91
- }
92
- })()
93
-
94
- try {
95
- await Promise.race([p.promise, t.promise])
96
- } finally {
97
- clearTimeout(timer)
98
- }
99
- })
100
-
101
- const results = await Promise.allSettled(promises)
102
- const rejectedResults = results.filter(v => v.status === 'rejected')
103
-
104
- return {
105
- result: events,
106
- errors: rejectedResults.map(v => ({ reason: v.reason, relay: relays[results.indexOf(v)] })),
107
- success: events.length > 0 || results.length !== rejectedResults.length
108
- }
109
- }
110
-
111
- // Send an event to a list of relays
112
- async sendEvent (event, relays, timeout = 3000) {
113
- const eventToSend = event.meta ? { ...event } : event
114
- if (eventToSend.meta) delete eventToSend.meta
115
-
116
- const promises = relays.map(async (url) => {
117
- const p = Promise.withResolvers()
118
- const t = Promise.withResolvers()
119
- const timer = maybeUnref(setTimeout(() => {
120
- t.reject(new Error(`timeout: ${url}`))
121
- }, timeout))
122
-
123
- ;(async () => {
124
- try {
125
- const relay = await this.#getRelay(url)
126
- await relay.publish(eventToSend)
127
- p.resolve()
128
- } catch (err) {
129
- if (err.message?.startsWith('duplicate:')) return p.resolve()
130
- if (err.message?.startsWith('mute:')) {
131
- console.info(`${url} - ${err.message}`)
132
- return p.resolve()
133
- }
134
- p.reject(err)
135
- }
136
- })()
137
-
138
- try {
139
- await Promise.race([p.promise, t.promise])
140
- } finally {
141
- clearTimeout(timer)
142
- }
143
- })
144
-
145
- const results = await Promise.allSettled(promises)
146
- const rejectedResults = results.filter(v => v.status === 'rejected')
16
+ export const nappRelays = ['wss://relay.44billion.net']
147
17
 
148
- return {
149
- result: null,
150
- errors: rejectedResults.map(v => ({ reason: v.reason, relay: relays[results.indexOf(v)] })),
151
- success: results.length !== rejectedResults.length
152
- }
153
- }
18
+ // sendEvent returns quickly after the first successful publish. Upload flows
19
+ // need the terminal per-relay report so retries and replication stay correct.
20
+ export async function sendEventReport (event, relays, options) {
21
+ const result = await relayPool.sendEvent(event, relays, options)
22
+ return await (result?.promise ?? result)
154
23
  }
155
24
 
156
- // Share same connection
157
- // Connections aren't authenticated, thus no need to split by authed user
158
- export default new NostrRelays()
25
+ export default relayPool
@@ -1,4 +1,4 @@
1
- import { getPublicKey } from 'nostr-tools/pure'
1
+ import { getPublicKey } from 'libp2r2p/key'
2
2
  import { base16ToBytes } from 'libp2r2p/base16'
3
3
  import { nsecDecode } from 'libp2r2p/nip19'
4
4
  import { parseBunkerSessionUrl } from '#services/bunker-url.js'
@@ -1,18 +1,12 @@
1
- import { getPublicKey } from 'nostr-tools/pure'
2
- import { getConversationKey, encrypt, decrypt } from 'nostr-tools/nip44'
3
- import nostrRelays, { seedRelays, freeRelays } from '#services/nostr-relays.js'
1
+ import { finalizeEvent } from 'libp2r2p/event'
2
+ import { generateSecretKey, getPublicKey } from 'libp2r2p/key'
3
+ import * as nip44 from 'libp2r2p/nip44'
4
+ import { seedRelays, freeRelays, sendEventReport } from '#services/nostr-relays.js'
4
5
  import { getRelays } from '#helpers/signer.js'
5
6
  import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
6
- import { finalizeEvent } from '#helpers/nip01.js'
7
7
  import { nsecDecode, nsecEncode } from 'libp2r2p/nip19'
8
8
  import { ensureDotenvInitialized, setEncryptedDotenvValue } from '#services/dotenv.js'
9
9
 
10
- const nip44 = {
11
- getConversationKey,
12
- encrypt,
13
- decrypt
14
- }
15
-
16
10
  const createToken = Symbol('createToken')
17
11
 
18
12
  export default class NostrSigner {
@@ -42,9 +36,8 @@ export default class NostrSigner {
42
36
  skBytes = base16ToBytes(envSk)
43
37
  } else {
44
38
  isNewSk = true
45
- sk = generateSecretKey()
46
- setEncryptedDotenvValue('NOSTR_SECRET_KEY', nsecEncode(sk))
47
- skBytes = base16ToBytes(sk)
39
+ skBytes = generateSecretKey()
40
+ setEncryptedDotenvValue('NOSTR_SECRET_KEY', nsecEncode(bytesToBase16(skBytes)))
48
41
  }
49
42
  const ret = new this(createToken, skBytes)
50
43
  if (isNewSk) await ret.#initSk(sk)
@@ -65,7 +58,7 @@ export default class NostrSigner {
65
58
  content: '',
66
59
  created_at: Math.floor(Date.now() / 1000)
67
60
  })
68
- await nostrRelays.sendEvent(relayList, [...new Set([...seedRelays, ...relays].map(r => r.trim().replace(/\/$/, '')))])
61
+ await sendEventReport(relayList, [...new Set([...seedRelays, ...relays].map(r => r.trim().replace(/\/$/, '')))])
69
62
 
70
63
  const profile = await this.signEvent({
71
64
  kind: 0,
@@ -77,7 +70,7 @@ export default class NostrSigner {
77
70
  }),
78
71
  created_at: Math.floor(Date.now() / 1000)
79
72
  })
80
- await nostrRelays.sendEvent(profile, relays)
73
+ await sendEventReport(profile, relays)
81
74
  }
82
75
 
83
76
  // hex
@@ -100,21 +93,11 @@ export default class NostrSigner {
100
93
 
101
94
  nip44Encrypt (pubkey, plaintext) {
102
95
  const conversationKey = nip44.getConversationKey(this.#secretKey, pubkey)
103
- return nip44.encrypt(conversationKey, plaintext)
96
+ return nip44.encrypt(plaintext, conversationKey)
104
97
  }
105
98
 
106
99
  nip44Decrypt (pubkey, ciphertext) {
107
100
  const conversationKey = nip44.getConversationKey(this.#secretKey, pubkey)
108
- return nip44.decrypt(conversationKey, ciphertext)
101
+ return nip44.decrypt(ciphertext, conversationKey)
109
102
  }
110
103
  }
111
-
112
- function generateSecretKey () {
113
- const randomBytes = crypto.getRandomValues(new Uint8Array(40))
114
- const B256 = 2n ** 256n // secp256k1 is short weierstrass curve
115
- const N = B256 - 0x14551231950b75fc4402da1732fc9bebfn // curve (group) order
116
- const bytesToNumber = b => BigInt('0x' + (bytesToBase16(b) || '0'))
117
- const mod = (a, b) => { const r = a % b; return r >= 0n ? r : b + r } // mod division
118
- const num = mod(bytesToNumber(randomBytes), N - 1n) + 1n // takes at least n+8 bytes
119
- return num.toString(16).padStart(64, '0')
120
- }
@@ -187,7 +187,7 @@ export async function uploadSiteManifest ({
187
187
  authors: [await signer.getPublicKey()],
188
188
  '#d': [dTag],
189
189
  limit: 1
190
- }, relays)).result
190
+ }, relays, { timeoutAfterFirstEose: null })).result
191
191
  events.sort(newestFirst)
192
192
  const previous = events[0]
193
193
  const tags = buildManifestTags({
@@ -1,32 +0,0 @@
1
- import { schnorr } from '@noble/curves/secp256k1.js'
2
- import { sha256 } from '@noble/hashes/sha2.js'
3
- import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
4
- import { getPublicKey } from 'nostr-tools/pure'
5
-
6
- function serializeEvent (event) {
7
- return JSON.stringify([
8
- 0,
9
- event.pubkey,
10
- event.created_at,
11
- event.kind,
12
- event.tags,
13
- event.content
14
- ])
15
- }
16
-
17
- function getEventHash (event) {
18
- return sha256(new TextEncoder().encode(serializeEvent(event)))
19
- }
20
-
21
- function getSignature (eventHash, privkey) {
22
- return bytesToBase16(schnorr.sign(eventHash, privkey))
23
- }
24
-
25
- export function finalizeEvent (event, privkey, withSig = true) {
26
- event.pubkey ??= getPublicKey(privkey)
27
- const eventHash = event.id ? base16ToBytes(event.id) : getEventHash(event)
28
- event.id ??= bytesToBase16(eventHash)
29
- if (withSig) event.sig ??= getSignature(eventHash, privkey)
30
- else delete event.sig
31
- return event
32
- }