libp2r2p 0.0.2 → 0.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/base93/index.js +193 -0
- package/index.js +1 -0
- package/nip19/index.js +305 -0
- package/package.json +26 -1
- package/AGENTS.md +0 -20
- package/tests/base16.test.js +0 -19
- package/tests/base64.test.js +0 -18
- package/tests/content-key-event.test.js +0 -48
- package/tests/fixtures/nip44v3-vectors.json +0 -1
- package/tests/helpers/test-signer.js +0 -185
- package/tests/idb-queue.test.js +0 -153
- package/tests/idb.test.js +0 -91
- package/tests/nip44-v3.test.js +0 -73
- package/tests/nip46.test.js +0 -668
- package/tests/private-channel.test.js +0 -1912
- package/tests/private-message.test.js +0 -501
- package/tests/private-messenger.test.js +0 -1737
- package/tests/queries.test.js +0 -101
- package/tests/queue-parity.test.js +0 -105
- package/tests/relay-hll.test.js +0 -32
- package/tests/relay-pool.test.js +0 -2063
- package/tests/temporary-storage.test.js +0 -89
- package/tests/web-storage-queue.test.js +0 -480
package/tests/nip46.test.js
DELETED
|
@@ -1,668 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'
|
|
4
|
-
import { decrypt, encrypt, getConversationKey } from 'nostr-tools/nip44'
|
|
5
|
-
import {
|
|
6
|
-
BunkerSigner,
|
|
7
|
-
createNostrConnectURI,
|
|
8
|
-
Nip46Client,
|
|
9
|
-
NIP46_KIND,
|
|
10
|
-
Nip46ServerSession,
|
|
11
|
-
parseBunkerUrl,
|
|
12
|
-
toBunkerUrl
|
|
13
|
-
} from '../nip46/index.js'
|
|
14
|
-
|
|
15
|
-
const RELAYS = ['wss://one.example', 'wss://two.example']
|
|
16
|
-
|
|
17
|
-
const tick = () => new Promise(resolve => setImmediate(resolve))
|
|
18
|
-
|
|
19
|
-
class FakeLiveStream {
|
|
20
|
-
#events = []
|
|
21
|
-
#waiters = []
|
|
22
|
-
#done = false
|
|
23
|
-
#ready = Promise.withResolvers()
|
|
24
|
-
#readyRelays = []
|
|
25
|
-
|
|
26
|
-
constructor (relays) {
|
|
27
|
-
this.relays = relays
|
|
28
|
-
this.ready = this.#ready.promise
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get readyRelays () {
|
|
32
|
-
return Object.freeze([...this.#readyRelays])
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setReady (relays = this.relays, errors = []) {
|
|
36
|
-
this.#readyRelays = [...relays]
|
|
37
|
-
this.#ready.resolve({ relays: Object.freeze([...relays]), errors: Object.freeze([...errors]) })
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
setReadyRelays (relays) {
|
|
41
|
-
this.#readyRelays = [...relays]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
emit (event) {
|
|
45
|
-
const waiter = this.#waiters.shift()
|
|
46
|
-
if (waiter) waiter({ value: event, done: false })
|
|
47
|
-
else this.#events.push(event)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
close () {
|
|
51
|
-
if (this.#done) return
|
|
52
|
-
this.#done = true
|
|
53
|
-
for (const waiter of this.#waiters.splice(0)) waiter({ value: undefined, done: true })
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async next () {
|
|
57
|
-
if (this.#events.length) return { value: this.#events.shift(), done: false }
|
|
58
|
-
if (this.#done) return { value: undefined, done: true }
|
|
59
|
-
return new Promise(resolve => this.#waiters.push(resolve))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async return () {
|
|
63
|
-
this.close()
|
|
64
|
-
return { value: undefined, done: true }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
[Symbol.asyncIterator] () {
|
|
68
|
-
return this
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
class FakeRelayPool {
|
|
73
|
-
constructor ({ autoReady = true } = {}) {
|
|
74
|
-
this.autoReady = autoReady
|
|
75
|
-
this.streams = []
|
|
76
|
-
this.sent = []
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
getLiveEventsGenerator (filter, relays, options) {
|
|
80
|
-
const stream = new FakeLiveStream(relays)
|
|
81
|
-
this.streams.push({ filter, relays, options, stream })
|
|
82
|
-
options.signal?.addEventListener('abort', () => stream.close(), { once: true })
|
|
83
|
-
if (this.autoReady) queueMicrotask(() => stream.setReady(relays))
|
|
84
|
-
return stream
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async sendEvent (event, relays, options) {
|
|
88
|
-
this.sent.push({ event, relays, options })
|
|
89
|
-
return {
|
|
90
|
-
success: true,
|
|
91
|
-
promise: Promise.resolve({ success: true, errors: [] })
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function matchesFilter (event, filter) {
|
|
97
|
-
if (filter.kinds?.length && !filter.kinds.includes(event.kind)) return false
|
|
98
|
-
if (filter.authors?.length && !filter.authors.includes(event.pubkey)) return false
|
|
99
|
-
for (const [key, values] of Object.entries(filter)) {
|
|
100
|
-
if (!key.startsWith('#')) continue
|
|
101
|
-
if (!event.tags.some(tag => tag[0] === key.slice(1) && values.includes(tag[1]))) return false
|
|
102
|
-
}
|
|
103
|
-
return true
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
class LoopbackRelayPool extends FakeRelayPool {
|
|
107
|
-
async sendEvent (event, relays, options) {
|
|
108
|
-
this.sent.push({ event, relays, options })
|
|
109
|
-
queueMicrotask(() => {
|
|
110
|
-
for (const entry of this.streams) {
|
|
111
|
-
if (!entry.relays.some(relay => relays.includes(relay))) continue
|
|
112
|
-
if (matchesFilter(event, entry.filter)) entry.stream.emit(event)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
return {
|
|
116
|
-
success: true,
|
|
117
|
-
promise: Promise.resolve({ success: true, errors: [] })
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function pointer (remoteSignerPubkey) {
|
|
123
|
-
return { remoteSignerPubkey, relays: RELAYS, secret: 'one-use-secret' }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function responseEvent ({ remoteSecretKey, clientPubkey, response }) {
|
|
127
|
-
return finalizeEvent({
|
|
128
|
-
kind: NIP46_KIND,
|
|
129
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
130
|
-
tags: [['p', clientPubkey]],
|
|
131
|
-
content: encrypt(JSON.stringify(response), getConversationKey(remoteSecretKey, clientPubkey))
|
|
132
|
-
}, remoteSecretKey)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function requestPayload ({ sent, remotePubkey, clientSecretKey }) {
|
|
136
|
-
return JSON.parse(decrypt(sent.event.content, getConversationKey(clientSecretKey, remotePubkey)))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
test('parses and builds direct bunker URLs without NIP-05 lookup', () => {
|
|
140
|
-
const remoteSignerPubkey = 'a'.repeat(64)
|
|
141
|
-
const value = `bunker://${remoteSignerPubkey}?relay=wss%3A%2F%2Fone.example&relay=wss%3A%2F%2Ftwo.example&secret=once`
|
|
142
|
-
const parsed = parseBunkerUrl(value)
|
|
143
|
-
|
|
144
|
-
assert.deepEqual(parsed, {
|
|
145
|
-
remoteSignerPubkey,
|
|
146
|
-
relays: RELAYS,
|
|
147
|
-
secret: 'once'
|
|
148
|
-
})
|
|
149
|
-
assert.deepEqual(parseBunkerUrl(toBunkerUrl(parsed)), parsed)
|
|
150
|
-
assert.equal(parseBunkerUrl('alice@example.com'), null)
|
|
151
|
-
assert.equal(parseBunkerUrl(`bunker://${remoteSignerPubkey}`), null)
|
|
152
|
-
|
|
153
|
-
const uri = createNostrConnectURI({
|
|
154
|
-
clientPubkey: remoteSignerPubkey,
|
|
155
|
-
relays: RELAYS,
|
|
156
|
-
secret: 'connect-secret',
|
|
157
|
-
perms: ['sign_event:1', 'nip44_encrypt'],
|
|
158
|
-
name: 'Test Client',
|
|
159
|
-
url: 'https://client.example',
|
|
160
|
-
image: 'https://client.example/icon.png'
|
|
161
|
-
})
|
|
162
|
-
const params = new URL(uri).searchParams
|
|
163
|
-
assert.equal(params.get('perms'), 'sign_event:1,nip44_encrypt')
|
|
164
|
-
assert.equal(params.get('name'), 'Test Client')
|
|
165
|
-
assert.equal(params.get('url'), 'https://client.example')
|
|
166
|
-
assert.equal(params.get('image'), 'https://client.example/icon.png')
|
|
167
|
-
assert.throws(() => createNostrConnectURI({ clientPubkey: remoteSignerPubkey, relays: RELAYS }), /INVALID_NOSTRCONNECT_URI/)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test('waits for the live listener before publishing a connect request', async () => {
|
|
171
|
-
const clientSecretKey = generateSecretKey()
|
|
172
|
-
const remoteSecretKey = generateSecretKey()
|
|
173
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
174
|
-
const relayPool = new FakeRelayPool({ autoReady: false })
|
|
175
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool, timeout: 100 })
|
|
176
|
-
const connecting = signer.connect({
|
|
177
|
-
requestedPermissions: ['sign_event:1'],
|
|
178
|
-
clientMetadata: { name: 'Test Vault', url: 'https://vault.example' }
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
await tick()
|
|
182
|
-
assert.equal(relayPool.sent.length, 0)
|
|
183
|
-
|
|
184
|
-
relayPool.streams[0].stream.setReady(RELAYS)
|
|
185
|
-
await tick()
|
|
186
|
-
assert.equal(relayPool.sent.length, 1)
|
|
187
|
-
assert.deepEqual(relayPool.sent[0].relays, RELAYS)
|
|
188
|
-
|
|
189
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
190
|
-
assert.equal(request.method, 'connect')
|
|
191
|
-
assert.deepEqual(request.params, [
|
|
192
|
-
remoteSignerPubkey,
|
|
193
|
-
'one-use-secret',
|
|
194
|
-
'sign_event:1',
|
|
195
|
-
JSON.stringify({ name: 'Test Vault', url: 'https://vault.example' })
|
|
196
|
-
])
|
|
197
|
-
|
|
198
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
199
|
-
remoteSecretKey,
|
|
200
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
201
|
-
response: { id: request.id, result: 'ack' }
|
|
202
|
-
}))
|
|
203
|
-
await tick()
|
|
204
|
-
const switchRequest = requestPayload({
|
|
205
|
-
sent: relayPool.sent[1],
|
|
206
|
-
remotePubkey: remoteSignerPubkey,
|
|
207
|
-
clientSecretKey
|
|
208
|
-
})
|
|
209
|
-
assert.equal(switchRequest.method, 'switch_relays')
|
|
210
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
211
|
-
remoteSecretKey,
|
|
212
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
213
|
-
response: { id: switchRequest.id, result: 'null' }
|
|
214
|
-
}))
|
|
215
|
-
await connecting
|
|
216
|
-
await signer.close()
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('keeps a request pending across an auth URL and ignores another signer', async () => {
|
|
220
|
-
const clientSecretKey = generateSecretKey()
|
|
221
|
-
const remoteSecretKey = generateSecretKey()
|
|
222
|
-
const wrongRemoteSecretKey = generateSecretKey()
|
|
223
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
224
|
-
const authUrls = []
|
|
225
|
-
const relayPool = new FakeRelayPool()
|
|
226
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), {
|
|
227
|
-
relayPool,
|
|
228
|
-
onAuthUrl: url => authUrls.push(url)
|
|
229
|
-
})
|
|
230
|
-
const pending = signer.sendRequest('ping', [])
|
|
231
|
-
|
|
232
|
-
await tick()
|
|
233
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
234
|
-
const invalidSignature = JSON.parse(JSON.stringify(responseEvent({
|
|
235
|
-
remoteSecretKey,
|
|
236
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
237
|
-
response: { id: request.id, result: 'forged' }
|
|
238
|
-
})))
|
|
239
|
-
invalidSignature.sig = '0'.repeat(invalidSignature.sig.length)
|
|
240
|
-
let settled = false
|
|
241
|
-
pending.finally(() => { settled = true }).catch(() => {})
|
|
242
|
-
relayPool.streams[0].stream.emit(invalidSignature)
|
|
243
|
-
await tick()
|
|
244
|
-
assert.equal(settled, false)
|
|
245
|
-
|
|
246
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
247
|
-
remoteSecretKey: wrongRemoteSecretKey,
|
|
248
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
249
|
-
response: { id: request.id, result: 'wrong' }
|
|
250
|
-
}))
|
|
251
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
252
|
-
remoteSecretKey,
|
|
253
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
254
|
-
response: { id: request.id, result: 'auth_url', error: 'https://auth.example' }
|
|
255
|
-
}))
|
|
256
|
-
await tick()
|
|
257
|
-
assert.deepEqual(authUrls, ['https://auth.example'])
|
|
258
|
-
|
|
259
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
260
|
-
remoteSecretKey,
|
|
261
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
262
|
-
response: { id: request.id, result: 'pong' }
|
|
263
|
-
}))
|
|
264
|
-
assert.equal(await pending, 'pong')
|
|
265
|
-
await signer.close()
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
test('preserves application request extensions without allowing protocol fields to be replaced', async () => {
|
|
269
|
-
const clientSecretKey = generateSecretKey()
|
|
270
|
-
const remoteSecretKey = generateSecretKey()
|
|
271
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
272
|
-
const relayPool = new FakeRelayPool()
|
|
273
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
274
|
-
const pending = signer.sendRequest('obfuscate', ['value'], {
|
|
275
|
-
extension: { tweak: ['withSharedKey', 'peer', 'scope'] }
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
await tick()
|
|
279
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
280
|
-
assert.deepEqual(request.tweak, ['withSharedKey', 'peer', 'scope'])
|
|
281
|
-
|
|
282
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
283
|
-
remoteSecretKey,
|
|
284
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
285
|
-
response: { id: request.id, result: 'ok' }
|
|
286
|
-
}))
|
|
287
|
-
assert.equal(await pending, 'ok')
|
|
288
|
-
await assert.rejects(
|
|
289
|
-
signer.sendRequest('ping', [], { extension: { id: 'not-allowed' } }),
|
|
290
|
-
/NIP46_REQUEST_EXTENSION_CANNOT_SET_ID/
|
|
291
|
-
)
|
|
292
|
-
await signer.close()
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
test('adds a relay to later requests after it becomes ready', async () => {
|
|
296
|
-
const clientSecretKey = generateSecretKey()
|
|
297
|
-
const remoteSecretKey = generateSecretKey()
|
|
298
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
299
|
-
const relayPool = new FakeRelayPool({ autoReady: false })
|
|
300
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
301
|
-
|
|
302
|
-
const first = signer.sendRequest('ping', [])
|
|
303
|
-
await tick()
|
|
304
|
-
relayPool.streams[0].stream.setReady([RELAYS[0]])
|
|
305
|
-
await tick()
|
|
306
|
-
assert.deepEqual(relayPool.sent[0].relays, [RELAYS[0]])
|
|
307
|
-
const firstRequest = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
308
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
309
|
-
remoteSecretKey,
|
|
310
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
311
|
-
response: { id: firstRequest.id, result: 'pong' }
|
|
312
|
-
}))
|
|
313
|
-
assert.equal(await first, 'pong')
|
|
314
|
-
|
|
315
|
-
relayPool.streams[0].stream.setReadyRelays(RELAYS)
|
|
316
|
-
const second = signer.sendRequest('ping', [])
|
|
317
|
-
await tick()
|
|
318
|
-
assert.deepEqual(relayPool.sent[1].relays, RELAYS)
|
|
319
|
-
const secondRequest = requestPayload({ sent: relayPool.sent[1], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
320
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
321
|
-
remoteSecretKey,
|
|
322
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
323
|
-
response: { id: secondRequest.id, result: 'pong' }
|
|
324
|
-
}))
|
|
325
|
-
assert.equal(await second, 'pong')
|
|
326
|
-
await signer.close()
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
test('rejects NIP-46 error responses', async () => {
|
|
330
|
-
const clientSecretKey = generateSecretKey()
|
|
331
|
-
const remoteSecretKey = generateSecretKey()
|
|
332
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
333
|
-
const relayPool = new FakeRelayPool()
|
|
334
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
335
|
-
const pending = signer.sendRequest('ping', [])
|
|
336
|
-
|
|
337
|
-
await tick()
|
|
338
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
339
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
340
|
-
remoteSecretKey,
|
|
341
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
342
|
-
response: { id: request.id, error: 'not allowed' }
|
|
343
|
-
}))
|
|
344
|
-
await assert.rejects(pending, /not allowed/)
|
|
345
|
-
await signer.close()
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
test('verifies signed events returned by the bunker', async () => {
|
|
349
|
-
const clientSecretKey = generateSecretKey()
|
|
350
|
-
const remoteSecretKey = generateSecretKey()
|
|
351
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
352
|
-
const relayPool = new FakeRelayPool()
|
|
353
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
354
|
-
const signing = signer.signEvent({ kind: 1, created_at: 10, tags: [], content: 'hello' })
|
|
355
|
-
|
|
356
|
-
await tick()
|
|
357
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
358
|
-
const signed = finalizeEvent({ kind: 1, created_at: 10, tags: [], content: 'hello' }, remoteSecretKey)
|
|
359
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
360
|
-
remoteSecretKey,
|
|
361
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
362
|
-
response: { id: request.id, result: JSON.stringify(signed) }
|
|
363
|
-
}))
|
|
364
|
-
|
|
365
|
-
assert.deepEqual(await signing, signed)
|
|
366
|
-
await signer.close()
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
test('validates the remote public key and closes after logout', async () => {
|
|
370
|
-
const clientSecretKey = generateSecretKey()
|
|
371
|
-
const remoteSecretKey = generateSecretKey()
|
|
372
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
373
|
-
const userPubkey = getPublicKey(generateSecretKey())
|
|
374
|
-
const relayPool = new FakeRelayPool()
|
|
375
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
376
|
-
|
|
377
|
-
const publicKey = signer.getPublicKey()
|
|
378
|
-
await tick()
|
|
379
|
-
const publicKeyRequest = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
380
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
381
|
-
remoteSecretKey,
|
|
382
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
383
|
-
response: { id: publicKeyRequest.id, result: userPubkey }
|
|
384
|
-
}))
|
|
385
|
-
assert.equal(await publicKey, userPubkey)
|
|
386
|
-
|
|
387
|
-
const loggingOut = signer.logout()
|
|
388
|
-
await tick()
|
|
389
|
-
const logoutRequest = requestPayload({ sent: relayPool.sent[1], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
390
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
391
|
-
remoteSecretKey,
|
|
392
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
393
|
-
response: { id: logoutRequest.id, result: 'ack' }
|
|
394
|
-
}))
|
|
395
|
-
await loggingOut
|
|
396
|
-
await assert.rejects(signer.ping(), /NIP46_CLOSED/)
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
test('switches relay listeners before sending later requests to the new set', async () => {
|
|
400
|
-
const clientSecretKey = generateSecretKey()
|
|
401
|
-
const remoteSecretKey = generateSecretKey()
|
|
402
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
403
|
-
const relayPool = new FakeRelayPool()
|
|
404
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(remoteSignerPubkey), { relayPool })
|
|
405
|
-
const switching = signer.switchRelays()
|
|
406
|
-
|
|
407
|
-
await tick()
|
|
408
|
-
const switchRequest = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
409
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
410
|
-
remoteSecretKey,
|
|
411
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
412
|
-
response: { id: switchRequest.id, result: JSON.stringify(['wss://new.example']) }
|
|
413
|
-
}))
|
|
414
|
-
|
|
415
|
-
assert.equal(await switching, true)
|
|
416
|
-
assert.deepEqual(signer.pointer.relays, ['wss://new.example'])
|
|
417
|
-
assert.deepEqual(relayPool.streams[1].relays, ['wss://new.example'])
|
|
418
|
-
|
|
419
|
-
const pending = signer.sendRequest('ping', [])
|
|
420
|
-
await tick()
|
|
421
|
-
assert.deepEqual(relayPool.sent[1].relays, ['wss://new.example'])
|
|
422
|
-
const pingRequest = requestPayload({ sent: relayPool.sent[1], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
423
|
-
relayPool.streams[1].stream.emit(responseEvent({
|
|
424
|
-
remoteSecretKey,
|
|
425
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
426
|
-
response: { id: pingRequest.id, result: 'pong' }
|
|
427
|
-
}))
|
|
428
|
-
assert.equal(await pending, 'pong')
|
|
429
|
-
await signer.close()
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
test('discovers and validates a client-initiated connection URI', async () => {
|
|
433
|
-
const clientSecretKey = generateSecretKey()
|
|
434
|
-
const remoteSecretKey = generateSecretKey()
|
|
435
|
-
const clientPubkey = getPublicKey(clientSecretKey)
|
|
436
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
437
|
-
const relayPool = new FakeRelayPool()
|
|
438
|
-
const uri = createNostrConnectURI({ clientPubkey, relays: RELAYS, secret: 'connection-secret' })
|
|
439
|
-
const creating = BunkerSigner.fromURI(clientSecretKey, uri, {
|
|
440
|
-
relayPool,
|
|
441
|
-
timeout: 100
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
await tick()
|
|
445
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
446
|
-
remoteSecretKey,
|
|
447
|
-
clientPubkey,
|
|
448
|
-
response: { id: 'ignored', result: 'wrong-secret' }
|
|
449
|
-
}))
|
|
450
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
451
|
-
remoteSecretKey,
|
|
452
|
-
clientPubkey,
|
|
453
|
-
response: { id: 'connected', result: 'connection-secret' }
|
|
454
|
-
}))
|
|
455
|
-
|
|
456
|
-
await tick()
|
|
457
|
-
const switchRequest = requestPayload({
|
|
458
|
-
sent: relayPool.sent[0],
|
|
459
|
-
remotePubkey: remoteSignerPubkey,
|
|
460
|
-
clientSecretKey
|
|
461
|
-
})
|
|
462
|
-
relayPool.streams[1].stream.emit(responseEvent({
|
|
463
|
-
remoteSecretKey,
|
|
464
|
-
clientPubkey,
|
|
465
|
-
response: { id: switchRequest.id, result: 'null' }
|
|
466
|
-
}))
|
|
467
|
-
|
|
468
|
-
const signer = await creating
|
|
469
|
-
assert.equal(signer.pointer.remoteSignerPubkey, remoteSignerPubkey)
|
|
470
|
-
assert.equal(relayPool.streams.length, 2)
|
|
471
|
-
await signer.close()
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
test('briefly attempts relay switching after a client-initiated connection', async () => {
|
|
475
|
-
const clientSecretKey = generateSecretKey()
|
|
476
|
-
const remoteSecretKey = generateSecretKey()
|
|
477
|
-
const clientPubkey = getPublicKey(clientSecretKey)
|
|
478
|
-
const relayPool = new FakeRelayPool()
|
|
479
|
-
const uri = createNostrConnectURI({ clientPubkey, relays: RELAYS, secret: 'connection-secret' })
|
|
480
|
-
const creating = BunkerSigner.fromURI(clientSecretKey, uri, { relayPool, timeout: 100 })
|
|
481
|
-
|
|
482
|
-
await tick()
|
|
483
|
-
relayPool.streams[0].stream.emit(responseEvent({
|
|
484
|
-
remoteSecretKey,
|
|
485
|
-
clientPubkey,
|
|
486
|
-
response: { id: 'connected', result: 'connection-secret' }
|
|
487
|
-
}))
|
|
488
|
-
await tick()
|
|
489
|
-
|
|
490
|
-
const remoteSignerPubkey = getPublicKey(remoteSecretKey)
|
|
491
|
-
const request = requestPayload({ sent: relayPool.sent[0], remotePubkey: remoteSignerPubkey, clientSecretKey })
|
|
492
|
-
assert.equal(request.method, 'switch_relays')
|
|
493
|
-
relayPool.streams[1].stream.emit(responseEvent({
|
|
494
|
-
remoteSecretKey,
|
|
495
|
-
clientPubkey,
|
|
496
|
-
response: { id: request.id, result: 'null' }
|
|
497
|
-
}))
|
|
498
|
-
|
|
499
|
-
const signer = await creating
|
|
500
|
-
await signer.close()
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
test('times out or aborts a client-initiated connection that has no matching secret', async () => {
|
|
504
|
-
const clientSecretKey = generateSecretKey()
|
|
505
|
-
const uri = createNostrConnectURI({
|
|
506
|
-
clientPubkey: getPublicKey(clientSecretKey),
|
|
507
|
-
relays: RELAYS,
|
|
508
|
-
secret: 'connection-secret'
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
await assert.rejects(
|
|
512
|
-
BunkerSigner.fromURI(clientSecretKey, uri, {
|
|
513
|
-
relayPool: new FakeRelayPool(),
|
|
514
|
-
timeout: 10
|
|
515
|
-
}),
|
|
516
|
-
/NIP46_CONNECTION_TIMEOUT/
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
const controller = new AbortController()
|
|
520
|
-
const pending = BunkerSigner.fromURI(clientSecretKey, uri, {
|
|
521
|
-
relayPool: new FakeRelayPool(),
|
|
522
|
-
timeout: null,
|
|
523
|
-
signal: controller.signal
|
|
524
|
-
})
|
|
525
|
-
await tick()
|
|
526
|
-
controller.abort()
|
|
527
|
-
await assert.rejects(pending, /Aborted/)
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
test('close rejects a pending NIP-46 request', async () => {
|
|
531
|
-
const clientSecretKey = generateSecretKey()
|
|
532
|
-
const remoteSecretKey = generateSecretKey()
|
|
533
|
-
const signer = BunkerSigner.fromBunker(clientSecretKey, pointer(getPublicKey(remoteSecretKey)), {
|
|
534
|
-
relayPool: new FakeRelayPool()
|
|
535
|
-
})
|
|
536
|
-
const pending = signer.sendRequest('ping', [])
|
|
537
|
-
await tick()
|
|
538
|
-
await signer.close()
|
|
539
|
-
await assert.rejects(pending, /NIP46_CLOSED/)
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
test('public client and server roles support bidirectional custom RPC and relay switching', async () => {
|
|
543
|
-
const relayPool = new LoopbackRelayPool()
|
|
544
|
-
const serverSecretKey = generateSecretKey()
|
|
545
|
-
const clientSecretKey = generateSecretKey()
|
|
546
|
-
const serverPubkey = getPublicKey(serverSecretKey)
|
|
547
|
-
const connected = []
|
|
548
|
-
const serverRequests = []
|
|
549
|
-
const clientRequests = []
|
|
550
|
-
const server = new Nip46ServerSession(serverSecretKey, {
|
|
551
|
-
relays: RELAYS,
|
|
552
|
-
secret: 'one-use-secret',
|
|
553
|
-
relayPool,
|
|
554
|
-
onConnect: value => connected.push(value),
|
|
555
|
-
onRequest: ({ method, params }) => {
|
|
556
|
-
serverRequests.push({ method, params })
|
|
557
|
-
if (method !== 'server_echo') throw new Error('unsupported custom method')
|
|
558
|
-
return JSON.stringify(params)
|
|
559
|
-
}
|
|
560
|
-
})
|
|
561
|
-
await server.start({ timeout: 100 })
|
|
562
|
-
|
|
563
|
-
const client = new Nip46Client(clientSecretKey, pointer(serverPubkey), {
|
|
564
|
-
relayPool,
|
|
565
|
-
onRequest: ({ method, params }) => {
|
|
566
|
-
clientRequests.push({ method, params })
|
|
567
|
-
if (method !== 'client_echo') throw new Error('unsupported custom method')
|
|
568
|
-
return params[0]
|
|
569
|
-
}
|
|
570
|
-
})
|
|
571
|
-
await client.connect({
|
|
572
|
-
requestedPermissions: ['server_echo'],
|
|
573
|
-
clientMetadata: { name: 'Pairing client' },
|
|
574
|
-
timeout: 100
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
assert.equal(server.clientPubkey, client.clientPubkey)
|
|
578
|
-
assert.deepEqual(connected, [{
|
|
579
|
-
peerPubkey: client.clientPubkey,
|
|
580
|
-
requestedPermissions: ['server_echo'],
|
|
581
|
-
clientMetadata: { name: 'Pairing client' }
|
|
582
|
-
}])
|
|
583
|
-
await client.ping({ timeout: 100 })
|
|
584
|
-
assert.equal(
|
|
585
|
-
await client.sendRequest('server_echo', ['one', 'two'], { timeout: 100 }),
|
|
586
|
-
JSON.stringify(['one', 'two'])
|
|
587
|
-
)
|
|
588
|
-
assert.equal(await server.sendRequest('client_echo', ['back'], { timeout: 100 }), 'back')
|
|
589
|
-
assert.deepEqual(serverRequests, [{ method: 'server_echo', params: ['one', 'two'] }])
|
|
590
|
-
assert.deepEqual(clientRequests, [{ method: 'client_echo', params: ['back'] }])
|
|
591
|
-
|
|
592
|
-
await server.updateRelays(['wss://new.example'])
|
|
593
|
-
assert.equal(await server.sendRequest('client_echo', ['before-switch'], { timeout: 100 }), 'before-switch')
|
|
594
|
-
assert.equal(await client.switchRelays({ timeout: 100 }), true)
|
|
595
|
-
assert.deepEqual(client.pointer.relays, ['wss://new.example'])
|
|
596
|
-
assert.deepEqual(server.relays, ['wss://new.example'])
|
|
597
|
-
assert.equal(await server.sendRequest('client_echo', ['after-switch'], { timeout: 100 }), 'after-switch')
|
|
598
|
-
assert.deepEqual(clientRequests, [
|
|
599
|
-
{ method: 'client_echo', params: ['back'] },
|
|
600
|
-
{ method: 'client_echo', params: ['before-switch'] },
|
|
601
|
-
{ method: 'client_echo', params: ['after-switch'] }
|
|
602
|
-
])
|
|
603
|
-
|
|
604
|
-
await client.logout({ timeout: 100 })
|
|
605
|
-
await server.close()
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
test('server session keeps an invalid secret from consuming its single client slot', async () => {
|
|
609
|
-
const relayPool = new LoopbackRelayPool()
|
|
610
|
-
const serverSecretKey = generateSecretKey()
|
|
611
|
-
const serverPubkey = getPublicKey(serverSecretKey)
|
|
612
|
-
const server = new Nip46ServerSession(serverSecretKey, {
|
|
613
|
-
relays: RELAYS,
|
|
614
|
-
secret: 'correct-secret',
|
|
615
|
-
relayPool
|
|
616
|
-
})
|
|
617
|
-
await server.start({ timeout: 100 })
|
|
618
|
-
|
|
619
|
-
const invalid = new Nip46Client(generateSecretKey(), {
|
|
620
|
-
remoteSignerPubkey: serverPubkey,
|
|
621
|
-
relays: RELAYS,
|
|
622
|
-
secret: 'wrong-secret'
|
|
623
|
-
}, { relayPool })
|
|
624
|
-
await assert.rejects(invalid.connect({ timeout: 100 }), /NIP46_INVALID_SECRET/)
|
|
625
|
-
assert.equal(server.clientPubkey, null)
|
|
626
|
-
await invalid.close()
|
|
627
|
-
|
|
628
|
-
const accepted = new Nip46Client(generateSecretKey(), {
|
|
629
|
-
remoteSignerPubkey: serverPubkey,
|
|
630
|
-
relays: RELAYS,
|
|
631
|
-
secret: 'correct-secret'
|
|
632
|
-
}, { relayPool })
|
|
633
|
-
await accepted.connect({ timeout: 100 })
|
|
634
|
-
|
|
635
|
-
const rejected = new Nip46Client(generateSecretKey(), {
|
|
636
|
-
remoteSignerPubkey: serverPubkey,
|
|
637
|
-
relays: RELAYS,
|
|
638
|
-
secret: 'correct-secret'
|
|
639
|
-
}, { relayPool })
|
|
640
|
-
await assert.rejects(rejected.connect({ timeout: 100 }), /NIP46_ALREADY_CONNECTED/)
|
|
641
|
-
|
|
642
|
-
await rejected.close()
|
|
643
|
-
await accepted.close()
|
|
644
|
-
await server.close()
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
test('generic client removes pending requests after timeout and abort', async () => {
|
|
648
|
-
const clientSecretKey = generateSecretKey()
|
|
649
|
-
const remoteSignerPubkey = getPublicKey(generateSecretKey())
|
|
650
|
-
const client = new Nip46Client(clientSecretKey, pointer(remoteSignerPubkey), {
|
|
651
|
-
relayPool: new FakeRelayPool()
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
await assert.rejects(
|
|
655
|
-
client.sendRequest('never_replies', [], { timeout: 5 }),
|
|
656
|
-
/NIP46_REQUEST_TIMEOUT/
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
const controller = new AbortController()
|
|
660
|
-
const pending = client.sendRequest('also_never_replies', [], {
|
|
661
|
-
timeout: null,
|
|
662
|
-
signal: controller.signal
|
|
663
|
-
})
|
|
664
|
-
await tick()
|
|
665
|
-
controller.abort()
|
|
666
|
-
await assert.rejects(pending, /Aborted/)
|
|
667
|
-
await client.close()
|
|
668
|
-
})
|