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 +3 -4
- package/src/services/bunker-session-codec.js +1 -1
- package/src/services/bunker-session.js +1 -1
- package/src/services/irfs-upload.js +5 -6
- package/src/services/nostr-relays.js +8 -141
- package/src/services/nostr-secret-key.js +1 -1
- package/src/services/nostr-signer.js +10 -27
- package/src/services/site-manifest.js +1 -1
- package/src/helpers/nip01.js +0 -32
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.
|
|
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.
|
|
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 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 } =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
// Connections aren't authenticated, thus no need to split by authed user
|
|
158
|
-
export default new NostrRelays()
|
|
25
|
+
export default relayPool
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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
|
-
|
|
46
|
-
setEncryptedDotenvValue('NOSTR_SECRET_KEY', nsecEncode(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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({
|
package/src/helpers/nip01.js
DELETED
|
@@ -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
|
-
}
|