libp2r2p 0.0.1

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.
Files changed (63) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +129 -0
  3. package/base16/index.js +14 -0
  4. package/base64/index.js +29 -0
  5. package/content-key/event/index.js +85 -0
  6. package/content-key/index.js +1 -0
  7. package/content-key/services/iykc-proof.js +129 -0
  8. package/double-dh/index.js +166 -0
  9. package/ecdh/index.js +8 -0
  10. package/idb/index.js +64 -0
  11. package/idb-queue/index.js +793 -0
  12. package/index.js +21 -0
  13. package/key/index.js +139 -0
  14. package/network/index.js +68 -0
  15. package/nip44-v3/index.js +175 -0
  16. package/nip46/constants/index.js +5 -0
  17. package/nip46/helpers/frame.js +51 -0
  18. package/nip46/helpers/url.js +96 -0
  19. package/nip46/index.js +5 -0
  20. package/nip46/services/bunker-signer.js +49 -0
  21. package/nip46/services/client.js +252 -0
  22. package/nip46/services/server-session.js +225 -0
  23. package/nip46/services/transport.js +265 -0
  24. package/package.json +49 -0
  25. package/private-channel/constants/index.js +5 -0
  26. package/private-channel/helpers/chunk-size.js +61 -0
  27. package/private-channel/helpers/chunks.js +282 -0
  28. package/private-channel/helpers/event.js +68 -0
  29. package/private-channel/index.js +942 -0
  30. package/private-channel/services/received-chunks.js +398 -0
  31. package/private-message/index.js +672 -0
  32. package/private-messenger/constants/index.js +1 -0
  33. package/private-messenger/index.js +1431 -0
  34. package/private-messenger/recovery/index.js +316 -0
  35. package/relay/constants/index.js +18 -0
  36. package/relay/helpers/hll.js +62 -0
  37. package/relay/helpers/publish.js +100 -0
  38. package/relay/helpers/routing.js +36 -0
  39. package/relay/helpers/timer.js +4 -0
  40. package/relay/index.js +4 -0
  41. package/relay/services/query.js +224 -0
  42. package/relay/services/relay-connection.js +156 -0
  43. package/relay/services/relay-pool.js +908 -0
  44. package/temporary-storage/index.js +85 -0
  45. package/tests/base16.test.js +19 -0
  46. package/tests/base64.test.js +18 -0
  47. package/tests/content-key-event.test.js +48 -0
  48. package/tests/fixtures/nip44v3-vectors.json +1 -0
  49. package/tests/helpers/test-signer.js +185 -0
  50. package/tests/idb-queue.test.js +153 -0
  51. package/tests/idb.test.js +91 -0
  52. package/tests/nip44-v3.test.js +73 -0
  53. package/tests/nip46.test.js +668 -0
  54. package/tests/private-channel.test.js +1899 -0
  55. package/tests/private-message.test.js +460 -0
  56. package/tests/private-messenger.test.js +1715 -0
  57. package/tests/queries.test.js +101 -0
  58. package/tests/queue-parity.test.js +105 -0
  59. package/tests/relay-hll.test.js +32 -0
  60. package/tests/relay-pool.test.js +2067 -0
  61. package/tests/temporary-storage.test.js +89 -0
  62. package/tests/web-storage-queue.test.js +480 -0
  63. package/web-storage-queue/index.js +652 -0
@@ -0,0 +1,668 @@
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
+ })