libp2r2p 0.0.2 → 0.1.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.
@@ -1,1912 +0,0 @@
1
- import { afterEach, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { finalizeEvent, generateSecretKey, getEventHash, getPublicKey } from 'nostr-tools'
4
- import NsecSigner, { deriveSharedKey } from './helpers/test-signer.js'
5
- import { deriveDoubleDhConversationKey } from '../double-dh/index.js'
6
- import {
7
- EXPIRATION_SECONDS,
8
- eventFromNymCarriers,
9
- fetch,
10
- getJsonlChunkByteSize,
11
- getNymCarrierChunkSize,
12
- MAX_EVENT_BYTES,
13
- NYM_CARRIER_KIND,
14
- PRIVATE_BROADCAST_KIND,
15
- publish,
16
- publishNymEvent,
17
- ROUTER_KIND,
18
- subscribe,
19
- unwrapEvent,
20
- wrapEvent,
21
- wrapEvents,
22
- wrapNymEvent
23
- } from '../private-channel/index.js'
24
- import { createReceivedChunkStore, DEFAULT_RECEIVED_CHUNK_MAX_BYTES } from '../private-channel/services/received-chunks.js'
25
- import { makeContentKeyEvent, parseContentKeyEvent, verifyContentKeyProof } from '../content-key/event/index.js'
26
- import { TEMPORARY_STORAGE_KEYS_KEY } from '../temporary-storage/index.js'
27
- import { bytesToBase64, base64ToBytes } from '../base64/index.js'
28
- import { bytesToHex, hexToBytes } from '../base16/index.js'
29
- import { relayPool } from '../relay/index.js'
30
-
31
- const originalEventsFeedGenerator = relayPool.getEventsFeedGenerator
32
- const originalLiveEventsGenerator = relayPool.getLiveEventsGenerator
33
- let mockedSubscribeMany = null
34
-
35
- // Adapts the old callback fixture style to the async-generator RelayPool API.
36
- // Resolving onevent waits until private-channel has consumed that event.
37
- function createMockEventsGenerator (subscribeMany) {
38
- return (filter, relays, { signal } = {}) => {
39
- const pending = []
40
- let wake = Promise.withResolvers()
41
- let current = null
42
- let closed = false
43
- let subscription
44
-
45
- const close = () => {
46
- if (closed) return
47
- closed = true
48
- current?.resolve()
49
- current = null
50
- for (const entry of pending.splice(0)) entry.resolve()
51
- subscription?.close?.()
52
- wake.resolve()
53
- }
54
- const handlers = {
55
- onevent: event => new Promise(resolve => {
56
- if (closed) return resolve()
57
- pending.push({ event, resolve })
58
- wake.resolve()
59
- wake = Promise.withResolvers()
60
- })
61
- }
62
- subscription = subscribeMany(relays, filter, handlers)
63
- signal?.addEventListener('abort', close, { once: true })
64
-
65
- return {
66
- async next () {
67
- current?.resolve()
68
- current = null
69
- while (!closed && pending.length === 0) await wake.promise
70
- if (closed) return { done: true }
71
- current = pending.shift()
72
- return { value: current.event, done: false }
73
- },
74
- async return () {
75
- close()
76
- return { done: true }
77
- },
78
- [Symbol.asyncIterator] () { return this }
79
- }
80
- }
81
- }
82
-
83
- const pool = {
84
- get subscribeMany () {
85
- return mockedSubscribeMany
86
- },
87
- set subscribeMany (subscribeMany) {
88
- mockedSubscribeMany = subscribeMany
89
- relayPool.getEventsFeedGenerator = subscribeMany
90
- ? createMockEventsGenerator(subscribeMany)
91
- : originalEventsFeedGenerator
92
- relayPool.getLiveEventsGenerator = subscribeMany
93
- ? createMockEventsGenerator(subscribeMany)
94
- : originalLiveEventsGenerator
95
- }
96
- }
97
-
98
- if (!globalThis.localStorage) {
99
- const data = new Map()
100
- globalThis.localStorage = {
101
- clear: () => data.clear(),
102
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
103
- key: index => [...data.keys()][index] || null,
104
- removeItem: key => { data.delete(String(key)) },
105
- setItem: (key, value) => { data.set(String(key), String(value)) },
106
- get length () { return data.size }
107
- }
108
- }
109
-
110
- if (!globalThis.sessionStorage) {
111
- const data = new Map()
112
- globalThis.sessionStorage = {
113
- clear: () => data.clear(),
114
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
115
- key: index => [...data.keys()][index] || null,
116
- removeItem: key => { data.delete(String(key)) },
117
- setItem: (key, value) => { data.set(String(key), String(value)) },
118
- get length () { return data.size }
119
- }
120
- }
121
-
122
- if (!globalThis.crypto) globalThis.crypto = crypto
123
- if (!globalThis.btoa) globalThis.btoa = s => Buffer.from(s, 'binary').toString('base64')
124
- if (!globalThis.atob) globalThis.atob = s => Buffer.from(s, 'base64').toString('binary')
125
-
126
- afterEach(() => {
127
- pool.subscribeMany = null
128
- NsecSigner.releaseAll()
129
- globalThis.localStorage.clear()
130
- globalThis.sessionStorage.clear()
131
- })
132
-
133
- function signer () {
134
- return NsecSigner.getOrCreate(bytesToHex(generateSecretKey()))
135
- }
136
-
137
- const encoder = new TextEncoder()
138
- const decoder = new TextDecoder()
139
-
140
- function textToBase64 (text) {
141
- return bytesToBase64(encoder.encode(text))
142
- }
143
-
144
- async function nip44v3EncryptText (signer, peerPubkey, kind, plaintext) {
145
- return signer.nip44v3Encrypt(peerPubkey, kind, '', textToBase64(plaintext))
146
- }
147
-
148
- async function nip44v3DecryptText (signer, peerPubkey, kind, ciphertext) {
149
- return decoder.decode(base64ToBytes(await signer.nip44v3Decrypt(peerPubkey, kind, '', ciphertext)))
150
- }
151
-
152
- async function decryptPrivateBroadcast (signer, peerPubkey, ciphertext) {
153
- return JSON.parse(await nip44v3DecryptText(signer, peerPubkey, PRIVATE_BROADCAST_KIND, ciphertext))
154
- }
155
-
156
- async function encryptPrivateBroadcast (signer, peerPubkey, value) {
157
- return nip44v3EncryptText(signer, peerPubkey, PRIVATE_BROADCAST_KIND, JSON.stringify(value))
158
- }
159
-
160
- async function signerWithInternalContentKey (identitySigner, contentSigner) {
161
- const identityPubkey = await identitySigner.getPublicKey()
162
- NsecSigner.setContentSigners(identitySigner, [contentSigner])
163
- return {
164
- getPublicKey: () => identityPubkey,
165
- signEvent: event => identitySigner.signEvent(event),
166
- nip44Encrypt: (peerPubkey, plaintext) => identitySigner.nip44Encrypt(peerPubkey, plaintext),
167
- nip44Decrypt: (peerPubkey, ciphertext) => identitySigner.nip44Decrypt(peerPubkey, ciphertext),
168
- nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => identitySigner.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
169
- nip44v3Decrypt: (peerPubkey, kind, scope, ciphertext) => identitySigner.nip44v3Decrypt(peerPubkey, kind, scope, ciphertext),
170
- nip44EncryptDoubleDH: (...params) => identitySigner.nip44EncryptDoubleDH(...params),
171
- nip44DecryptDoubleDH: (...params) => identitySigner.nip44DecryptDoubleDH(...params)
172
- }
173
- }
174
-
175
- function eventFixture (content = 'hello') {
176
- return { kind: 1, created_at: 1, tags: [], content }
177
- }
178
-
179
- function unwrappedFixture (event, pubkey) {
180
- const unwrapped = { ...event, pubkey }
181
- return { ...unwrapped, id: getEventHash(unwrapped) }
182
- }
183
-
184
- async function noContentKeys () {
185
- return {}
186
- }
187
-
188
- function routerJsonlRows (router) {
189
- return routerJsonlLines(router).map(line => JSON.parse(line))
190
- }
191
-
192
- function routerJsonlLines (router) {
193
- return new TextDecoder()
194
- .decode(Buffer.from(router.content, 'base64'))
195
- .trim()
196
- .split('\n')
197
- }
198
-
199
- function routerRecipientRows (router) {
200
- return routerJsonlRows(router).filter(row => row.length !== 1)
201
- }
202
-
203
- test('shared-key signer derives matching deniable key from either side', async () => {
204
- const alice = signer()
205
- const bob = signer()
206
- const alicePubkey = await alice.getPublicKey()
207
- const bobPubkey = await bob.getPublicKey()
208
- const aliceShared = alice.withSharedKey(bobPubkey)
209
- const bobShared = bob.withSharedKey(alicePubkey)
210
-
211
- assert.equal(await aliceShared.getPublicKey(), await bobShared.getPublicKey())
212
- const sharedPubkey = await aliceShared.getPublicKey()
213
- const ciphertext = await aliceShared.nip44Encrypt(sharedPubkey, 'secret')
214
- assert.equal(await bobShared.nip44Decrypt(sharedPubkey, ciphertext), 'secret')
215
-
216
- const syncShared = alice.withSharedKey(bobPubkey, 'trusted-signer-sync-v1')
217
- assert.notEqual(await syncShared.getPublicKey(), sharedPubkey)
218
- })
219
-
220
- test('shared-key derivation matches the 48-byte reduction vector', async () => {
221
- const aliceSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
222
- const bobSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
223
- const alicePubkey = getPublicKey(aliceSecret)
224
- const bobPubkey = getPublicKey(bobSecret)
225
-
226
- const aliceShared = await deriveSharedKey(aliceSecret, bobPubkey, 'trusted-signer-sync-v1')
227
- const bobShared = await deriveSharedKey(bobSecret, alicePubkey, 'trusted-signer-sync-v1')
228
-
229
- assert.equal(bytesToHex(aliceShared), '10228022a480c7db02f2d68796e30f029dd77853dca392174e623ea9fc275de0')
230
- assert.equal(bytesToHex(bobShared), bytesToHex(aliceShared))
231
- })
232
-
233
- test('double-DH signer round-trips every content-key mode', async () => {
234
- const alice = signer()
235
- const bob = signer()
236
- const aliceContent = signer()
237
- const bobContent = signer()
238
- const alicePubkey = await alice.getPublicKey()
239
- const bobPubkey = await bob.getPublicKey()
240
- const aliceContentPubkey = await aliceContent.getPublicKey()
241
- const bobContentPubkey = await bobContent.getPublicKey()
242
-
243
- const cases = [
244
- { mode: 'identity' },
245
- { mode: 'sender-content', aliceContent },
246
- { mode: 'receiver-content', bobContent },
247
- { mode: 'both-content', aliceContent, bobContent }
248
- ]
249
-
250
- for (const c of cases) {
251
- NsecSigner.setContentSigners(alice, c.aliceContent ? [aliceContent] : [])
252
- NsecSigner.setContentSigners(bob, c.bobContent ? [bobContent] : [])
253
- const encrypted = await alice.nip44EncryptDoubleDH(
254
- bobPubkey,
255
- ROUTER_KIND,
256
- '',
257
- textToBase64(c.mode),
258
- c.bobContent ? bobContentPubkey : ''
259
- )
260
- const decrypted = await bob.nip44DecryptDoubleDH(
261
- alicePubkey,
262
- ROUTER_KIND,
263
- '',
264
- encrypted[0],
265
- encrypted[1],
266
- c.bobContent ? bobContentPubkey : ''
267
- )
268
-
269
- assert.equal(encrypted[1], c.aliceContent ? aliceContentPubkey : '')
270
- assert.equal(decoder.decode(base64ToBytes(decrypted)), c.mode)
271
- if (c.mode === 'both-content') {
272
- await assert.rejects(
273
- () => bob.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND + 1, '', encrypted[0], aliceContentPubkey, bobContentPubkey),
274
- /kind mismatch/
275
- )
276
- }
277
- }
278
- })
279
-
280
- test('double-DH conversation key matches the fixed salt vector', () => {
281
- const aliceSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
282
- const bobSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
283
- const aliceContentSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000003')
284
- const bobContentSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000004')
285
- const alicePubkey = getPublicKey(aliceSecret)
286
- const bobPubkey = getPublicKey(bobSecret)
287
- const aliceContentPubkey = getPublicKey(aliceContentSecret)
288
- const bobContentPubkey = getPublicKey(bobContentSecret)
289
- const { mode, conversationKey } = deriveDoubleDhConversationKey({
290
- role: 'sender',
291
- identitySecretKey: aliceSecret,
292
- identityPubkey: alicePubkey,
293
- contentSecretKey: aliceContentSecret,
294
- contentPubkey: aliceContentPubkey,
295
- peerIdentityPubkey: bobPubkey,
296
- peerContentPubkey: bobContentPubkey,
297
- kind: ROUTER_KIND,
298
- scope: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
299
- })
300
-
301
- assert.equal(mode, 'both-content')
302
- assert.equal(bytesToHex(conversationKey), '995fec3e81d662f029fc4218bb3cf52305957c849c7b8356bd5d71852f4629b0')
303
- })
304
-
305
- test('double-DH conversation key is pair-oriented, not direction-oriented', () => {
306
- const aliceSecret = generateSecretKey()
307
- const bobSecret = generateSecretKey()
308
- const aliceContentSecret = generateSecretKey()
309
- const bobContentSecret = generateSecretKey()
310
- const alicePubkey = getPublicKey(aliceSecret)
311
- const bobPubkey = getPublicKey(bobSecret)
312
- const aliceContentPubkey = getPublicKey(aliceContentSecret)
313
- const bobContentPubkey = getPublicKey(bobContentSecret)
314
- const aliceToBob = deriveDoubleDhConversationKey({
315
- role: 'sender',
316
- identitySecretKey: aliceSecret,
317
- identityPubkey: alicePubkey,
318
- contentSecretKey: aliceContentSecret,
319
- contentPubkey: aliceContentPubkey,
320
- peerIdentityPubkey: bobPubkey,
321
- peerContentPubkey: bobContentPubkey,
322
- kind: ROUTER_KIND,
323
- scope: ''
324
- })
325
- const bobReceiving = deriveDoubleDhConversationKey({
326
- role: 'receiver',
327
- identitySecretKey: bobSecret,
328
- identityPubkey: bobPubkey,
329
- contentSecretKey: bobContentSecret,
330
- contentPubkey: bobContentPubkey,
331
- peerIdentityPubkey: alicePubkey,
332
- peerContentPubkey: aliceContentPubkey,
333
- kind: ROUTER_KIND,
334
- scope: ''
335
- })
336
- const bobToAlice = deriveDoubleDhConversationKey({
337
- role: 'sender',
338
- identitySecretKey: bobSecret,
339
- identityPubkey: bobPubkey,
340
- contentSecretKey: bobContentSecret,
341
- contentPubkey: bobContentPubkey,
342
- peerIdentityPubkey: alicePubkey,
343
- peerContentPubkey: aliceContentPubkey,
344
- kind: ROUTER_KIND,
345
- scope: ''
346
- })
347
- const aliceToBobInChannel = deriveDoubleDhConversationKey({
348
- role: 'sender',
349
- identitySecretKey: aliceSecret,
350
- identityPubkey: alicePubkey,
351
- contentSecretKey: aliceContentSecret,
352
- contentPubkey: aliceContentPubkey,
353
- peerIdentityPubkey: bobPubkey,
354
- peerContentPubkey: bobContentPubkey,
355
- kind: ROUTER_KIND,
356
- scope: '1'.repeat(64)
357
- })
358
- const bobReceivingInChannel = deriveDoubleDhConversationKey({
359
- role: 'receiver',
360
- identitySecretKey: bobSecret,
361
- identityPubkey: bobPubkey,
362
- contentSecretKey: bobContentSecret,
363
- contentPubkey: bobContentPubkey,
364
- peerIdentityPubkey: alicePubkey,
365
- peerContentPubkey: aliceContentPubkey,
366
- kind: ROUTER_KIND,
367
- scope: '1'.repeat(64)
368
- })
369
- const aliceToBobInOtherChannel = deriveDoubleDhConversationKey({
370
- role: 'sender',
371
- identitySecretKey: aliceSecret,
372
- identityPubkey: alicePubkey,
373
- contentSecretKey: aliceContentSecret,
374
- contentPubkey: aliceContentPubkey,
375
- peerIdentityPubkey: bobPubkey,
376
- peerContentPubkey: bobContentPubkey,
377
- kind: ROUTER_KIND,
378
- scope: '2'.repeat(64)
379
- })
380
- const aliceToBobInOtherKind = deriveDoubleDhConversationKey({
381
- role: 'sender',
382
- identitySecretKey: aliceSecret,
383
- identityPubkey: alicePubkey,
384
- contentSecretKey: aliceContentSecret,
385
- contentPubkey: aliceContentPubkey,
386
- peerIdentityPubkey: bobPubkey,
387
- peerContentPubkey: bobContentPubkey,
388
- kind: ROUTER_KIND + 1,
389
- scope: '1'.repeat(64)
390
- })
391
-
392
- assert.equal(bytesToHex(aliceToBob.conversationKey), bytesToHex(bobReceiving.conversationKey))
393
- assert.equal(bytesToHex(aliceToBob.conversationKey), bytesToHex(bobToAlice.conversationKey))
394
- assert.equal(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(bobReceivingInChannel.conversationKey))
395
- assert.notEqual(bytesToHex(aliceToBob.conversationKey), bytesToHex(aliceToBobInChannel.conversationKey))
396
- assert.notEqual(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(aliceToBobInOtherChannel.conversationKey))
397
- assert.notEqual(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(aliceToBobInOtherKind.conversationKey))
398
- })
399
-
400
- test('double-DH self-encryption with a content key requires both secrets', async () => {
401
- const alice = signer()
402
- const aliceContent = signer()
403
- const wrongIdentity = signer()
404
- const alicePubkey = await alice.getPublicKey()
405
- const aliceContentPubkey = await aliceContent.getPublicKey()
406
- NsecSigner.setContentSigners(alice, [aliceContent])
407
-
408
- const encrypted = await alice.nip44EncryptDoubleDH(alicePubkey, ROUTER_KIND, '', textToBase64('note to self'), aliceContentPubkey)
409
-
410
- assert.equal(encrypted[1], aliceContentPubkey)
411
- assert.equal(decoder.decode(base64ToBytes(await alice.nip44DecryptDoubleDH(
412
- alicePubkey,
413
- ROUTER_KIND,
414
- '',
415
- encrypted[0],
416
- aliceContentPubkey,
417
- aliceContentPubkey
418
- ))), 'note to self')
419
-
420
- await assert.rejects(
421
- () => alice.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', encrypted[0], aliceContentPubkey, ''),
422
- /RECEIVER_CONTENT_KEY_REQUIRED/
423
- )
424
-
425
- NsecSigner.setContentSigners(wrongIdentity, [aliceContent])
426
- await assert.rejects(
427
- () => wrongIdentity.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', encrypted[0], aliceContentPubkey, aliceContentPubkey),
428
- /invalid MAC/
429
- )
430
- })
431
-
432
- test('wrapEvent creates private broadcast events under relay event size limit', async () => {
433
- const alice = signer()
434
- const bob = signer()
435
- const bobPubkey = await bob.getPublicKey()
436
- const before = Math.floor(Date.now() / 1000)
437
- const wrapped = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: eventFixture('hello bob'), _getIykcProofs: noContentKeys })
438
-
439
- assert.equal(wrapped.length, 1)
440
- assert.equal(wrapped[0].kind, PRIVATE_BROADCAST_KIND)
441
- assert.equal(wrapped[0].pubkey, await alice.getPublicKey())
442
- assert.equal(wrapped[0].tags.some(tag => tag[0] === 's'), false)
443
- assert.ok(Number(wrapped[0].tags[0][1]) >= before + EXPIRATION_SECONDS)
444
- assert.ok(new TextEncoder().encode(JSON.stringify(wrapped[0])).length <= MAX_EVENT_BYTES)
445
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
446
- })
447
-
448
- test('private broadcast wrappers share and normalize one deletion pubkey per logical message', async () => {
449
- const alice = signer()
450
- const bob = signer()
451
- const bobPubkey = await bob.getPublicKey()
452
- const deletionPubkey = 'A'.repeat(64)
453
- const [wrapped] = await wrapEvent({
454
- senderSigner: alice,
455
- receivers: [bobPubkey],
456
- deletionPubkey,
457
- event: eventFixture('deletable'),
458
- _getIykcProofs: noContentKeys
459
- })
460
-
461
- assert.deepEqual(wrapped.tags.find(tag => tag[0] === 's'), ['s', deletionPubkey.toLowerCase()])
462
- assert.ok(Number(wrapped.tags.find(tag => tag[0] === 'expiration')?.[1]) > 0)
463
- assert.deepEqual(
464
- await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
465
- unwrappedFixture(eventFixture('deletable'), await alice.getPublicKey())
466
- )
467
-
468
- for (const invalidDeletionPubkey of ['', 'not-a-pubkey']) {
469
- await assert.rejects(
470
- () => wrapEvent({
471
- senderSigner: alice,
472
- receivers: [bobPubkey],
473
- deletionPubkey: invalidDeletionPubkey,
474
- event: eventFixture('invalid'),
475
- _getIykcProofs: noContentKeys
476
- }),
477
- /INVALID_DELETION_PUBKEY/
478
- )
479
- }
480
-
481
- })
482
-
483
- test('publish splits relay-targeted routers by receiver subset with separate router pubkeys', async () => {
484
- const alice = signer()
485
- const bob = signer()
486
- const carol = signer()
487
- const dave = signer()
488
- const alicePubkey = await alice.getPublicKey()
489
- const bobPubkey = await bob.getPublicKey()
490
- const carolPubkey = await carol.getPublicKey()
491
- const davePubkey = await dave.getPublicKey()
492
- const event = eventFixture('split publish')
493
- const published = []
494
- const temporaryWrites = []
495
- const temporaryStorageArea = {
496
- getItem: key => globalThis.localStorage.getItem(key),
497
- removeItem: key => globalThis.localStorage.removeItem(key),
498
- setItem: (key, value) => {
499
- temporaryWrites.push(key)
500
- globalThis.localStorage.setItem(key, value)
501
- }
502
- }
503
-
504
- const reports = await publish({
505
- senderSigner: alice,
506
- receivers: [bobPubkey, carolPubkey, davePubkey],
507
- relayToReceivers: new Map([
508
- ['wss://one.example', [bobPubkey, carolPubkey]],
509
- ['wss://two.example', [carolPubkey, bobPubkey]],
510
- ['wss://three.example', [carolPubkey, davePubkey]]
511
- ]),
512
- recoveryRelays: ['wss://seed.example', 'wss://one.example'],
513
- deletionPubkey: 'd'.repeat(64),
514
- event,
515
- temporaryStorageArea,
516
- _getIykcProofs: noContentKeys,
517
- _publish: async (outer, relays) => {
518
- published.push({ outer, relays })
519
- return { success: true }
520
- }
521
- })
522
-
523
- assert.equal(published.length, 2)
524
- assert.deepEqual(published.map(({ outer }) => outer.tags.find(tag => tag[0] === 's')), [
525
- ['s', 'd'.repeat(64)],
526
- ['s', 'd'.repeat(64)]
527
- ])
528
- assert.deepEqual(reports, [{ success: true }, { success: true }])
529
- assert.deepEqual(published[0].relays, ['wss://one.example', 'wss://two.example', 'wss://seed.example'])
530
- assert.deepEqual(published[1].relays, ['wss://three.example', 'wss://seed.example', 'wss://one.example'])
531
-
532
- const routers = []
533
- for (const { outer } of published) {
534
- routers.push(await decryptPrivateBroadcast(alice, alicePubkey, outer.content))
535
- }
536
- assert.deepEqual(routers.map(router => router.tags.some(tag => tag[0] === 'id')), [false, false])
537
- assert.notEqual(routers[0].pubkey, routers[1].pubkey)
538
- assert.deepEqual(
539
- routerRecipientRows(routers[0]).map(row => row[0]).sort(),
540
- [bobPubkey, carolPubkey].sort()
541
- )
542
- assert.deepEqual(
543
- routerRecipientRows(routers[1]).map(row => row[0]),
544
- [carolPubkey, davePubkey]
545
- )
546
- const firstLines = routerJsonlLines(routers[0])
547
- const secondLines = routerJsonlLines(routers[1])
548
- assert.equal(firstLines[0], secondLines[0])
549
- assert.equal(
550
- firstLines.find(line => JSON.parse(line)[0] === carolPubkey),
551
- secondLines.find(line => JSON.parse(line)[0] === carolPubkey)
552
- )
553
- assert.ok(temporaryWrites.includes(TEMPORARY_STORAGE_KEYS_KEY))
554
- assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
555
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
556
- })
557
-
558
- test('publish sends wrapped events through RelayPool.sendEvent', async () => {
559
- const originalSendEvent = relayPool.sendEvent
560
- const published = []
561
- relayPool.sendEvent = async (outer, relays) => {
562
- published.push({ outer, relays })
563
- return { success: true, total: relays.length, promise: Promise.resolve({ success: true }) }
564
- }
565
-
566
- try {
567
- const alice = signer()
568
- const bob = signer()
569
- await publish({
570
- senderSigner: alice,
571
- receivers: [await bob.getPublicKey()],
572
- event: eventFixture('relay pool publish'),
573
- relays: ['wss://relay.example'],
574
- _getIykcProofs: noContentKeys
575
- })
576
- } finally {
577
- relayPool.sendEvent = originalSendEvent
578
- }
579
-
580
- assert.equal(published.length, 1)
581
- assert.equal(published[0].outer.kind, PRIVATE_BROADCAST_KIND)
582
- assert.deepEqual(published[0].relays, ['wss://relay.example'])
583
- })
584
-
585
- test('fetch uses complete RelayPool reads for private-channel recovery', async () => {
586
- let call
587
- const events = await fetch({
588
- relays: ['wss://relay.example'],
589
- _getEvents: async (filter, relays, options) => {
590
- call = { filter, relays, options }
591
- return { result: [] }
592
- }
593
- })
594
-
595
- assert.deepEqual(events, [])
596
- assert.deepEqual(call.relays, ['wss://relay.example'])
597
- assert.deepEqual(call.options, { timeout: 5000, timeoutAfterFirstEose: null })
598
- assert.deepEqual(call.filter.kinds, [PRIVATE_BROADCAST_KIND])
599
- })
600
-
601
- test('publish cleans prepared rows when grouped publishing fails', async () => {
602
- const alice = signer()
603
- const bob = signer()
604
- const carol = signer()
605
- const bobPubkey = await bob.getPublicKey()
606
- const carolPubkey = await carol.getPublicKey()
607
-
608
- await assert.rejects(
609
- publish({
610
- senderSigner: alice,
611
- receivers: [bobPubkey, carolPubkey],
612
- relayToReceivers: new Map([
613
- ['wss://one.example', [bobPubkey]],
614
- ['wss://two.example', [carolPubkey]]
615
- ]),
616
- event: eventFixture('publish fail'),
617
- _getIykcProofs: noContentKeys,
618
- _publish: async () => { throw new Error('relay offline') }
619
- }),
620
- /relay offline/
621
- )
622
-
623
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
624
- })
625
-
626
- test('publishNymEvent mirrors carrier chunks to recovery relays', async () => {
627
- const channel = signer()
628
- const nym = signer()
629
- const published = []
630
-
631
- const reports = await publishNymEvent({
632
- nymSigner: nym,
633
- privateChannelSigner: channel,
634
- event: eventFixture('nym mirror'),
635
- relays: ['wss://receiver.example'],
636
- recoveryRelays: ['wss://seed.example', 'wss://receiver.example'],
637
- deletionPubkey: 'e'.repeat(64),
638
- _publish: async (outer, relays) => {
639
- published.push({ outer, relays })
640
- return { success: true }
641
- }
642
- })
643
-
644
- assert.equal(published.length, 1)
645
- assert.deepEqual(published[0].relays, ['wss://receiver.example', 'wss://seed.example'])
646
- assert.deepEqual(published[0].outer.tags.find(tag => tag[0] === 's'), ['s', 'e'.repeat(64)])
647
- assert.deepEqual(reports, [{ success: true }])
648
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
649
- })
650
-
651
- test('received chunk default cap is proportional to private-channel chunk size', () => {
652
- assert.equal(getJsonlChunkByteSize(), 30159)
653
- assert.equal(DEFAULT_RECEIVED_CHUNK_MAX_BYTES, Math.min(getJsonlChunkByteSize() * 64, 3 * 1024 * 1024))
654
- })
655
-
656
- test('wrapEvent supports overriding the outer expiration window', async () => {
657
- const alice = signer()
658
- const bob = signer()
659
- const bobPubkey = await bob.getPublicKey()
660
- const before = Math.floor(Date.now() / 1000)
661
- const [wrapped] = await wrapEvent({
662
- senderSigner: alice,
663
- receivers: [bobPubkey],
664
- event: eventFixture('hello bob'),
665
- expirationSeconds: 7 * 24 * 60 * 60,
666
- _getIykcProofs: noContentKeys
667
- })
668
-
669
- assert.ok(Number(wrapped.tags[0][1]) >= before + 7 * 24 * 60 * 60)
670
- })
671
-
672
- test('unwrapEvent returns the addressed receiver event or null', async () => {
673
- const alice = signer()
674
- const bob = signer()
675
- const carol = signer()
676
- const bobPubkey = await bob.getPublicKey()
677
- const carolPubkey = await carol.getPublicKey()
678
- const alicePubkey = await alice.getPublicKey()
679
- const original = { ...eventFixture('private'), pubkey: '0'.repeat(64), id: 'f'.repeat(64) }
680
- const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey, carolPubkey], event: original, _getIykcProofs: noContentKeys })
681
-
682
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
683
- assert.deepEqual(await unwrapEvent({ receiverSigner: carol, privateChannelSigner: alice, event: wrapped, receiverPubkey: carolPubkey }), unwrappedFixture(original, alicePubkey))
684
- assert.equal(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: await signer().getPublicKey() }), null)
685
- })
686
-
687
- test('wrapEvent can encrypt the outer router to a separate reader key', async () => {
688
- const sender = signer()
689
- const channel = signer()
690
- const reader = signer()
691
- const bob = signer()
692
- const bobPubkey = await bob.getPublicKey()
693
- const readerPubkey = await reader.getPublicKey()
694
- const channelPubkey = await channel.getPublicKey()
695
- const original = eventFixture('reader decrypts channel')
696
- const [wrapped] = await wrapEvent({
697
- senderSigner: sender,
698
- privateChannelSigner: channel,
699
- privateChannelReaderPubkey: readerPubkey,
700
- receivers: [bobPubkey],
701
- event: original,
702
- _getIykcProofs: noContentKeys
703
- })
704
-
705
- assert.equal(wrapped.pubkey, channelPubkey)
706
- const router = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
707
- assert.equal(router.kind, ROUTER_KIND)
708
- assert.deepEqual(await unwrapEvent({
709
- receiverSigner: bob,
710
- privateChannelSigner: channel,
711
- privateChannelReaderSigner: reader,
712
- event: wrapped,
713
- receiverPubkey: bobPubkey
714
- }), unwrappedFixture(original, await sender.getPublicKey()))
715
- assert.deepEqual(await unwrapEvent({
716
- receiverSigner: bob,
717
- privateChannelSigner: channel,
718
- privateChannelReaderPubkey: readerPubkey,
719
- event: wrapped,
720
- receiverPubkey: bobPubkey
721
- }), unwrappedFixture(original, await sender.getPublicKey()))
722
- })
723
-
724
- test('subscribe can read channel events with only a reader signer', async () => {
725
- const originalSubscribeMany = pool.subscribeMany
726
- let handlers = null
727
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
728
- handlers = nextHandlers
729
- return { close: () => {} }
730
- }
731
-
732
- try {
733
- const sender = signer()
734
- const channel = signer()
735
- const reader = signer()
736
- const bob = signer()
737
- const bobPubkey = await bob.getPublicKey()
738
- const channelPubkey = await channel.getPublicKey()
739
- const original = eventFixture('reader-only subscribe')
740
- const [wrapped] = await wrapEvent({
741
- senderSigner: sender,
742
- privateChannelSigner: channel,
743
- privateChannelReaderPubkey: await reader.getPublicKey(),
744
- receivers: [bobPubkey],
745
- event: original,
746
- _getIykcProofs: noContentKeys
747
- })
748
- const events = []
749
-
750
- subscribe({
751
- receiverSigner: bob,
752
- privateChannelSigner: null,
753
- privateChannelReaderSigner: reader,
754
- privateChannelPubkey: channelPubkey,
755
- receiverPubkey: bobPubkey,
756
- relays: ['wss://relay.example'],
757
- onEvent: event => events.push(event)
758
- })
759
- await handlers.onevent(wrapped)
760
-
761
- assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
762
- } finally {
763
- pool.subscribeMany = originalSubscribeMany
764
- }
765
- })
766
-
767
- test('live-only subscribe closes its RelayPool stream', async () => {
768
- const sender = signer()
769
- const bob = signer()
770
- const bobPubkey = await bob.getPublicKey()
771
- const channelPubkey = await sender.getPublicKey()
772
- const original = eventFixture('live subscription')
773
- const [wrapped] = await wrapEvent({
774
- senderSigner: sender,
775
- receivers: [bobPubkey],
776
- event: original,
777
- _getIykcProofs: noContentKeys
778
- })
779
- const received = []
780
- let aborted = false
781
-
782
- const subscription = subscribe({
783
- receiverSigner: bob,
784
- privateChannelSigner: sender,
785
- privateChannelPubkey: channelPubkey,
786
- receiverPubkey: bobPubkey,
787
- relays: ['wss://relay.example'],
788
- liveOnly: true,
789
- onEvent: event => received.push(event),
790
- _liveEventsGenerator: (_filter, _relays, { signal }) => (async function * () {
791
- yield wrapped
792
- await new Promise(resolve => signal.addEventListener('abort', () => {
793
- aborted = true
794
- resolve()
795
- }, { once: true }))
796
- })()
797
- })
798
-
799
- for (let attempt = 0; attempt < 20 && received.length === 0; attempt++) {
800
- await new Promise(resolve => setImmediate(resolve))
801
- }
802
- assert.deepEqual(received, [unwrappedFixture(original, await sender.getPublicKey())])
803
-
804
- subscription.close()
805
- for (let attempt = 0; attempt < 20 && !aborted; attempt++) {
806
- await new Promise(resolve => setImmediate(resolve))
807
- }
808
- assert.ok(aborted)
809
- })
810
-
811
- test('subscribe can read reader-targeted channel events with the writer signer', async () => {
812
- const originalSubscribeMany = pool.subscribeMany
813
- let handlers = null
814
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
815
- handlers = nextHandlers
816
- return { close: () => {} }
817
- }
818
-
819
- try {
820
- const sender = signer()
821
- const channel = signer()
822
- const reader = signer()
823
- const bob = signer()
824
- const bobPubkey = await bob.getPublicKey()
825
- const readerPubkey = await reader.getPublicKey()
826
- const channelPubkey = await channel.getPublicKey()
827
- const original = eventFixture('writer reads reader-targeted router')
828
- const [wrapped] = await wrapEvent({
829
- senderSigner: sender,
830
- privateChannelSigner: channel,
831
- privateChannelReaderPubkey: readerPubkey,
832
- receivers: [bobPubkey],
833
- event: original,
834
- _getIykcProofs: noContentKeys
835
- })
836
- const events = []
837
-
838
- subscribe({
839
- receiverSigner: bob,
840
- privateChannelSigner: channel,
841
- privateChannelReaderPubkey: readerPubkey,
842
- privateChannelPubkey: channelPubkey,
843
- receiverPubkey: bobPubkey,
844
- relays: ['wss://relay.example'],
845
- onEvent: event => events.push(event)
846
- })
847
- await handlers.onevent(wrapped)
848
-
849
- assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
850
- } finally {
851
- pool.subscribeMany = originalSubscribeMany
852
- }
853
- })
854
-
855
- test('unwrapEvent preserves valid signed inner events', async () => {
856
- const alice = signer()
857
- const bob = signer()
858
- const authorSecret = generateSecretKey()
859
- const bobPubkey = await bob.getPublicKey()
860
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [['x', '1']], content: 'signed' }, authorSecret)
861
- const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: signed, _getIykcProofs: noContentKeys })
862
-
863
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), signed)
864
- })
865
-
866
- test('unwrapEvent rejects invalid signed inner events', async () => {
867
- const alice = signer()
868
- const bob = signer()
869
- const bobPubkey = await bob.getPublicKey()
870
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
871
- const [wrapped] = await wrapEvent({
872
- senderSigner: alice,
873
- receivers: [bobPubkey],
874
- event: { ...signed, content: 'tampered' },
875
- _getIykcProofs: noContentKeys
876
- })
877
-
878
- await assert.rejects(
879
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
880
- /INVALID_SIGNED_INNER_EVENT/
881
- )
882
- })
883
-
884
- test('unwrapEvent rejects malformed signed inner events', async () => {
885
- const alice = signer()
886
- const bob = signer()
887
- const bobPubkey = await bob.getPublicKey()
888
- const [wrapped] = await wrapEvent({
889
- senderSigner: alice,
890
- receivers: [bobPubkey],
891
- event: { id: 'e'.repeat(64), pubkey: 'a'.repeat(64), kind: 9002, created_at: 2, tags: [], content: 'signed', sig: 42 },
892
- _getIykcProofs: noContentKeys
893
- })
894
-
895
- await assert.rejects(
896
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
897
- /INVALID_SIGNED_INNER_EVENT/
898
- )
899
- })
900
-
901
- test('wrapNymEvent signs nym carriers and reconstructs nym rumors', async () => {
902
- const channel = signer()
903
- const reader = signer()
904
- const nym = signer()
905
- const channelPubkey = await channel.getPublicKey()
906
- const readerPubkey = await reader.getPublicKey()
907
- const nymPubkey = await nym.getPublicKey()
908
- const original = eventFixture('nym rumor')
909
- const [wrapped] = await wrapNymEvent({
910
- nymSigner: nym,
911
- privateChannelSigner: channel,
912
- privateChannelReaderPubkey: readerPubkey,
913
- event: original
914
- })
915
-
916
- assert.equal(wrapped.kind, PRIVATE_BROADCAST_KIND)
917
- assert.equal(wrapped.pubkey, channelPubkey)
918
- assert.ok(new TextEncoder().encode(JSON.stringify(wrapped)).length <= MAX_EVENT_BYTES)
919
- const carrier = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
920
- const expected = unwrappedFixture(original, nymPubkey)
921
-
922
- assert.equal(carrier.kind, NYM_CARRIER_KIND)
923
- assert.equal(carrier.pubkey, nymPubkey)
924
- assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], expected.id)
925
- assert.deepEqual(carrier.tags.find(tag => tag[0] === 'c'), ['c', '0', '1'])
926
- assert.deepEqual(eventFromNymCarriers([carrier]), expected)
927
- })
928
-
929
- test('wrapNymEvent preserves signed inner event authors distinct from carrier nym', async () => {
930
- const channel = signer()
931
- const nym = signer()
932
- const signed = finalizeEvent({ kind: 9002, created_at: 23, tags: [['x', 'signed']], content: 'signed by someone else' }, generateSecretKey())
933
- const [wrapped] = await wrapNymEvent({
934
- nymSigner: nym,
935
- privateChannelSigner: channel,
936
- event: signed
937
- })
938
- const carrier = await decryptPrivateBroadcast(channel, await channel.getPublicKey(), wrapped.content)
939
-
940
- assert.notEqual(carrier.pubkey, signed.pubkey)
941
- assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], signed.id)
942
- assert.deepEqual(eventFromNymCarriers([carrier]), signed)
943
- })
944
-
945
- test('subscribe buffers out-of-order nym carrier chunks by nym pubkey and inner id', async () => {
946
- const originalSubscribeMany = pool.subscribeMany
947
- let handlers = null
948
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
949
- handlers = nextHandlers
950
- return { close: () => {} }
951
- }
952
-
953
- try {
954
- const channel = signer()
955
- const nym = signer()
956
- const nymPubkey = await nym.getPublicKey()
957
- const original = eventFixture('x'.repeat(getNymCarrierChunkSize()))
958
- const wrapped = await wrapNymEvent({
959
- nymSigner: nym,
960
- privateChannelSigner: channel,
961
- deletionPubkey: 'e'.repeat(64),
962
- event: original
963
- })
964
- const routedEvents = []
965
- const nymEvents = []
966
- const seeds = []
967
-
968
- assert.ok(wrapped.length > 1)
969
- for (const event of wrapped) assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
970
- assert.deepEqual(new Set(wrapped.map(event => event.tags.find(tag => tag[0] === 's')?.[1])), new Set(['e'.repeat(64)]))
971
-
972
- subscribe({
973
- privateChannelSigner: channel,
974
- privateChannelPubkey: await channel.getPublicKey(),
975
- relays: ['wss://relay.example'],
976
- mode: 'seeder',
977
- onEvent: event => routedEvents.push(event),
978
- onNymEvent: event => nymEvents.push(event),
979
- onSeedEvent: seed => seeds.push(seed)
980
- })
981
- for (const event of [...wrapped].reverse()) await handlers.onevent(event)
982
-
983
- assert.deepEqual(routedEvents, [])
984
- assert.deepEqual(nymEvents, [unwrappedFixture(original, nymPubkey)])
985
- assert.equal(seeds.length, 1)
986
- assert.equal(seeds[0].recordType, 'nymCarrier_v1')
987
- assert.equal(seeds[0].carriers.length, wrapped.length)
988
- } finally {
989
- pool.subscribeMany = originalSubscribeMany
990
- }
991
- })
992
-
993
- test('subscribe separates nym carrier groups that use different chunk totals', async () => {
994
- const originalSubscribeMany = pool.subscribeMany
995
- let handlers = null
996
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
997
- handlers = nextHandlers
998
- return { close: () => {} }
999
- }
1000
-
1001
- try {
1002
- const channel = signer()
1003
- const nym = signer()
1004
- const channelPubkey = await channel.getPublicKey()
1005
- const nymPubkey = await nym.getPublicKey()
1006
- const original = eventFixture('same inner event, different chunking')
1007
- const expected = unwrappedFixture(original, nymPubkey)
1008
- const encoded = Buffer.from(JSON.stringify(original)).toString('base64')
1009
- const midpoint = Math.ceil(encoded.length / 2)
1010
- const nymEvents = []
1011
- const errors = []
1012
-
1013
- async function carrier (index, total, content) {
1014
- return nym.signEvent({
1015
- kind: NYM_CARRIER_KIND,
1016
- created_at: 9,
1017
- tags: [['id', expected.id], ['c', String(index), String(total)]],
1018
- content
1019
- })
1020
- }
1021
-
1022
- async function outer (carrier) {
1023
- return channel.signEvent({
1024
- kind: PRIVATE_BROADCAST_KIND,
1025
- created_at: 10,
1026
- tags: [],
1027
- content: await encryptPrivateBroadcast(channel, channelPubkey, carrier)
1028
- })
1029
- }
1030
-
1031
- const twoChunkFirst = await outer(await carrier(0, 2, encoded.slice(0, midpoint)))
1032
- const oneChunk = await outer(await carrier(0, 1, encoded))
1033
- const twoChunkLast = await outer(await carrier(1, 2, encoded.slice(midpoint)))
1034
-
1035
- subscribe({
1036
- privateChannelSigner: channel,
1037
- privateChannelPubkey: channelPubkey,
1038
- relays: ['wss://relay.example'],
1039
- onNymEvent: event => nymEvents.push(event),
1040
- onError: err => errors.push(err)
1041
- })
1042
- await handlers.onevent(twoChunkFirst)
1043
- await handlers.onevent(oneChunk)
1044
- await handlers.onevent(twoChunkLast)
1045
-
1046
- assert.deepEqual(errors, [])
1047
- assert.deepEqual(nymEvents, [expected, expected])
1048
- } finally {
1049
- pool.subscribeMany = originalSubscribeMany
1050
- }
1051
- })
1052
-
1053
- test('unwrapEvent uses imkc tag as the row encryption pubkey', async () => {
1054
- const alice = signer()
1055
- const imkc = signer()
1056
- const bob = signer()
1057
- const alicePubkey = await alice.getPublicKey()
1058
- const bobPubkey = await bob.getPublicKey()
1059
- const imkcPubkey = await imkc.getPublicKey()
1060
- const original = eventFixture('private')
1061
- NsecSigner.setContentSigners(alice, [imkc])
1062
- const [wrapped] = await wrapEvent({ senderSigner: alice, imkcSigner: imkc, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1063
- const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
1064
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1065
-
1066
- assert.equal(router.tags.find(t => t[0] === 'f')?.[1], alicePubkey)
1067
- assert.equal(imkcTag?.[1], imkcPubkey)
1068
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: imkcPubkey, proof: imkcTag?.[2] }), true)
1069
- assert.deepEqual(
1070
- await unwrapEvent({
1071
- receiverSigner: bob,
1072
- privateChannelSigner: alice,
1073
- event: wrapped,
1074
- receiverPubkey: bobPubkey
1075
- }),
1076
- unwrappedFixture(original, alicePubkey)
1077
- )
1078
- })
1079
-
1080
- test('wrapEvent can add imkc from senderSigner Double-DH without direct content signer access', async () => {
1081
- const alice = signer()
1082
- const aliceContent = signer()
1083
- const bob = signer()
1084
- const aliceProxy = await signerWithInternalContentKey(alice, aliceContent)
1085
- const alicePubkey = await alice.getPublicKey()
1086
- const bobPubkey = await bob.getPublicKey()
1087
- const contentPubkey = await aliceContent.getPublicKey()
1088
- const original = eventFixture('private')
1089
- const [wrapped] = await wrapEvent({
1090
- senderSigner: aliceProxy,
1091
- privateChannelSigner: alice,
1092
- receivers: [bobPubkey],
1093
- event: original,
1094
- _getIykcProofs: noContentKeys
1095
- })
1096
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1097
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1098
-
1099
- assert.equal(imkcTag?.[1], contentPubkey)
1100
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey, proof: imkcTag?.[2] }), true)
1101
- assert.deepEqual(
1102
- await unwrapEvent({
1103
- receiverSigner: bob,
1104
- privateChannelSigner: alice,
1105
- event: wrapped,
1106
- receiverPubkey: bobPubkey
1107
- }),
1108
- unwrappedFixture(original, alicePubkey)
1109
- )
1110
- })
1111
-
1112
- test('wrapEvent uses Double-DH returned own content pubkey for imkc tag', async () => {
1113
- const alice = signer()
1114
- const oldContent = signer()
1115
- const actualContent = signer()
1116
- const bob = signer()
1117
- const alicePubkey = await alice.getPublicKey()
1118
- const bobPubkey = await bob.getPublicKey()
1119
- const actualContentPubkey = await actualContent.getPublicKey()
1120
- const senderSigner = {
1121
- getPublicKey: () => alice.getPublicKey(),
1122
- signEvent: event => alice.signEvent(event),
1123
- nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
1124
- nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
1125
- nip44EncryptDoubleDH: (...params) => {
1126
- NsecSigner.setContentSigners(alice, [actualContent])
1127
- return alice.nip44EncryptDoubleDH(...params)
1128
- }
1129
- }
1130
- const [wrapped] = await wrapEvent({
1131
- senderSigner,
1132
- imkcSigner: oldContent,
1133
- privateChannelSigner: alice,
1134
- receivers: [bobPubkey],
1135
- event: eventFixture('private'),
1136
- _getIykcProofs: noContentKeys
1137
- })
1138
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1139
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1140
-
1141
- assert.equal(imkcTag?.[1], actualContentPubkey)
1142
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: actualContentPubkey, proof: imkcTag?.[2] }), true)
1143
- })
1144
-
1145
- test('wrapEvent retries once when sender content key rotates while writing chunks', async () => {
1146
- const alice = signer()
1147
- const oldContent = signer()
1148
- const actualContent = signer()
1149
- const bob = signer()
1150
- const carol = signer()
1151
- const alicePubkey = await alice.getPublicKey()
1152
- const bobPubkey = await bob.getPublicKey()
1153
- const carolPubkey = await carol.getPublicKey()
1154
- const actualContentPubkey = await actualContent.getPublicKey()
1155
- let encryptCalls = 0
1156
- const senderSigner = {
1157
- getPublicKey: () => alice.getPublicKey(),
1158
- signEvent: event => alice.signEvent(event),
1159
- nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
1160
- nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
1161
- nip44EncryptDoubleDH: (...params) => {
1162
- encryptCalls++
1163
- NsecSigner.setContentSigners(alice, [encryptCalls === 1 ? oldContent : actualContent])
1164
- return alice.nip44EncryptDoubleDH(...params)
1165
- }
1166
- }
1167
- const original = eventFixture('private after rotation')
1168
- const [wrapped] = await wrapEvent({
1169
- senderSigner,
1170
- imkcSigner: oldContent,
1171
- privateChannelSigner: alice,
1172
- receivers: [bobPubkey, carolPubkey],
1173
- event: original,
1174
- _getIykcProofs: noContentKeys
1175
- })
1176
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1177
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1178
-
1179
- assert.equal(encryptCalls, 4)
1180
- assert.equal(imkcTag?.[1], actualContentPubkey)
1181
- assert.deepEqual(
1182
- await unwrapEvent({
1183
- receiverSigner: bob,
1184
- privateChannelSigner: alice,
1185
- event: wrapped,
1186
- receiverPubkey: bobPubkey
1187
- }),
1188
- unwrappedFixture(original, alicePubkey)
1189
- )
1190
- })
1191
-
1192
- test('unwrapEvent rejects sender imkc keys that do not match the ciphertext', async () => {
1193
- const alice = signer()
1194
- const oldImkc = signer()
1195
- const newImkc = signer()
1196
- const bob = signer()
1197
- const bobPubkey = await bob.getPublicKey()
1198
- NsecSigner.setContentSigners(alice, [oldImkc])
1199
- const [wrapped] = await wrapEvent({
1200
- senderSigner: alice,
1201
- imkcSigner: oldImkc,
1202
- receivers: [bobPubkey],
1203
- event: eventFixture('private'),
1204
- _getIykcProofs: noContentKeys
1205
- })
1206
- const channelPubkey = await alice.getPublicKey()
1207
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1208
- const newImkcPubkey = await newImkc.getPublicKey()
1209
- const newImkcProof = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: alice, contentKeySigner: newImkc, createdAt: 7 })).iykcProof
1210
- router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', newImkcPubkey, newImkcProof] : tag)
1211
- const tampered = await alice.signEvent({
1212
- kind: PRIVATE_BROADCAST_KIND,
1213
- created_at: wrapped.created_at,
1214
- tags: wrapped.tags,
1215
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1216
- })
1217
-
1218
- await assert.rejects(
1219
- () => unwrapEvent({
1220
- receiverSigner: bob,
1221
- privateChannelSigner: alice,
1222
- event: tampered,
1223
- receiverPubkey: bobPubkey
1224
- }),
1225
- /invalid MAC/
1226
- )
1227
- })
1228
-
1229
- test('unwrapEvent rejects sender imkc rows without valid proofs', async () => {
1230
- const alice = signer()
1231
- const imkc = signer()
1232
- const bob = signer()
1233
- const bobPubkey = await bob.getPublicKey()
1234
- NsecSigner.setContentSigners(alice, [imkc])
1235
- const [wrapped] = await wrapEvent({
1236
- senderSigner: alice,
1237
- imkcSigner: imkc,
1238
- receivers: [bobPubkey],
1239
- event: eventFixture('private'),
1240
- _getIykcProofs: noContentKeys
1241
- })
1242
- const channelPubkey = await alice.getPublicKey()
1243
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1244
- router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', tag[1]] : tag)
1245
- const tampered = await alice.signEvent({
1246
- kind: PRIVATE_BROADCAST_KIND,
1247
- created_at: wrapped.created_at,
1248
- tags: wrapped.tags,
1249
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1250
- })
1251
-
1252
- await assert.rejects(
1253
- () => unwrapEvent({
1254
- receiverSigner: bob,
1255
- privateChannelSigner: alice,
1256
- event: tampered,
1257
- receiverPubkey: bobPubkey
1258
- }),
1259
- /INVALID_IMKC_PROOF/
1260
- )
1261
- })
1262
-
1263
- test('subscribe emits content key usage for own sent direct messages', async () => {
1264
- const originalSubscribeMany = pool.subscribeMany
1265
- let handlers = null
1266
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1267
- handlers = nextHandlers
1268
- return { close: () => {} }
1269
- }
1270
-
1271
- try {
1272
- const alice = signer()
1273
- const bob = signer()
1274
- const bobPubkey = await bob.getPublicKey()
1275
- const [wrapped] = await wrapEvent({
1276
- senderSigner: alice,
1277
- receivers: [bobPubkey],
1278
- receiverTag: bobPubkey,
1279
- event: eventFixture('private'),
1280
- _getIykcProofs: noContentKeys
1281
- })
1282
- const usages = []
1283
- const delivered = []
1284
-
1285
- subscribe({
1286
- receiverSigner: alice,
1287
- privateChannelSigner: alice,
1288
- receiverPubkey: await alice.getPublicKey(),
1289
- relays: ['wss://relay.example'],
1290
- onContentKeyUsage: usage => usages.push(usage),
1291
- onEvent: event => delivered.push(event)
1292
- })
1293
- await handlers.onevent(wrapped)
1294
-
1295
- assert.equal(usages.length, 1)
1296
- assert.equal(usages[0].direction, 'sent')
1297
- assert.equal(usages[0].senderPubkey, await alice.getPublicKey())
1298
- assert.equal(usages[0].receiverPubkey, bobPubkey)
1299
- assert.equal(usages[0].contentKeyPubkey, '')
1300
- assert.equal(delivered.length, 0)
1301
- } finally {
1302
- pool.subscribeMany = originalSubscribeMany
1303
- }
1304
- })
1305
-
1306
- test('subscribe treats watchtower mode as recovery seed storage', async () => {
1307
- const originalSubscribeMany = pool.subscribeMany
1308
- let handlers = null
1309
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1310
- handlers = nextHandlers
1311
- return { close: () => {} }
1312
- }
1313
-
1314
- try {
1315
- const alice = signer()
1316
- const bob = signer()
1317
- const bobPubkey = await bob.getPublicKey()
1318
- const original = eventFixture('watchtower payload')
1319
- const [wrapped] = await wrapEvent({
1320
- senderSigner: alice,
1321
- receivers: [bobPubkey],
1322
- event: original,
1323
- _getIykcProofs: noContentKeys
1324
- })
1325
- const events = []
1326
- const seeds = []
1327
-
1328
- subscribe({
1329
- receiverSigner: bob,
1330
- privateChannelSigner: alice,
1331
- receiverPubkey: bobPubkey,
1332
- relays: ['wss://relay.example'],
1333
- mode: 'watchtower',
1334
- onEvent: event => events.push(event),
1335
- onSeedEvent: seed => seeds.push(seed)
1336
- })
1337
- await handlers.onevent(wrapped)
1338
-
1339
- assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
1340
- assert.equal(seeds.length, 1)
1341
- assert.equal(seeds[0].channelPubkey, await alice.getPublicKey())
1342
- assert.ok(seeds[0].router.content)
1343
- } finally {
1344
- pool.subscribeMany = originalSubscribeMany
1345
- }
1346
- })
1347
-
1348
- test('wrapEvent uses receiver content key rows when iykc is advertised', async () => {
1349
- const alice = signer()
1350
- const bob = signer()
1351
- const bobContent = signer()
1352
- const alicePubkey = await alice.getPublicKey()
1353
- const bobPubkey = await bob.getPublicKey()
1354
- const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
1355
- const contentKey = parseContentKeyEvent(contentKeyEvent)
1356
- const original = eventFixture('private')
1357
- const [wrapped] = await wrapEvent({
1358
- senderSigner: alice,
1359
- receivers: [bobPubkey],
1360
- event: original,
1361
- _getIykcProofs: async () => ({
1362
- [bobPubkey]: contentKey
1363
- })
1364
- })
1365
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1366
- const line = routerRecipientRows(router)[0]
1367
-
1368
- assert.deepEqual(line.slice(0, 1), [bobPubkey])
1369
- assert.deepEqual(line.slice(2), [contentKey.iykcPubkey, contentKey.iykcProof])
1370
- await assert.rejects(
1371
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
1372
- /RECEIVER_CONTENT_KEY_REQUIRED/
1373
- )
1374
- NsecSigner.setContentSigners(bob, [bobContent])
1375
- await assert.rejects(
1376
- () => bob.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', line[1], router.tags.find(t => t[0] === 'imkc')?.[1] || '', contentKey.iykcPubkey),
1377
- /scope mismatch/
1378
- )
1379
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
1380
- })
1381
-
1382
- test('unwrapEvent can decrypt iykc rows through receiverSigner without direct content signer access', async () => {
1383
- const alice = signer()
1384
- const bob = signer()
1385
- const bobContent = signer()
1386
- const bobProxy = await signerWithInternalContentKey(bob, bobContent)
1387
- const bobPubkey = await bob.getPublicKey()
1388
- const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
1389
- const contentKey = parseContentKeyEvent(contentKeyEvent)
1390
- const original = eventFixture('private')
1391
- const [wrapped] = await wrapEvent({
1392
- senderSigner: alice,
1393
- receivers: [bobPubkey],
1394
- event: original,
1395
- _getIykcProofs: async () => ({
1396
- [bobPubkey]: contentKey
1397
- })
1398
- })
1399
-
1400
- assert.deepEqual(
1401
- await unwrapEvent({
1402
- receiverSigner: bobProxy,
1403
- privateChannelSigner: alice,
1404
- event: wrapped,
1405
- receiverPubkey: bobPubkey
1406
- }),
1407
- unwrappedFixture(original, await alice.getPublicKey())
1408
- )
1409
- })
1410
-
1411
- test('unwrapEvent lets receiverSigner resolve older iykc when current content signer differs', async () => {
1412
- const alice = signer()
1413
- const bob = signer()
1414
- const bobOldContent = signer()
1415
- const bobLatestContent = signer()
1416
- const bobProxy = await signerWithInternalContentKey(bob, bobOldContent)
1417
- const bobPubkey = await bob.getPublicKey()
1418
- const oldContentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobOldContent, createdAt: 7 }))
1419
- const original = eventFixture('older key private')
1420
- const [wrapped] = await wrapEvent({
1421
- senderSigner: alice,
1422
- receivers: [bobPubkey],
1423
- event: original,
1424
- _getIykcProofs: async () => ({
1425
- [bobPubkey]: oldContentKey
1426
- })
1427
- })
1428
-
1429
- assert.deepEqual(
1430
- await unwrapEvent({
1431
- receiverSigner: bobProxy,
1432
- iykcSigner: bobLatestContent,
1433
- privateChannelSigner: alice,
1434
- event: wrapped,
1435
- receiverPubkey: bobPubkey
1436
- }),
1437
- unwrappedFixture(original, await alice.getPublicKey())
1438
- )
1439
- })
1440
-
1441
- test('wrapEvent accepts explicit receiver content keys with valid proofs', async () => {
1442
- const alice = signer()
1443
- const bob = signer()
1444
- const bobContent = signer()
1445
- const bobPubkey = await bob.getPublicKey()
1446
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1447
- const original = eventFixture('private')
1448
- NsecSigner.setContentSigners(bob, [bobContent])
1449
- const [wrapped] = await wrapEvent({
1450
- senderSigner: alice,
1451
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1452
- event: original,
1453
- _getIykcProofs: noContentKeys
1454
- })
1455
- const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
1456
- const line = routerRecipientRows(router)[0]
1457
-
1458
- assert.equal(line.length, 4)
1459
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, await alice.getPublicKey()))
1460
- })
1461
-
1462
- test('wrapEvent rejects explicit receiver content keys without valid proofs', async () => {
1463
- const alice = signer()
1464
- const bob = signer()
1465
- const bobContent = signer()
1466
- const bobPubkey = await bob.getPublicKey()
1467
- const bobContentPubkey = await bobContent.getPublicKey()
1468
-
1469
- await assert.rejects(
1470
- () => wrapEvent({
1471
- senderSigner: alice,
1472
- receivers: [[bobPubkey, bobContentPubkey]],
1473
- event: eventFixture('private'),
1474
- _getIykcProofs: noContentKeys
1475
- }),
1476
- /INVALID_IYKC_PROOF/
1477
- )
1478
- })
1479
-
1480
- test('unwrapEvent rejects receiver iykc rows without valid proofs', async () => {
1481
- const alice = signer()
1482
- const bob = signer()
1483
- const bobContent = signer()
1484
- const bobPubkey = await bob.getPublicKey()
1485
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1486
- const original = eventFixture('private')
1487
- const [wrapped] = await wrapEvent({
1488
- senderSigner: alice,
1489
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1490
- event: original,
1491
- _getIykcProofs: noContentKeys
1492
- })
1493
- const channelPubkey = await alice.getPublicKey()
1494
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1495
- const rows = routerJsonlRows(router)
1496
- const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
1497
- router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
1498
- const tampered = await alice.signEvent({
1499
- kind: PRIVATE_BROADCAST_KIND,
1500
- created_at: wrapped.created_at,
1501
- tags: wrapped.tags,
1502
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1503
- })
1504
-
1505
- await assert.rejects(
1506
- () => unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: tampered, receiverPubkey: bobPubkey }),
1507
- /INVALID_IYKC_PROOF/
1508
- )
1509
- })
1510
-
1511
- test('subscribe only emits receiver content key usage after iykc proof validation', async () => {
1512
- const originalSubscribeMany = pool.subscribeMany
1513
- let handlers = null
1514
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1515
- handlers = nextHandlers
1516
- return { close: () => {} }
1517
- }
1518
-
1519
- try {
1520
- const alice = signer()
1521
- const bob = signer()
1522
- const bobContent = signer()
1523
- const bobPubkey = await bob.getPublicKey()
1524
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1525
- const [wrapped] = await wrapEvent({
1526
- senderSigner: alice,
1527
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1528
- event: eventFixture('private'),
1529
- _getIykcProofs: noContentKeys
1530
- })
1531
- const channelPubkey = await alice.getPublicKey()
1532
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1533
- const rows = routerJsonlRows(router)
1534
- const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
1535
- router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
1536
- const tampered = await alice.signEvent({
1537
- kind: PRIVATE_BROADCAST_KIND,
1538
- created_at: wrapped.created_at,
1539
- tags: wrapped.tags,
1540
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1541
- })
1542
- const usages = []
1543
- const errors = []
1544
-
1545
- subscribe({
1546
- privateChannelSigner: alice,
1547
- receiverPubkey: bobPubkey,
1548
- relays: ['wss://relay.example'],
1549
- onContentKeyUsage: usage => usages.push(usage),
1550
- onError: err => errors.push(err)
1551
- })
1552
-
1553
- await handlers.onevent(tampered)
1554
-
1555
- assert.deepEqual(usages, [])
1556
- assert.equal(errors.length, 1)
1557
- assert.equal(errors[0].message, 'INVALID_IYKC_PROOF')
1558
- } finally {
1559
- pool.subscribeMany = originalSubscribeMany
1560
- }
1561
- })
1562
-
1563
- test('wrapEvent chunks large jsonl without oversize events and unwraps reassembled bytes', async () => {
1564
- const alice = signer()
1565
- const imkc = signer()
1566
- const bob = signer()
1567
- const bobPubkey = await bob.getPublicKey()
1568
- const imkcPubkey = await imkc.getPublicKey()
1569
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1570
- NsecSigner.setContentSigners(alice, [imkc])
1571
- const wrapped = await wrapEvent({
1572
- senderSigner: alice,
1573
- imkcSigner: imkc,
1574
- receivers: [bobPubkey],
1575
- deletionPubkey: 'd'.repeat(64),
1576
- event: original,
1577
- _getIykcProofs: noContentKeys
1578
- })
1579
-
1580
- assert.ok(wrapped.length > 1)
1581
- const deletionPubkeys = new Set()
1582
- for (const event of wrapped) {
1583
- assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
1584
- deletionPubkeys.add(event.tags.find(tag => tag[0] === 's')?.[1])
1585
- }
1586
- assert.deepEqual(deletionPubkeys, new Set(['d'.repeat(64)]))
1587
-
1588
- const routers = []
1589
- for (const event of wrapped) {
1590
- routers.push(await decryptPrivateBroadcast(alice, await alice.getPublicKey(), event.content))
1591
- }
1592
- assert.equal(routers[0].kind, ROUTER_KIND)
1593
- assert.equal(routers[0].tags.find(t => t[0] === 'r')?.[1], bobPubkey)
1594
- assert.equal(routers[0].tags.find(t => t[0] === 'imkc')?.[1], imkcPubkey)
1595
- assert.ok(routers[0].tags.find(t => t[0] === 'imkc')?.[2])
1596
- assert.equal(routers.length, Number(routers[0].tags.find(t => t[0] === 'c')[2]))
1597
- })
1598
-
1599
- test('wrapEvents cleans temporary chunks when the stream is stopped early', async () => {
1600
- const alice = signer()
1601
- const bob = signer()
1602
- const bobPubkey = await bob.getPublicKey()
1603
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1604
- const stream = wrapEvents({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1605
-
1606
- const first = await stream.next()
1607
- assert.equal(first.done, false)
1608
- assert.notEqual(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1609
-
1610
- await stream.return()
1611
-
1612
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1613
- })
1614
-
1615
- test('wrapEvents uses a caller-supplied temporary storage area', async () => {
1616
- const alice = signer()
1617
- const bob = signer()
1618
- const bobPubkey = await bob.getPublicKey()
1619
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1620
- const stream = wrapEvents({
1621
- senderSigner: alice,
1622
- receivers: [bobPubkey],
1623
- event: original,
1624
- temporaryStorageArea: globalThis.localStorage,
1625
- _getIykcProofs: noContentKeys
1626
- })
1627
-
1628
- await stream.next()
1629
-
1630
- assert.notEqual(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1631
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1632
-
1633
- await stream.return()
1634
-
1635
- assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1636
- })
1637
-
1638
- test('subscribe buffers out-of-order chunks and unwraps when the missing chunk arrives', async () => {
1639
- const originalSubscribeMany = pool.subscribeMany
1640
- let handlers = null
1641
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1642
- handlers = nextHandlers
1643
- return { close: () => {} }
1644
- }
1645
-
1646
- try {
1647
- const alice = signer()
1648
- const bob = signer()
1649
- const bobPubkey = await bob.getPublicKey()
1650
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1651
- const wrapped = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1652
- const events = []
1653
- const chunks = []
1654
-
1655
- assert.ok(wrapped.length > 1)
1656
- subscribe({
1657
- receiverSigner: bob,
1658
- privateChannelSigner: alice,
1659
- receiverPubkey: bobPubkey,
1660
- relays: ['wss://relay.example'],
1661
- onChunk: chunk => chunks.push(chunk),
1662
- onEvent: event => events.push(event),
1663
- _getIykcProofs: noContentKeys
1664
- })
1665
-
1666
- for (const event of wrapped.slice(1)) await handlers.onevent(event)
1667
-
1668
- assert.equal(events.length, 0)
1669
- assert.ok(chunks.at(-1).missing.includes(0))
1670
-
1671
- await handlers.onevent(wrapped[0])
1672
-
1673
- assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
1674
- } finally {
1675
- pool.subscribeMany = originalSubscribeMany
1676
- }
1677
- })
1678
-
1679
- test('subscribe drops invalid signed inner events and emits an error', async () => {
1680
- const originalSubscribeMany = pool.subscribeMany
1681
- let handlers = null
1682
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1683
- handlers = nextHandlers
1684
- return { close: () => {} }
1685
- }
1686
-
1687
- try {
1688
- const alice = signer()
1689
- const bob = signer()
1690
- const bobPubkey = await bob.getPublicKey()
1691
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
1692
- const [wrapped] = await wrapEvent({
1693
- senderSigner: alice,
1694
- receivers: [bobPubkey],
1695
- event: { ...signed, content: 'tampered' },
1696
- _getIykcProofs: noContentKeys
1697
- })
1698
- const events = []
1699
- const errors = []
1700
-
1701
- subscribe({
1702
- receiverSigner: bob,
1703
- privateChannelSigner: alice,
1704
- receiverPubkey: bobPubkey,
1705
- relays: ['wss://relay.example'],
1706
- onEvent: event => events.push(event),
1707
- onError: err => errors.push(err),
1708
- _getIykcProofs: noContentKeys
1709
- })
1710
-
1711
- await handlers.onevent(wrapped)
1712
- await handlers.onevent(wrapped)
1713
-
1714
- assert.equal(events.length, 0)
1715
- assert.equal(errors.length, 1)
1716
- assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
1717
- } finally {
1718
- pool.subscribeMany = originalSubscribeMany
1719
- }
1720
- })
1721
-
1722
- test('subscribe forgets ignored groups after their ttl', async () => {
1723
- const originalSubscribeMany = pool.subscribeMany
1724
- const originalNow = Date.now
1725
- let handlers = null
1726
- let now = 1000
1727
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1728
- handlers = nextHandlers
1729
- return { close: () => {} }
1730
- }
1731
- Date.now = () => now
1732
-
1733
- try {
1734
- const alice = signer()
1735
- const bob = signer()
1736
- const bobPubkey = await bob.getPublicKey()
1737
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
1738
- const [wrapped] = await wrapEvent({
1739
- senderSigner: alice,
1740
- receivers: [bobPubkey],
1741
- event: { ...signed, content: 'tampered' },
1742
- _getIykcProofs: noContentKeys
1743
- })
1744
- const errors = []
1745
-
1746
- subscribe({
1747
- receiverSigner: bob,
1748
- privateChannelSigner: alice,
1749
- receiverPubkey: bobPubkey,
1750
- relays: ['wss://relay.example'],
1751
- ignoredGroupTtlMs: 5,
1752
- onError: err => errors.push(err),
1753
- _getIykcProofs: noContentKeys
1754
- })
1755
-
1756
- await handlers.onevent(wrapped)
1757
- now += 4
1758
- await handlers.onevent(wrapped)
1759
- now += 2
1760
- await handlers.onevent(wrapped)
1761
-
1762
- assert.equal(errors.length, 2)
1763
- assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
1764
- assert.equal(errors[1].message, 'INVALID_SIGNED_INNER_EVENT')
1765
- } finally {
1766
- Date.now = originalNow
1767
- pool.subscribeMany = originalSubscribeMany
1768
- }
1769
- })
1770
-
1771
- test('subscribe evicts old ignored groups when the tombstone cache is full', async () => {
1772
- const originalSubscribeMany = pool.subscribeMany
1773
- let handlers = null
1774
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1775
- handlers = nextHandlers
1776
- return { close: () => {} }
1777
- }
1778
-
1779
- try {
1780
- const alice = signer()
1781
- const bob = signer()
1782
- const bobPubkey = await bob.getPublicKey()
1783
- const signedA = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'a' }, generateSecretKey())
1784
- const signedB = finalizeEvent({ kind: 9002, created_at: 3, tags: [], content: 'b' }, generateSecretKey())
1785
- const [wrappedA] = await wrapEvent({
1786
- senderSigner: alice,
1787
- receivers: [bobPubkey],
1788
- event: { ...signedA, content: 'tampered-a' },
1789
- _getIykcProofs: noContentKeys
1790
- })
1791
- const [wrappedB] = await wrapEvent({
1792
- senderSigner: alice,
1793
- receivers: [bobPubkey],
1794
- event: { ...signedB, content: 'tampered-b' },
1795
- _getIykcProofs: noContentKeys
1796
- })
1797
- const errors = []
1798
-
1799
- subscribe({
1800
- receiverSigner: bob,
1801
- privateChannelSigner: alice,
1802
- receiverPubkey: bobPubkey,
1803
- relays: ['wss://relay.example'],
1804
- ignoredGroupMaxEntries: 1,
1805
- onError: err => errors.push(err),
1806
- _getIykcProofs: noContentKeys
1807
- })
1808
-
1809
- await handlers.onevent(wrappedA)
1810
- await handlers.onevent(wrappedA)
1811
- await handlers.onevent(wrappedB)
1812
- await handlers.onevent(wrappedA)
1813
-
1814
- assert.equal(errors.length, 3)
1815
- assert.ok(errors.every(err => err.message === 'INVALID_SIGNED_INNER_EVENT'))
1816
- } finally {
1817
- pool.subscribeMany = originalSubscribeMany
1818
- }
1819
- })
1820
-
1821
- test('received chunk store purges stale incomplete groups', () => {
1822
- const originalNow = Date.now
1823
- const storage = (() => {
1824
- const data = new Map()
1825
- return {
1826
- clear: () => data.clear(),
1827
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
1828
- removeItem: key => { data.delete(String(key)) },
1829
- setItem: (key, value) => { data.set(String(key), String(value)) },
1830
- get length () { return data.size }
1831
- }
1832
- })()
1833
- let now = 1000
1834
- Date.now = () => now
1835
-
1836
- try {
1837
- const store = createReceivedChunkStore({
1838
- prefix: 'test:received-chunks',
1839
- storageArea: storage,
1840
- ttlMs: 5
1841
- })
1842
- store.put({
1843
- channelPubkey: 'channel',
1844
- routerPubkey: 'router',
1845
- index: 1,
1846
- total: 2,
1847
- content: 'abc'
1848
- })
1849
-
1850
- assert.ok(storage.length > 0)
1851
-
1852
- now += 6
1853
- store.cleanupStale()
1854
-
1855
- assert.equal(storage.length, 0)
1856
- } finally {
1857
- Date.now = originalNow
1858
- }
1859
- })
1860
-
1861
- test('received chunk store resumes incremental parsing after reload', async () => {
1862
- const storage = (() => {
1863
- const data = new Map()
1864
- return {
1865
- clear: () => data.clear(),
1866
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
1867
- removeItem: key => { data.delete(String(key)) },
1868
- setItem: (key, value) => { data.set(String(key), String(value)) },
1869
- get length () { return data.size }
1870
- }
1871
- })()
1872
- const line = `${JSON.stringify(['alice', 'ciphertext'])}\n`
1873
- const first = line.slice(0, 12)
1874
- const second = line.slice(12)
1875
- const firstStore = createReceivedChunkStore({
1876
- prefix: 'test:received-chunks:reload',
1877
- storageArea: storage
1878
- })
1879
- const lines = []
1880
-
1881
- firstStore.put({
1882
- channelPubkey: 'channel',
1883
- routerPubkey: 'router',
1884
- index: 0,
1885
- total: 2,
1886
- content: Buffer.from(first).toString('base64')
1887
- })
1888
-
1889
- const firstDrain = await firstStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
1890
-
1891
- assert.equal(firstDrain.complete, false)
1892
- assert.equal(firstDrain.meta.nextIndex, 1)
1893
- assert.equal(firstDrain.meta.carry, first)
1894
- assert.deepEqual(lines, [])
1895
-
1896
- const secondStore = createReceivedChunkStore({
1897
- prefix: 'test:received-chunks:reload',
1898
- storageArea: storage
1899
- })
1900
- secondStore.put({
1901
- channelPubkey: 'channel',
1902
- routerPubkey: 'router',
1903
- index: 1,
1904
- total: 2,
1905
- content: Buffer.from(second).toString('base64')
1906
- })
1907
-
1908
- const secondDrain = await secondStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
1909
-
1910
- assert.equal(secondDrain.complete, true)
1911
- assert.deepEqual(lines, [line.trim()])
1912
- })