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,460 @@
1
+ import { afterEach, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { finalizeEvent, generateSecretKey, getEventHash } from 'nostr-tools'
4
+ import {
5
+ ASK_KIND,
6
+ REPLY_KIND,
7
+ TELL_KIND,
8
+ broadcastEvent,
9
+ broadcastNymEvent,
10
+ broadcastNymRumor,
11
+ ask,
12
+ broadcastRumor,
13
+ parseRumorContent,
14
+ reply,
15
+ tell,
16
+ unwatch,
17
+ watch,
18
+ yell
19
+ } from '../private-message/index.js'
20
+ import { keypairFromSeckey } from '../key/index.js'
21
+
22
+ const data = new Map()
23
+ globalThis.localStorage = {
24
+ clear: () => data.clear(),
25
+ getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
26
+ removeItem: key => { data.delete(String(key)) },
27
+ setItem: (key, value) => { data.set(String(key), String(value)) }
28
+ }
29
+
30
+ afterEach(() => {
31
+ unwatch()
32
+ globalThis.localStorage.clear()
33
+ })
34
+
35
+ function signer (pubkey) {
36
+ return {
37
+ getPublicKey: () => pubkey,
38
+ withSharedKey: () => ({})
39
+ }
40
+ }
41
+
42
+ function pubkeyFixture (index) {
43
+ return index.toString(16).padStart(64, '0')
44
+ }
45
+
46
+ function fakeSubscribeFactory () {
47
+ const calls = []
48
+ const closed = []
49
+ const fakeSubscribe = options => {
50
+ calls.push(options)
51
+ return {
52
+ close: () => closed.push(options)
53
+ }
54
+ }
55
+ return { calls, closed, fakeSubscribe }
56
+ }
57
+
58
+ test('watch merges overlapping relay subscriptions by channel author', async () => {
59
+ const { calls, closed, fakeSubscribe } = fakeSubscribeFactory()
60
+ const channel1 = signer('channel1')
61
+ const reader1 = signer('reader1')
62
+ await watch({
63
+ channels: ['channel1'],
64
+ relays: ['wss://a.example'],
65
+ receiverSigner: signer('receiver'),
66
+ privateChannelSigner: channel1,
67
+ privateChannelReaderSigner: reader1,
68
+ privateChannelReaderPubkey: 'reader1',
69
+ _subscribe: fakeSubscribe
70
+ })
71
+ await watch({
72
+ channels: ['channel1'],
73
+ relays: ['wss://a.example'],
74
+ receiverSigner: signer('receiver'),
75
+ privateChannelSigner: channel1,
76
+ privateChannelReaderSigner: reader1,
77
+ privateChannelReaderPubkey: 'reader1',
78
+ _subscribe: fakeSubscribe
79
+ })
80
+ await watch({
81
+ channels: ['channel2'],
82
+ relays: ['wss://a.example', 'wss://b.example'],
83
+ receiverSigner: signer('receiver'),
84
+ privateChannelSigner: signer('channel2'),
85
+ privateChannelReaderSigner: signer('reader2'),
86
+ privateChannelReaderPubkey: 'reader2',
87
+ mode: 'seeder',
88
+ _subscribe: fakeSubscribe
89
+ })
90
+
91
+ assert.equal(calls.length, 3)
92
+ assert.deepEqual(calls[0].privateChannelPubkeys, ['channel1'])
93
+ assert.deepEqual(calls[0].relays, ['wss://a.example'])
94
+ assert.equal(calls[0].limit, 0)
95
+ assert.equal(calls[0].liveOnly, true)
96
+ assert.deepEqual(calls[1].privateChannelPubkeys.sort(), ['channel1', 'channel2'])
97
+ assert.deepEqual(calls[1].relays, ['wss://a.example'])
98
+ assert.deepEqual(calls[1].modeByPubkey, { channel1: 'leecher', channel2: 'seeder' })
99
+ assert.deepEqual(Object.fromEntries(Object.entries(calls[1].privateChannelReaderSignersByPubkey).map(([channel, nextSigner]) => [channel, nextSigner.getPublicKey()])), {
100
+ channel1: 'reader1',
101
+ channel2: 'reader2'
102
+ })
103
+ assert.deepEqual(calls[1].privateChannelReaderPubkeysByPubkey, {
104
+ channel1: 'reader1',
105
+ channel2: 'reader2'
106
+ })
107
+ assert.deepEqual(calls[2].privateChannelPubkeys, ['channel2'])
108
+ assert.deepEqual(calls[2].relays, ['wss://b.example'])
109
+
110
+ await new Promise(resolve => setTimeout(resolve, 550))
111
+ assert.equal(closed.length, 1)
112
+ assert.deepEqual(closed[0].privateChannelPubkeys, ['channel1'])
113
+ })
114
+
115
+ test('watch refresh keeps shared relay subscriptions and gracefully closes removed relays', async () => {
116
+ const { calls, closed, fakeSubscribe } = fakeSubscribeFactory()
117
+ await watch({
118
+ channels: ['channel1'],
119
+ relays: ['wss://a.example', 'wss://b.example'],
120
+ receiverSigner: signer('receiver'),
121
+ privateChannelSigner: signer('channel1'),
122
+ _subscribe: fakeSubscribe
123
+ })
124
+ await watch({
125
+ channels: ['channel1'],
126
+ relays: ['wss://b.example', 'wss://c.example'],
127
+ receiverSigner: signer('receiver'),
128
+ privateChannelSigner: signer('channel1'),
129
+ _subscribe: fakeSubscribe
130
+ })
131
+
132
+ assert.equal(calls.length, 3)
133
+ assert.deepEqual(calls.map(call => call.relays[0]), ['wss://a.example', 'wss://b.example', 'wss://c.example'])
134
+ assert.equal(closed.length, 0)
135
+
136
+ await new Promise(resolve => setTimeout(resolve, 550))
137
+ assert.equal(closed.length, 1)
138
+ assert.deepEqual(closed[0].relays, ['wss://a.example'])
139
+ })
140
+
141
+ test('ask requires watching the sender private channel first', async () => {
142
+ await assert.rejects(
143
+ () => ask({
144
+ senderSigner: signer('sender'),
145
+ privateChannelSigner: signer('sender-channel'),
146
+ receiverPubkey: 'receiver',
147
+ relays: ['wss://relay.example'],
148
+ message: { code: 'PING' },
149
+ _publish: async () => ({ results: [] })
150
+ }),
151
+ /PRIVATE_MESSAGE_NOT_WATCHING/
152
+ )
153
+ })
154
+
155
+ test('ask from a reader-only channel requires a writer signer', async () => {
156
+ const { fakeSubscribe } = fakeSubscribeFactory()
157
+ await watch({
158
+ channels: ['writer-channel'],
159
+ relays: ['wss://relay.example'],
160
+ receiverSigner: signer('sender'),
161
+ privateChannelSigner: null,
162
+ privateChannelReaderSigner: signer('reader'),
163
+ _subscribe: fakeSubscribe
164
+ })
165
+
166
+ await assert.rejects(
167
+ () => ask({
168
+ senderSigner: signer('sender'),
169
+ privateChannelSigner: null,
170
+ receiverPubkey: 'receiver',
171
+ relays: ['wss://relay.example'],
172
+ message: { code: 'PING' },
173
+ _publish: async () => ({ results: [] })
174
+ }),
175
+ /PRIVATE_CHANNEL_WRITER_REQUIRED/
176
+ )
177
+ })
178
+
179
+ test('watch dispatches content key usage callbacks', async () => {
180
+ const { calls, fakeSubscribe } = fakeSubscribeFactory()
181
+ const usages = []
182
+ await watch({
183
+ channels: ['channel1'],
184
+ relays: ['wss://relay.example'],
185
+ receiverSigner: signer('receiver'),
186
+ privateChannelSigner: signer('channel1'),
187
+ onContentKeyUsage: usage => usages.push(usage),
188
+ _subscribe: fakeSubscribe
189
+ })
190
+
191
+ calls[0].onContentKeyUsage({
192
+ channelPubkey: 'channel1',
193
+ direction: 'sent',
194
+ contentKeyPubkey: ''
195
+ })
196
+
197
+ assert.deepEqual(usages, [{ channelPubkey: 'channel1', direction: 'sent', contentKeyPubkey: '' }])
198
+ })
199
+
200
+ test('ask publishes an ask rumor and watch dispatches the reply with its question', async () => {
201
+ const { calls, fakeSubscribe } = fakeSubscribeFactory()
202
+ const replies = []
203
+ let published = null
204
+ const senderPubkey = pubkeyFixture(1)
205
+ await watch({
206
+ channels: ['sender-channel'],
207
+ relays: ['wss://relay.example'],
208
+ receiverSigner: signer(senderPubkey),
209
+ privateChannelSigner: signer('sender-channel'),
210
+ onReply: event => replies.push(event),
211
+ _subscribe: fakeSubscribe
212
+ })
213
+ const result = await ask({
214
+ senderSigner: signer(senderPubkey),
215
+ privateChannelSigner: signer('sender-channel'),
216
+ privateChannelReaderPubkey: 'reader-channel',
217
+ receiverPubkey: 'receiver',
218
+ relays: ['wss://relay.example'],
219
+ temporaryStorageArea: globalThis.localStorage,
220
+ message: { code: 'PING', payload: { ok: true } },
221
+ _publish: async options => {
222
+ published = options
223
+ return { results: [{ success: true }] }
224
+ }
225
+ })
226
+
227
+ assert.equal(published.event.kind, ASK_KIND)
228
+ assert.equal(published.event.sig, undefined)
229
+ assert.equal(published.event.id, undefined)
230
+ assert.equal(published.event.pubkey, undefined)
231
+ assert.equal(published.receiverTag, 'receiver')
232
+ assert.deepEqual(published.receivers, ['receiver'])
233
+ assert.equal(published.privateChannelReaderPubkey, 'reader-channel')
234
+ assert.equal(published.temporaryStorageArea, globalThis.localStorage)
235
+ assert.deepEqual(JSON.parse(published.event.content), { ok: true })
236
+ assert.deepEqual(published.event.tags, [['r', 'receiver'], ['h', 'PING']])
237
+ assert.equal(result.question.pubkey, senderPubkey)
238
+ assert.equal(result.question.id, getEventHash({ ...published.event, pubkey: senderPubkey }))
239
+ assert.throws(() => getEventHash(published.event), /wrong or missing properties/)
240
+
241
+ calls[0].onEvent({
242
+ kind: REPLY_KIND,
243
+ id: 'reply-id',
244
+ pubkey: 'receiver',
245
+ created_at: 1,
246
+ tags: [['q', result.question.id]],
247
+ content: 'pong'
248
+ }, { created_at: 2 }, { channelPubkey: 'sender-channel' })
249
+
250
+ assert.equal(replies.length, 1)
251
+ assert.equal(replies[0].question, undefined)
252
+ assert.equal(replies[0].questionId, result.question.id)
253
+ assert.equal(replies[0].reply.pubkey, 'receiver')
254
+ assert.deepEqual(replies[0].payload, { payload: 'pong' })
255
+ })
256
+
257
+ test('reply tell and yell publish recognizable private message rumors', async () => {
258
+ const published = []
259
+ const bobPubkey = pubkeyFixture(3)
260
+ const _publish = async options => {
261
+ published.push(options)
262
+ return { results: [] }
263
+ }
264
+ const question = {
265
+ id: 'question-id',
266
+ pubkey: 'alice',
267
+ kind: ASK_KIND,
268
+ created_at: 1,
269
+ tags: [['r', 'bob']],
270
+ content: '{}'
271
+ }
272
+
273
+ const replyResult = await reply({ senderSigner: signer(bobPubkey), question, relays: ['wss://relay.example'], payload: 'answer', _publish })
274
+ const tellResult = await tell({ senderSigner: signer(bobPubkey), receiverPubkey: 'alice', relays: ['wss://relay.example'], payload: 'note', _publish })
275
+ const yellResult = await yell({ senderSigner: signer(bobPubkey), receiverPubkeys: ['alice', 'carol'], relays: ['wss://relay.example'], payload: 'broadcast', _publish })
276
+ const rawResult = await broadcastRumor({
277
+ senderSigner: signer(bobPubkey),
278
+ receiverPubkeys: ['alice', 'carol'],
279
+ relays: ['wss://relay.example'],
280
+ rumor: { kind: 9001, created_at: 22, tags: [['x', '1']], content: 'raw' },
281
+ _publish
282
+ })
283
+ const signedEvent = finalizeEvent({ kind: 9002, created_at: 23, tags: [['x', '2']], content: 'signed' }, generateSecretKey())
284
+ const signedResult = await broadcastEvent({
285
+ senderSigner: signer(bobPubkey),
286
+ receiverPubkeys: ['alice', 'carol'],
287
+ relays: ['wss://relay.example'],
288
+ event: signedEvent,
289
+ _publish
290
+ })
291
+
292
+ assert.equal(published[0].event.kind, REPLY_KIND)
293
+ assert.equal(published[0].event.id, undefined)
294
+ assert.equal(published[0].event.pubkey, undefined)
295
+ assert.equal(replyResult.reply.pubkey, bobPubkey)
296
+ assert.equal(replyResult.reply.id, getEventHash({ ...published[0].event, pubkey: bobPubkey }))
297
+ assert.deepEqual(published[0].event.tags, [['q', 'question-id'], ['r', 'alice']])
298
+ assert.equal(published[0].receiverTag, 'alice')
299
+ assert.equal(published[1].event.kind, TELL_KIND)
300
+ assert.equal(tellResult.tell.id, getEventHash({ ...published[1].event, pubkey: bobPubkey }))
301
+ assert.deepEqual(published[1].event.tags, [['r', 'alice']])
302
+ assert.equal(published[1].receiverTag, 'alice')
303
+ assert.equal(published[2].event.kind, TELL_KIND)
304
+ assert.equal(yellResult.yell.id, getEventHash({ ...published[2].event, pubkey: bobPubkey }))
305
+ assert.deepEqual(published[2].event.tags, [])
306
+ assert.equal(published[2].receiverTag, '')
307
+ assert.deepEqual(published[2].receivers, ['alice', 'carol'])
308
+ assert.equal(published[3].event.kind, 9001)
309
+ assert.equal(published[3].event.created_at, 22)
310
+ assert.equal(published[3].event.content, 'raw')
311
+ assert.equal(published[3].event.id, undefined)
312
+ assert.equal(published[3].event.pubkey, undefined)
313
+ assert.equal(published[3].event.sig, undefined)
314
+ assert.deepEqual(published[3].event.tags, [['x', '1']])
315
+ assert.equal(published[3].receiverTag, '')
316
+ assert.deepEqual(published[3].receivers, ['alice', 'carol'])
317
+ assert.equal(rawResult.rumor.pubkey, bobPubkey)
318
+ assert.equal(rawResult.rumor.id, getEventHash({ ...published[3].event, pubkey: bobPubkey }))
319
+ assert.deepEqual(published[4].event, signedEvent)
320
+ assert.deepEqual(published[4].receivers, ['alice', 'carol'])
321
+ assert.deepEqual(signedResult.event, signedEvent)
322
+ })
323
+
324
+ test('private message sends return generated deletion keys and forward supplied pubkeys', async () => {
325
+ const published = []
326
+ const _publish = async options => {
327
+ published.push(options)
328
+ return { results: [] }
329
+ }
330
+ const suppliedPubkey = pubkeyFixture(99).toUpperCase()
331
+
332
+ const supplied = await tell({
333
+ senderSigner: signer(pubkeyFixture(3)),
334
+ receiverPubkey: 'alice',
335
+ relays: ['wss://relay.example'],
336
+ payload: 'supplied',
337
+ deletionPubkey: suppliedPubkey,
338
+ _publish
339
+ })
340
+ const generated = await broadcastNymRumor({
341
+ nymSigner: signer(pubkeyFixture(4)),
342
+ privateChannelSigner: signer(pubkeyFixture(5)),
343
+ relays: ['wss://relay.example'],
344
+ rumor: { kind: 9001, created_at: 1, tags: [], content: 'generated' },
345
+ _publish
346
+ })
347
+
348
+ assert.equal(supplied.deletionPubkey, suppliedPubkey.toLowerCase())
349
+ assert.equal('deletionSeckey' in supplied, false)
350
+ assert.equal(published[0].deletionPubkey, suppliedPubkey.toLowerCase())
351
+ assert.match(generated.deletionPubkey, /^[0-9a-f]{64}$/)
352
+ assert.match(generated.deletionSeckey, /^[0-9a-f]{64}$/)
353
+ assert.equal(keypairFromSeckey(generated.deletionSeckey).pubkey, generated.deletionPubkey)
354
+ assert.equal(published[1].deletionPubkey, generated.deletionPubkey)
355
+
356
+ await assert.rejects(
357
+ () => tell({
358
+ senderSigner: signer(pubkeyFixture(3)),
359
+ receiverPubkey: 'alice',
360
+ relays: ['wss://relay.example'],
361
+ payload: 'bad key',
362
+ deletionPubkey: 'not-a-pubkey',
363
+ _publish
364
+ }),
365
+ /INVALID_DELETION_PUBKEY/
366
+ )
367
+ })
368
+
369
+ test('broadcastRumor validates normalized unsigned rumors before publishing', async () => {
370
+ let published = false
371
+
372
+ await assert.rejects(
373
+ () => broadcastRumor({
374
+ senderSigner: signer(pubkeyFixture(4)),
375
+ receiverPubkeys: ['alice'],
376
+ relays: ['wss://relay.example'],
377
+ rumor: { kind: 9001, created_at: 22, tags: ['not-a-tag'], content: 'raw' },
378
+ _publish: async () => { published = true }
379
+ }),
380
+ /INVALID_RUMOR/
381
+ )
382
+
383
+ assert.equal(published, false)
384
+ })
385
+
386
+ test('broadcastEvent refuses unsigned or invalid signed events', async () => {
387
+ const valid = finalizeEvent({ kind: 9002, created_at: 23, tags: [], content: 'signed' }, generateSecretKey())
388
+ let published = false
389
+
390
+ await assert.rejects(
391
+ () => broadcastEvent({
392
+ senderSigner: signer(pubkeyFixture(5)),
393
+ receiverPubkeys: ['alice'],
394
+ relays: ['wss://relay.example'],
395
+ event: { ...valid, content: 'tampered' },
396
+ _publish: async () => { published = true }
397
+ }),
398
+ /INVALID_SIGNED_EVENT/
399
+ )
400
+ await assert.rejects(
401
+ () => broadcastEvent({
402
+ senderSigner: signer(pubkeyFixture(5)),
403
+ receiverPubkeys: ['alice'],
404
+ relays: ['wss://relay.example'],
405
+ event: { kind: 9002, created_at: 23, tags: [], content: 'unsigned' },
406
+ _publish: async () => { published = true }
407
+ }),
408
+ /INVALID_SIGNED_EVENT/
409
+ )
410
+
411
+ assert.equal(published, false)
412
+ })
413
+
414
+ test('nym broadcasts publish through the nym channel path without receivers', async () => {
415
+ const published = []
416
+ const nymPubkey = pubkeyFixture(6)
417
+ const channelPubkey = pubkeyFixture(7)
418
+ const authorEvent = finalizeEvent({ kind: 9002, created_at: 30, tags: [], content: 'signed by another key' }, generateSecretKey())
419
+ const _publish = async options => {
420
+ published.push(options)
421
+ return { results: [] }
422
+ }
423
+
424
+ const rumorResult = await broadcastNymRumor({
425
+ nymSigner: signer(nymPubkey),
426
+ privateChannelSigner: signer(channelPubkey),
427
+ relays: ['wss://relay.example'],
428
+ rumor: { kind: 9001, created_at: 29, tags: [['x', 'nym']], content: 'rumor' },
429
+ _publish
430
+ })
431
+ const eventResult = await broadcastNymEvent({
432
+ nymSigner: signer(nymPubkey),
433
+ privateChannelSigner: signer(channelPubkey),
434
+ relays: ['wss://relay.example'],
435
+ event: authorEvent,
436
+ _publish
437
+ })
438
+
439
+ assert.equal(published.length, 2)
440
+ assert.equal(published[0].nymSigner.getPublicKey(), nymPubkey)
441
+ assert.equal(published[0].privateChannelSigner.getPublicKey(), channelPubkey)
442
+ assert.equal(published[0].event.pubkey, undefined)
443
+ assert.equal(published[0].event.id, undefined)
444
+ assert.equal(rumorResult.rumor.pubkey, nymPubkey)
445
+ assert.equal(rumorResult.rumor.id, getEventHash({ ...published[0].event, pubkey: nymPubkey }))
446
+ assert.deepEqual(published[1].event, authorEvent)
447
+ assert.notEqual(published[1].event.pubkey, nymPubkey)
448
+ assert.deepEqual(eventResult.event, authorEvent)
449
+ })
450
+
451
+ test('parseRumorContent only reads h tags for private message kinds', () => {
452
+ assert.deepEqual(
453
+ parseRumorContent({ kind: TELL_KIND, tags: [['h', 'NOTE']], content: 'hello' }),
454
+ { payload: 'hello', code: 'NOTE' }
455
+ )
456
+ assert.deepEqual(
457
+ parseRumorContent({ kind: 9001, content: JSON.stringify(['hello', 'NOTE']) }),
458
+ ['hello', 'NOTE']
459
+ )
460
+ })