libp2r2p 0.0.1 → 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,1899 +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 add a normalized deletion pubkey when supplied', 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
- test('publish splits relay-targeted routers by receiver subset with separate router pubkeys', async () => {
483
- const alice = signer()
484
- const bob = signer()
485
- const carol = signer()
486
- const dave = signer()
487
- const alicePubkey = await alice.getPublicKey()
488
- const bobPubkey = await bob.getPublicKey()
489
- const carolPubkey = await carol.getPublicKey()
490
- const davePubkey = await dave.getPublicKey()
491
- const event = eventFixture('split publish')
492
- const published = []
493
- const temporaryWrites = []
494
- const temporaryStorageArea = {
495
- getItem: key => globalThis.localStorage.getItem(key),
496
- removeItem: key => globalThis.localStorage.removeItem(key),
497
- setItem: (key, value) => {
498
- temporaryWrites.push(key)
499
- globalThis.localStorage.setItem(key, value)
500
- }
501
- }
502
-
503
- await publish({
504
- senderSigner: alice,
505
- receivers: [bobPubkey, carolPubkey, davePubkey],
506
- relayToReceivers: new Map([
507
- ['wss://one.example', [bobPubkey, carolPubkey]],
508
- ['wss://two.example', [carolPubkey, bobPubkey]],
509
- ['wss://three.example', [carolPubkey, davePubkey]]
510
- ]),
511
- recoveryRelays: ['wss://seed.example', 'wss://one.example'],
512
- deletionPubkey: 'd'.repeat(64),
513
- event,
514
- temporaryStorageArea,
515
- _getIykcProofs: noContentKeys,
516
- _publish: async (outer, relays) => {
517
- published.push({ outer, relays })
518
- return { success: true }
519
- }
520
- })
521
-
522
- assert.equal(published.length, 2)
523
- assert.deepEqual(published.map(({ outer }) => outer.tags.find(tag => tag[0] === 's')), [
524
- ['s', 'd'.repeat(64)],
525
- ['s', 'd'.repeat(64)]
526
- ])
527
- assert.deepEqual(published[0].relays, ['wss://one.example', 'wss://two.example', 'wss://seed.example'])
528
- assert.deepEqual(published[1].relays, ['wss://three.example', 'wss://seed.example', 'wss://one.example'])
529
-
530
- const routers = []
531
- for (const { outer } of published) {
532
- routers.push(await decryptPrivateBroadcast(alice, alicePubkey, outer.content))
533
- }
534
- assert.deepEqual(routers.map(router => router.tags.some(tag => tag[0] === 'id')), [false, false])
535
- assert.notEqual(routers[0].pubkey, routers[1].pubkey)
536
- assert.deepEqual(
537
- routerRecipientRows(routers[0]).map(row => row[0]).sort(),
538
- [bobPubkey, carolPubkey].sort()
539
- )
540
- assert.deepEqual(
541
- routerRecipientRows(routers[1]).map(row => row[0]),
542
- [carolPubkey, davePubkey]
543
- )
544
- const firstLines = routerJsonlLines(routers[0])
545
- const secondLines = routerJsonlLines(routers[1])
546
- assert.equal(firstLines[0], secondLines[0])
547
- assert.equal(
548
- firstLines.find(line => JSON.parse(line)[0] === carolPubkey),
549
- secondLines.find(line => JSON.parse(line)[0] === carolPubkey)
550
- )
551
- assert.ok(temporaryWrites.includes(TEMPORARY_STORAGE_KEYS_KEY))
552
- assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
553
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
554
- })
555
-
556
- test('publish sends wrapped events through RelayPool.sendEvent', async () => {
557
- const originalSendEvent = relayPool.sendEvent
558
- const published = []
559
- relayPool.sendEvent = async (outer, relays) => {
560
- published.push({ outer, relays })
561
- return { success: true, total: relays.length, promise: Promise.resolve({ success: true }) }
562
- }
563
-
564
- try {
565
- const alice = signer()
566
- const bob = signer()
567
- await publish({
568
- senderSigner: alice,
569
- receivers: [await bob.getPublicKey()],
570
- event: eventFixture('relay pool publish'),
571
- relays: ['wss://relay.example'],
572
- _getIykcProofs: noContentKeys
573
- })
574
- } finally {
575
- relayPool.sendEvent = originalSendEvent
576
- }
577
-
578
- assert.equal(published.length, 1)
579
- assert.equal(published[0].outer.kind, PRIVATE_BROADCAST_KIND)
580
- assert.deepEqual(published[0].relays, ['wss://relay.example'])
581
- })
582
-
583
- test('fetch uses complete RelayPool reads for private-channel recovery', async () => {
584
- let call
585
- const events = await fetch({
586
- relays: ['wss://relay.example'],
587
- _getEvents: async (filter, relays, options) => {
588
- call = { filter, relays, options }
589
- return { result: [] }
590
- }
591
- })
592
-
593
- assert.deepEqual(events, [])
594
- assert.deepEqual(call.relays, ['wss://relay.example'])
595
- assert.deepEqual(call.options, { timeout: 5000, timeoutAfterFirstEose: null })
596
- assert.deepEqual(call.filter.kinds, [PRIVATE_BROADCAST_KIND])
597
- })
598
-
599
- test('publish cleans prepared rows when grouped publishing fails', async () => {
600
- const alice = signer()
601
- const bob = signer()
602
- const carol = signer()
603
- const bobPubkey = await bob.getPublicKey()
604
- const carolPubkey = await carol.getPublicKey()
605
-
606
- await assert.rejects(
607
- publish({
608
- senderSigner: alice,
609
- receivers: [bobPubkey, carolPubkey],
610
- relayToReceivers: new Map([
611
- ['wss://one.example', [bobPubkey]],
612
- ['wss://two.example', [carolPubkey]]
613
- ]),
614
- event: eventFixture('publish fail'),
615
- _getIykcProofs: noContentKeys,
616
- _publish: async () => { throw new Error('relay offline') }
617
- }),
618
- /relay offline/
619
- )
620
-
621
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
622
- })
623
-
624
- test('publishNymEvent mirrors carrier chunks to recovery relays', async () => {
625
- const channel = signer()
626
- const nym = signer()
627
- const published = []
628
-
629
- await publishNymEvent({
630
- nymSigner: nym,
631
- privateChannelSigner: channel,
632
- event: eventFixture('nym mirror'),
633
- relays: ['wss://receiver.example'],
634
- recoveryRelays: ['wss://seed.example', 'wss://receiver.example'],
635
- deletionPubkey: 'e'.repeat(64),
636
- _publish: async (outer, relays) => {
637
- published.push({ outer, relays })
638
- return { success: true }
639
- }
640
- })
641
-
642
- assert.equal(published.length, 1)
643
- assert.deepEqual(published[0].relays, ['wss://receiver.example', 'wss://seed.example'])
644
- assert.deepEqual(published[0].outer.tags.find(tag => tag[0] === 's'), ['s', 'e'.repeat(64)])
645
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
646
- })
647
-
648
- test('received chunk default cap is proportional to private-channel chunk size', () => {
649
- assert.equal(getJsonlChunkByteSize(), 30159)
650
- assert.equal(DEFAULT_RECEIVED_CHUNK_MAX_BYTES, Math.min(getJsonlChunkByteSize() * 64, 3 * 1024 * 1024))
651
- })
652
-
653
- test('wrapEvent supports overriding the outer expiration window', async () => {
654
- const alice = signer()
655
- const bob = signer()
656
- const bobPubkey = await bob.getPublicKey()
657
- const before = Math.floor(Date.now() / 1000)
658
- const [wrapped] = await wrapEvent({
659
- senderSigner: alice,
660
- receivers: [bobPubkey],
661
- event: eventFixture('hello bob'),
662
- expirationSeconds: 7 * 24 * 60 * 60,
663
- _getIykcProofs: noContentKeys
664
- })
665
-
666
- assert.ok(Number(wrapped.tags[0][1]) >= before + 7 * 24 * 60 * 60)
667
- })
668
-
669
- test('unwrapEvent returns the addressed receiver event or null', async () => {
670
- const alice = signer()
671
- const bob = signer()
672
- const carol = signer()
673
- const bobPubkey = await bob.getPublicKey()
674
- const carolPubkey = await carol.getPublicKey()
675
- const alicePubkey = await alice.getPublicKey()
676
- const original = { ...eventFixture('private'), pubkey: '0'.repeat(64), id: 'f'.repeat(64) }
677
- const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey, carolPubkey], event: original, _getIykcProofs: noContentKeys })
678
-
679
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
680
- assert.deepEqual(await unwrapEvent({ receiverSigner: carol, privateChannelSigner: alice, event: wrapped, receiverPubkey: carolPubkey }), unwrappedFixture(original, alicePubkey))
681
- assert.equal(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: await signer().getPublicKey() }), null)
682
- })
683
-
684
- test('wrapEvent can encrypt the outer router to a separate reader key', async () => {
685
- const sender = signer()
686
- const channel = signer()
687
- const reader = signer()
688
- const bob = signer()
689
- const bobPubkey = await bob.getPublicKey()
690
- const readerPubkey = await reader.getPublicKey()
691
- const channelPubkey = await channel.getPublicKey()
692
- const original = eventFixture('reader decrypts channel')
693
- const [wrapped] = await wrapEvent({
694
- senderSigner: sender,
695
- privateChannelSigner: channel,
696
- privateChannelReaderPubkey: readerPubkey,
697
- receivers: [bobPubkey],
698
- event: original,
699
- _getIykcProofs: noContentKeys
700
- })
701
-
702
- assert.equal(wrapped.pubkey, channelPubkey)
703
- const router = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
704
- assert.equal(router.kind, ROUTER_KIND)
705
- assert.deepEqual(await unwrapEvent({
706
- receiverSigner: bob,
707
- privateChannelSigner: channel,
708
- privateChannelReaderSigner: reader,
709
- event: wrapped,
710
- receiverPubkey: bobPubkey
711
- }), unwrappedFixture(original, await sender.getPublicKey()))
712
- assert.deepEqual(await unwrapEvent({
713
- receiverSigner: bob,
714
- privateChannelSigner: channel,
715
- privateChannelReaderPubkey: readerPubkey,
716
- event: wrapped,
717
- receiverPubkey: bobPubkey
718
- }), unwrappedFixture(original, await sender.getPublicKey()))
719
- })
720
-
721
- test('subscribe can read channel events with only a reader signer', async () => {
722
- const originalSubscribeMany = pool.subscribeMany
723
- let handlers = null
724
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
725
- handlers = nextHandlers
726
- return { close: () => {} }
727
- }
728
-
729
- try {
730
- const sender = signer()
731
- const channel = signer()
732
- const reader = signer()
733
- const bob = signer()
734
- const bobPubkey = await bob.getPublicKey()
735
- const channelPubkey = await channel.getPublicKey()
736
- const original = eventFixture('reader-only subscribe')
737
- const [wrapped] = await wrapEvent({
738
- senderSigner: sender,
739
- privateChannelSigner: channel,
740
- privateChannelReaderPubkey: await reader.getPublicKey(),
741
- receivers: [bobPubkey],
742
- event: original,
743
- _getIykcProofs: noContentKeys
744
- })
745
- const events = []
746
-
747
- subscribe({
748
- receiverSigner: bob,
749
- privateChannelSigner: null,
750
- privateChannelReaderSigner: reader,
751
- privateChannelPubkey: channelPubkey,
752
- receiverPubkey: bobPubkey,
753
- relays: ['wss://relay.example'],
754
- onEvent: event => events.push(event)
755
- })
756
- await handlers.onevent(wrapped)
757
-
758
- assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
759
- } finally {
760
- pool.subscribeMany = originalSubscribeMany
761
- }
762
- })
763
-
764
- test('live-only subscribe closes its RelayPool stream', async () => {
765
- const sender = signer()
766
- const bob = signer()
767
- const bobPubkey = await bob.getPublicKey()
768
- const channelPubkey = await sender.getPublicKey()
769
- const original = eventFixture('live subscription')
770
- const [wrapped] = await wrapEvent({
771
- senderSigner: sender,
772
- receivers: [bobPubkey],
773
- event: original,
774
- _getIykcProofs: noContentKeys
775
- })
776
- const received = []
777
- let aborted = false
778
-
779
- const subscription = subscribe({
780
- receiverSigner: bob,
781
- privateChannelSigner: sender,
782
- privateChannelPubkey: channelPubkey,
783
- receiverPubkey: bobPubkey,
784
- relays: ['wss://relay.example'],
785
- liveOnly: true,
786
- onEvent: event => received.push(event),
787
- _liveEventsGenerator: (_filter, _relays, { signal }) => (async function * () {
788
- yield wrapped
789
- await new Promise(resolve => signal.addEventListener('abort', () => {
790
- aborted = true
791
- resolve()
792
- }, { once: true }))
793
- })()
794
- })
795
-
796
- for (let attempt = 0; attempt < 20 && received.length === 0; attempt++) {
797
- await new Promise(resolve => setImmediate(resolve))
798
- }
799
- assert.deepEqual(received, [unwrappedFixture(original, await sender.getPublicKey())])
800
-
801
- subscription.close()
802
- for (let attempt = 0; attempt < 20 && !aborted; attempt++) {
803
- await new Promise(resolve => setImmediate(resolve))
804
- }
805
- assert.ok(aborted)
806
- })
807
-
808
- test('subscribe can read reader-targeted channel events with the writer signer', async () => {
809
- const originalSubscribeMany = pool.subscribeMany
810
- let handlers = null
811
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
812
- handlers = nextHandlers
813
- return { close: () => {} }
814
- }
815
-
816
- try {
817
- const sender = signer()
818
- const channel = signer()
819
- const reader = signer()
820
- const bob = signer()
821
- const bobPubkey = await bob.getPublicKey()
822
- const readerPubkey = await reader.getPublicKey()
823
- const channelPubkey = await channel.getPublicKey()
824
- const original = eventFixture('writer reads reader-targeted router')
825
- const [wrapped] = await wrapEvent({
826
- senderSigner: sender,
827
- privateChannelSigner: channel,
828
- privateChannelReaderPubkey: readerPubkey,
829
- receivers: [bobPubkey],
830
- event: original,
831
- _getIykcProofs: noContentKeys
832
- })
833
- const events = []
834
-
835
- subscribe({
836
- receiverSigner: bob,
837
- privateChannelSigner: channel,
838
- privateChannelReaderPubkey: readerPubkey,
839
- privateChannelPubkey: channelPubkey,
840
- receiverPubkey: bobPubkey,
841
- relays: ['wss://relay.example'],
842
- onEvent: event => events.push(event)
843
- })
844
- await handlers.onevent(wrapped)
845
-
846
- assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
847
- } finally {
848
- pool.subscribeMany = originalSubscribeMany
849
- }
850
- })
851
-
852
- test('unwrapEvent preserves valid signed inner events', async () => {
853
- const alice = signer()
854
- const bob = signer()
855
- const authorSecret = generateSecretKey()
856
- const bobPubkey = await bob.getPublicKey()
857
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [['x', '1']], content: 'signed' }, authorSecret)
858
- const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: signed, _getIykcProofs: noContentKeys })
859
-
860
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), signed)
861
- })
862
-
863
- test('unwrapEvent rejects invalid signed inner events', async () => {
864
- const alice = signer()
865
- const bob = signer()
866
- const bobPubkey = await bob.getPublicKey()
867
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
868
- const [wrapped] = await wrapEvent({
869
- senderSigner: alice,
870
- receivers: [bobPubkey],
871
- event: { ...signed, content: 'tampered' },
872
- _getIykcProofs: noContentKeys
873
- })
874
-
875
- await assert.rejects(
876
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
877
- /INVALID_SIGNED_INNER_EVENT/
878
- )
879
- })
880
-
881
- test('unwrapEvent rejects malformed signed inner events', async () => {
882
- const alice = signer()
883
- const bob = signer()
884
- const bobPubkey = await bob.getPublicKey()
885
- const [wrapped] = await wrapEvent({
886
- senderSigner: alice,
887
- receivers: [bobPubkey],
888
- event: { id: 'e'.repeat(64), pubkey: 'a'.repeat(64), kind: 9002, created_at: 2, tags: [], content: 'signed', sig: 42 },
889
- _getIykcProofs: noContentKeys
890
- })
891
-
892
- await assert.rejects(
893
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
894
- /INVALID_SIGNED_INNER_EVENT/
895
- )
896
- })
897
-
898
- test('wrapNymEvent signs nym carriers and reconstructs nym rumors', async () => {
899
- const channel = signer()
900
- const reader = signer()
901
- const nym = signer()
902
- const channelPubkey = await channel.getPublicKey()
903
- const readerPubkey = await reader.getPublicKey()
904
- const nymPubkey = await nym.getPublicKey()
905
- const original = eventFixture('nym rumor')
906
- const [wrapped] = await wrapNymEvent({
907
- nymSigner: nym,
908
- privateChannelSigner: channel,
909
- privateChannelReaderPubkey: readerPubkey,
910
- event: original
911
- })
912
-
913
- assert.equal(wrapped.kind, PRIVATE_BROADCAST_KIND)
914
- assert.equal(wrapped.pubkey, channelPubkey)
915
- assert.ok(new TextEncoder().encode(JSON.stringify(wrapped)).length <= MAX_EVENT_BYTES)
916
- const carrier = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
917
- const expected = unwrappedFixture(original, nymPubkey)
918
-
919
- assert.equal(carrier.kind, NYM_CARRIER_KIND)
920
- assert.equal(carrier.pubkey, nymPubkey)
921
- assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], expected.id)
922
- assert.deepEqual(carrier.tags.find(tag => tag[0] === 'c'), ['c', '0', '1'])
923
- assert.deepEqual(eventFromNymCarriers([carrier]), expected)
924
- })
925
-
926
- test('wrapNymEvent preserves signed inner event authors distinct from carrier nym', async () => {
927
- const channel = signer()
928
- const nym = signer()
929
- const signed = finalizeEvent({ kind: 9002, created_at: 23, tags: [['x', 'signed']], content: 'signed by someone else' }, generateSecretKey())
930
- const [wrapped] = await wrapNymEvent({
931
- nymSigner: nym,
932
- privateChannelSigner: channel,
933
- event: signed
934
- })
935
- const carrier = await decryptPrivateBroadcast(channel, await channel.getPublicKey(), wrapped.content)
936
-
937
- assert.notEqual(carrier.pubkey, signed.pubkey)
938
- assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], signed.id)
939
- assert.deepEqual(eventFromNymCarriers([carrier]), signed)
940
- })
941
-
942
- test('subscribe buffers out-of-order nym carrier chunks by nym pubkey and inner id', async () => {
943
- const originalSubscribeMany = pool.subscribeMany
944
- let handlers = null
945
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
946
- handlers = nextHandlers
947
- return { close: () => {} }
948
- }
949
-
950
- try {
951
- const channel = signer()
952
- const nym = signer()
953
- const nymPubkey = await nym.getPublicKey()
954
- const original = eventFixture('x'.repeat(getNymCarrierChunkSize()))
955
- const wrapped = await wrapNymEvent({
956
- nymSigner: nym,
957
- privateChannelSigner: channel,
958
- event: original
959
- })
960
- const routedEvents = []
961
- const nymEvents = []
962
- const seeds = []
963
-
964
- assert.ok(wrapped.length > 1)
965
- for (const event of wrapped) assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
966
-
967
- subscribe({
968
- privateChannelSigner: channel,
969
- privateChannelPubkey: await channel.getPublicKey(),
970
- relays: ['wss://relay.example'],
971
- mode: 'seeder',
972
- onEvent: event => routedEvents.push(event),
973
- onNymEvent: event => nymEvents.push(event),
974
- onSeedEvent: seed => seeds.push(seed)
975
- })
976
- for (const event of [...wrapped].reverse()) await handlers.onevent(event)
977
-
978
- assert.deepEqual(routedEvents, [])
979
- assert.deepEqual(nymEvents, [unwrappedFixture(original, nymPubkey)])
980
- assert.equal(seeds.length, 1)
981
- assert.equal(seeds[0].recordType, 'nymCarrier_v1')
982
- assert.equal(seeds[0].carriers.length, wrapped.length)
983
- } finally {
984
- pool.subscribeMany = originalSubscribeMany
985
- }
986
- })
987
-
988
- test('subscribe separates nym carrier groups that use different chunk totals', async () => {
989
- const originalSubscribeMany = pool.subscribeMany
990
- let handlers = null
991
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
992
- handlers = nextHandlers
993
- return { close: () => {} }
994
- }
995
-
996
- try {
997
- const channel = signer()
998
- const nym = signer()
999
- const channelPubkey = await channel.getPublicKey()
1000
- const nymPubkey = await nym.getPublicKey()
1001
- const original = eventFixture('same inner event, different chunking')
1002
- const expected = unwrappedFixture(original, nymPubkey)
1003
- const encoded = Buffer.from(JSON.stringify(original)).toString('base64')
1004
- const midpoint = Math.ceil(encoded.length / 2)
1005
- const nymEvents = []
1006
- const errors = []
1007
-
1008
- async function carrier (index, total, content) {
1009
- return nym.signEvent({
1010
- kind: NYM_CARRIER_KIND,
1011
- created_at: 9,
1012
- tags: [['id', expected.id], ['c', String(index), String(total)]],
1013
- content
1014
- })
1015
- }
1016
-
1017
- async function outer (carrier) {
1018
- return channel.signEvent({
1019
- kind: PRIVATE_BROADCAST_KIND,
1020
- created_at: 10,
1021
- tags: [],
1022
- content: await encryptPrivateBroadcast(channel, channelPubkey, carrier)
1023
- })
1024
- }
1025
-
1026
- const twoChunkFirst = await outer(await carrier(0, 2, encoded.slice(0, midpoint)))
1027
- const oneChunk = await outer(await carrier(0, 1, encoded))
1028
- const twoChunkLast = await outer(await carrier(1, 2, encoded.slice(midpoint)))
1029
-
1030
- subscribe({
1031
- privateChannelSigner: channel,
1032
- privateChannelPubkey: channelPubkey,
1033
- relays: ['wss://relay.example'],
1034
- onNymEvent: event => nymEvents.push(event),
1035
- onError: err => errors.push(err)
1036
- })
1037
- await handlers.onevent(twoChunkFirst)
1038
- await handlers.onevent(oneChunk)
1039
- await handlers.onevent(twoChunkLast)
1040
-
1041
- assert.deepEqual(errors, [])
1042
- assert.deepEqual(nymEvents, [expected, expected])
1043
- } finally {
1044
- pool.subscribeMany = originalSubscribeMany
1045
- }
1046
- })
1047
-
1048
- test('unwrapEvent uses imkc tag as the row encryption pubkey', async () => {
1049
- const alice = signer()
1050
- const imkc = signer()
1051
- const bob = signer()
1052
- const alicePubkey = await alice.getPublicKey()
1053
- const bobPubkey = await bob.getPublicKey()
1054
- const imkcPubkey = await imkc.getPublicKey()
1055
- const original = eventFixture('private')
1056
- NsecSigner.setContentSigners(alice, [imkc])
1057
- const [wrapped] = await wrapEvent({ senderSigner: alice, imkcSigner: imkc, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1058
- const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
1059
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1060
-
1061
- assert.equal(router.tags.find(t => t[0] === 'f')?.[1], alicePubkey)
1062
- assert.equal(imkcTag?.[1], imkcPubkey)
1063
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: imkcPubkey, proof: imkcTag?.[2] }), true)
1064
- assert.deepEqual(
1065
- await unwrapEvent({
1066
- receiverSigner: bob,
1067
- privateChannelSigner: alice,
1068
- event: wrapped,
1069
- receiverPubkey: bobPubkey
1070
- }),
1071
- unwrappedFixture(original, alicePubkey)
1072
- )
1073
- })
1074
-
1075
- test('wrapEvent can add imkc from senderSigner Double-DH without direct content signer access', async () => {
1076
- const alice = signer()
1077
- const aliceContent = signer()
1078
- const bob = signer()
1079
- const aliceProxy = await signerWithInternalContentKey(alice, aliceContent)
1080
- const alicePubkey = await alice.getPublicKey()
1081
- const bobPubkey = await bob.getPublicKey()
1082
- const contentPubkey = await aliceContent.getPublicKey()
1083
- const original = eventFixture('private')
1084
- const [wrapped] = await wrapEvent({
1085
- senderSigner: aliceProxy,
1086
- privateChannelSigner: alice,
1087
- receivers: [bobPubkey],
1088
- event: original,
1089
- _getIykcProofs: noContentKeys
1090
- })
1091
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1092
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1093
-
1094
- assert.equal(imkcTag?.[1], contentPubkey)
1095
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey, proof: imkcTag?.[2] }), true)
1096
- assert.deepEqual(
1097
- await unwrapEvent({
1098
- receiverSigner: bob,
1099
- privateChannelSigner: alice,
1100
- event: wrapped,
1101
- receiverPubkey: bobPubkey
1102
- }),
1103
- unwrappedFixture(original, alicePubkey)
1104
- )
1105
- })
1106
-
1107
- test('wrapEvent uses Double-DH returned own content pubkey for imkc tag', async () => {
1108
- const alice = signer()
1109
- const oldContent = signer()
1110
- const actualContent = signer()
1111
- const bob = signer()
1112
- const alicePubkey = await alice.getPublicKey()
1113
- const bobPubkey = await bob.getPublicKey()
1114
- const actualContentPubkey = await actualContent.getPublicKey()
1115
- const senderSigner = {
1116
- getPublicKey: () => alice.getPublicKey(),
1117
- signEvent: event => alice.signEvent(event),
1118
- nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
1119
- nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
1120
- nip44EncryptDoubleDH: (...params) => {
1121
- NsecSigner.setContentSigners(alice, [actualContent])
1122
- return alice.nip44EncryptDoubleDH(...params)
1123
- }
1124
- }
1125
- const [wrapped] = await wrapEvent({
1126
- senderSigner,
1127
- imkcSigner: oldContent,
1128
- privateChannelSigner: alice,
1129
- receivers: [bobPubkey],
1130
- event: eventFixture('private'),
1131
- _getIykcProofs: noContentKeys
1132
- })
1133
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1134
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1135
-
1136
- assert.equal(imkcTag?.[1], actualContentPubkey)
1137
- assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: actualContentPubkey, proof: imkcTag?.[2] }), true)
1138
- })
1139
-
1140
- test('wrapEvent retries once when sender content key rotates while writing chunks', async () => {
1141
- const alice = signer()
1142
- const oldContent = signer()
1143
- const actualContent = signer()
1144
- const bob = signer()
1145
- const carol = signer()
1146
- const alicePubkey = await alice.getPublicKey()
1147
- const bobPubkey = await bob.getPublicKey()
1148
- const carolPubkey = await carol.getPublicKey()
1149
- const actualContentPubkey = await actualContent.getPublicKey()
1150
- let encryptCalls = 0
1151
- const senderSigner = {
1152
- getPublicKey: () => alice.getPublicKey(),
1153
- signEvent: event => alice.signEvent(event),
1154
- nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
1155
- nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
1156
- nip44EncryptDoubleDH: (...params) => {
1157
- encryptCalls++
1158
- NsecSigner.setContentSigners(alice, [encryptCalls === 1 ? oldContent : actualContent])
1159
- return alice.nip44EncryptDoubleDH(...params)
1160
- }
1161
- }
1162
- const original = eventFixture('private after rotation')
1163
- const [wrapped] = await wrapEvent({
1164
- senderSigner,
1165
- imkcSigner: oldContent,
1166
- privateChannelSigner: alice,
1167
- receivers: [bobPubkey, carolPubkey],
1168
- event: original,
1169
- _getIykcProofs: noContentKeys
1170
- })
1171
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1172
- const imkcTag = router.tags.find(t => t[0] === 'imkc')
1173
-
1174
- assert.equal(encryptCalls, 4)
1175
- assert.equal(imkcTag?.[1], actualContentPubkey)
1176
- assert.deepEqual(
1177
- await unwrapEvent({
1178
- receiverSigner: bob,
1179
- privateChannelSigner: alice,
1180
- event: wrapped,
1181
- receiverPubkey: bobPubkey
1182
- }),
1183
- unwrappedFixture(original, alicePubkey)
1184
- )
1185
- })
1186
-
1187
- test('unwrapEvent rejects sender imkc keys that do not match the ciphertext', async () => {
1188
- const alice = signer()
1189
- const oldImkc = signer()
1190
- const newImkc = signer()
1191
- const bob = signer()
1192
- const bobPubkey = await bob.getPublicKey()
1193
- NsecSigner.setContentSigners(alice, [oldImkc])
1194
- const [wrapped] = await wrapEvent({
1195
- senderSigner: alice,
1196
- imkcSigner: oldImkc,
1197
- receivers: [bobPubkey],
1198
- event: eventFixture('private'),
1199
- _getIykcProofs: noContentKeys
1200
- })
1201
- const channelPubkey = await alice.getPublicKey()
1202
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1203
- const newImkcPubkey = await newImkc.getPublicKey()
1204
- const newImkcProof = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: alice, contentKeySigner: newImkc, createdAt: 7 })).iykcProof
1205
- router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', newImkcPubkey, newImkcProof] : tag)
1206
- const tampered = await alice.signEvent({
1207
- kind: PRIVATE_BROADCAST_KIND,
1208
- created_at: wrapped.created_at,
1209
- tags: wrapped.tags,
1210
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1211
- })
1212
-
1213
- await assert.rejects(
1214
- () => unwrapEvent({
1215
- receiverSigner: bob,
1216
- privateChannelSigner: alice,
1217
- event: tampered,
1218
- receiverPubkey: bobPubkey
1219
- }),
1220
- /invalid MAC/
1221
- )
1222
- })
1223
-
1224
- test('unwrapEvent rejects sender imkc rows without valid proofs', async () => {
1225
- const alice = signer()
1226
- const imkc = signer()
1227
- const bob = signer()
1228
- const bobPubkey = await bob.getPublicKey()
1229
- NsecSigner.setContentSigners(alice, [imkc])
1230
- const [wrapped] = await wrapEvent({
1231
- senderSigner: alice,
1232
- imkcSigner: imkc,
1233
- receivers: [bobPubkey],
1234
- event: eventFixture('private'),
1235
- _getIykcProofs: noContentKeys
1236
- })
1237
- const channelPubkey = await alice.getPublicKey()
1238
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1239
- router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', tag[1]] : tag)
1240
- const tampered = await alice.signEvent({
1241
- kind: PRIVATE_BROADCAST_KIND,
1242
- created_at: wrapped.created_at,
1243
- tags: wrapped.tags,
1244
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1245
- })
1246
-
1247
- await assert.rejects(
1248
- () => unwrapEvent({
1249
- receiverSigner: bob,
1250
- privateChannelSigner: alice,
1251
- event: tampered,
1252
- receiverPubkey: bobPubkey
1253
- }),
1254
- /INVALID_IMKC_PROOF/
1255
- )
1256
- })
1257
-
1258
- test('subscribe emits content key usage for own sent direct messages', async () => {
1259
- const originalSubscribeMany = pool.subscribeMany
1260
- let handlers = null
1261
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1262
- handlers = nextHandlers
1263
- return { close: () => {} }
1264
- }
1265
-
1266
- try {
1267
- const alice = signer()
1268
- const bob = signer()
1269
- const bobPubkey = await bob.getPublicKey()
1270
- const [wrapped] = await wrapEvent({
1271
- senderSigner: alice,
1272
- receivers: [bobPubkey],
1273
- receiverTag: bobPubkey,
1274
- event: eventFixture('private'),
1275
- _getIykcProofs: noContentKeys
1276
- })
1277
- const usages = []
1278
- const delivered = []
1279
-
1280
- subscribe({
1281
- receiverSigner: alice,
1282
- privateChannelSigner: alice,
1283
- receiverPubkey: await alice.getPublicKey(),
1284
- relays: ['wss://relay.example'],
1285
- onContentKeyUsage: usage => usages.push(usage),
1286
- onEvent: event => delivered.push(event)
1287
- })
1288
- await handlers.onevent(wrapped)
1289
-
1290
- assert.equal(usages.length, 1)
1291
- assert.equal(usages[0].direction, 'sent')
1292
- assert.equal(usages[0].senderPubkey, await alice.getPublicKey())
1293
- assert.equal(usages[0].receiverPubkey, bobPubkey)
1294
- assert.equal(usages[0].contentKeyPubkey, '')
1295
- assert.equal(delivered.length, 0)
1296
- } finally {
1297
- pool.subscribeMany = originalSubscribeMany
1298
- }
1299
- })
1300
-
1301
- test('subscribe treats watchtower mode as recovery seed storage', async () => {
1302
- const originalSubscribeMany = pool.subscribeMany
1303
- let handlers = null
1304
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1305
- handlers = nextHandlers
1306
- return { close: () => {} }
1307
- }
1308
-
1309
- try {
1310
- const alice = signer()
1311
- const bob = signer()
1312
- const bobPubkey = await bob.getPublicKey()
1313
- const original = eventFixture('watchtower payload')
1314
- const [wrapped] = await wrapEvent({
1315
- senderSigner: alice,
1316
- receivers: [bobPubkey],
1317
- event: original,
1318
- _getIykcProofs: noContentKeys
1319
- })
1320
- const events = []
1321
- const seeds = []
1322
-
1323
- subscribe({
1324
- receiverSigner: bob,
1325
- privateChannelSigner: alice,
1326
- receiverPubkey: bobPubkey,
1327
- relays: ['wss://relay.example'],
1328
- mode: 'watchtower',
1329
- onEvent: event => events.push(event),
1330
- onSeedEvent: seed => seeds.push(seed)
1331
- })
1332
- await handlers.onevent(wrapped)
1333
-
1334
- assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
1335
- assert.equal(seeds.length, 1)
1336
- assert.equal(seeds[0].channelPubkey, await alice.getPublicKey())
1337
- assert.ok(seeds[0].router.content)
1338
- } finally {
1339
- pool.subscribeMany = originalSubscribeMany
1340
- }
1341
- })
1342
-
1343
- test('wrapEvent uses receiver content key rows when iykc is advertised', async () => {
1344
- const alice = signer()
1345
- const bob = signer()
1346
- const bobContent = signer()
1347
- const alicePubkey = await alice.getPublicKey()
1348
- const bobPubkey = await bob.getPublicKey()
1349
- const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
1350
- const contentKey = parseContentKeyEvent(contentKeyEvent)
1351
- const original = eventFixture('private')
1352
- const [wrapped] = await wrapEvent({
1353
- senderSigner: alice,
1354
- receivers: [bobPubkey],
1355
- event: original,
1356
- _getIykcProofs: async () => ({
1357
- [bobPubkey]: contentKey
1358
- })
1359
- })
1360
- const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
1361
- const line = routerRecipientRows(router)[0]
1362
-
1363
- assert.deepEqual(line.slice(0, 1), [bobPubkey])
1364
- assert.deepEqual(line.slice(2), [contentKey.iykcPubkey, contentKey.iykcProof])
1365
- await assert.rejects(
1366
- () => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
1367
- /RECEIVER_CONTENT_KEY_REQUIRED/
1368
- )
1369
- NsecSigner.setContentSigners(bob, [bobContent])
1370
- await assert.rejects(
1371
- () => bob.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', line[1], router.tags.find(t => t[0] === 'imkc')?.[1] || '', contentKey.iykcPubkey),
1372
- /scope mismatch/
1373
- )
1374
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
1375
- })
1376
-
1377
- test('unwrapEvent can decrypt iykc rows through receiverSigner without direct content signer access', async () => {
1378
- const alice = signer()
1379
- const bob = signer()
1380
- const bobContent = signer()
1381
- const bobProxy = await signerWithInternalContentKey(bob, bobContent)
1382
- const bobPubkey = await bob.getPublicKey()
1383
- const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
1384
- const contentKey = parseContentKeyEvent(contentKeyEvent)
1385
- const original = eventFixture('private')
1386
- const [wrapped] = await wrapEvent({
1387
- senderSigner: alice,
1388
- receivers: [bobPubkey],
1389
- event: original,
1390
- _getIykcProofs: async () => ({
1391
- [bobPubkey]: contentKey
1392
- })
1393
- })
1394
-
1395
- assert.deepEqual(
1396
- await unwrapEvent({
1397
- receiverSigner: bobProxy,
1398
- privateChannelSigner: alice,
1399
- event: wrapped,
1400
- receiverPubkey: bobPubkey
1401
- }),
1402
- unwrappedFixture(original, await alice.getPublicKey())
1403
- )
1404
- })
1405
-
1406
- test('unwrapEvent lets receiverSigner resolve older iykc when current content signer differs', async () => {
1407
- const alice = signer()
1408
- const bob = signer()
1409
- const bobOldContent = signer()
1410
- const bobLatestContent = signer()
1411
- const bobProxy = await signerWithInternalContentKey(bob, bobOldContent)
1412
- const bobPubkey = await bob.getPublicKey()
1413
- const oldContentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobOldContent, createdAt: 7 }))
1414
- const original = eventFixture('older key private')
1415
- const [wrapped] = await wrapEvent({
1416
- senderSigner: alice,
1417
- receivers: [bobPubkey],
1418
- event: original,
1419
- _getIykcProofs: async () => ({
1420
- [bobPubkey]: oldContentKey
1421
- })
1422
- })
1423
-
1424
- assert.deepEqual(
1425
- await unwrapEvent({
1426
- receiverSigner: bobProxy,
1427
- iykcSigner: bobLatestContent,
1428
- privateChannelSigner: alice,
1429
- event: wrapped,
1430
- receiverPubkey: bobPubkey
1431
- }),
1432
- unwrappedFixture(original, await alice.getPublicKey())
1433
- )
1434
- })
1435
-
1436
- test('wrapEvent accepts explicit receiver content keys with valid proofs', async () => {
1437
- const alice = signer()
1438
- const bob = signer()
1439
- const bobContent = signer()
1440
- const bobPubkey = await bob.getPublicKey()
1441
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1442
- const original = eventFixture('private')
1443
- NsecSigner.setContentSigners(bob, [bobContent])
1444
- const [wrapped] = await wrapEvent({
1445
- senderSigner: alice,
1446
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1447
- event: original,
1448
- _getIykcProofs: noContentKeys
1449
- })
1450
- const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
1451
- const line = routerRecipientRows(router)[0]
1452
-
1453
- assert.equal(line.length, 4)
1454
- assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, await alice.getPublicKey()))
1455
- })
1456
-
1457
- test('wrapEvent rejects explicit receiver content keys without valid proofs', async () => {
1458
- const alice = signer()
1459
- const bob = signer()
1460
- const bobContent = signer()
1461
- const bobPubkey = await bob.getPublicKey()
1462
- const bobContentPubkey = await bobContent.getPublicKey()
1463
-
1464
- await assert.rejects(
1465
- () => wrapEvent({
1466
- senderSigner: alice,
1467
- receivers: [[bobPubkey, bobContentPubkey]],
1468
- event: eventFixture('private'),
1469
- _getIykcProofs: noContentKeys
1470
- }),
1471
- /INVALID_IYKC_PROOF/
1472
- )
1473
- })
1474
-
1475
- test('unwrapEvent rejects receiver iykc rows without valid proofs', async () => {
1476
- const alice = signer()
1477
- const bob = signer()
1478
- const bobContent = signer()
1479
- const bobPubkey = await bob.getPublicKey()
1480
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1481
- const original = eventFixture('private')
1482
- const [wrapped] = await wrapEvent({
1483
- senderSigner: alice,
1484
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1485
- event: original,
1486
- _getIykcProofs: noContentKeys
1487
- })
1488
- const channelPubkey = await alice.getPublicKey()
1489
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1490
- const rows = routerJsonlRows(router)
1491
- const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
1492
- router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
1493
- const tampered = await alice.signEvent({
1494
- kind: PRIVATE_BROADCAST_KIND,
1495
- created_at: wrapped.created_at,
1496
- tags: wrapped.tags,
1497
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1498
- })
1499
-
1500
- await assert.rejects(
1501
- () => unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: tampered, receiverPubkey: bobPubkey }),
1502
- /INVALID_IYKC_PROOF/
1503
- )
1504
- })
1505
-
1506
- test('subscribe only emits receiver content key usage after iykc proof validation', async () => {
1507
- const originalSubscribeMany = pool.subscribeMany
1508
- let handlers = null
1509
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1510
- handlers = nextHandlers
1511
- return { close: () => {} }
1512
- }
1513
-
1514
- try {
1515
- const alice = signer()
1516
- const bob = signer()
1517
- const bobContent = signer()
1518
- const bobPubkey = await bob.getPublicKey()
1519
- const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
1520
- const [wrapped] = await wrapEvent({
1521
- senderSigner: alice,
1522
- receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
1523
- event: eventFixture('private'),
1524
- _getIykcProofs: noContentKeys
1525
- })
1526
- const channelPubkey = await alice.getPublicKey()
1527
- const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
1528
- const rows = routerJsonlRows(router)
1529
- const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
1530
- router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
1531
- const tampered = await alice.signEvent({
1532
- kind: PRIVATE_BROADCAST_KIND,
1533
- created_at: wrapped.created_at,
1534
- tags: wrapped.tags,
1535
- content: await encryptPrivateBroadcast(alice, channelPubkey, router)
1536
- })
1537
- const usages = []
1538
- const errors = []
1539
-
1540
- subscribe({
1541
- privateChannelSigner: alice,
1542
- receiverPubkey: bobPubkey,
1543
- relays: ['wss://relay.example'],
1544
- onContentKeyUsage: usage => usages.push(usage),
1545
- onError: err => errors.push(err)
1546
- })
1547
-
1548
- await handlers.onevent(tampered)
1549
-
1550
- assert.deepEqual(usages, [])
1551
- assert.equal(errors.length, 1)
1552
- assert.equal(errors[0].message, 'INVALID_IYKC_PROOF')
1553
- } finally {
1554
- pool.subscribeMany = originalSubscribeMany
1555
- }
1556
- })
1557
-
1558
- test('wrapEvent chunks large jsonl without oversize events and unwraps reassembled bytes', async () => {
1559
- const alice = signer()
1560
- const imkc = signer()
1561
- const bob = signer()
1562
- const bobPubkey = await bob.getPublicKey()
1563
- const imkcPubkey = await imkc.getPublicKey()
1564
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1565
- NsecSigner.setContentSigners(alice, [imkc])
1566
- const deletionPubkey = 'd'.repeat(64)
1567
- const wrapped = await wrapEvent({ senderSigner: alice, imkcSigner: imkc, receivers: [bobPubkey], deletionPubkey, event: original, _getIykcProofs: noContentKeys })
1568
-
1569
- assert.ok(wrapped.length > 1)
1570
- for (const event of wrapped) {
1571
- assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
1572
- assert.deepEqual(event.tags.find(tag => tag[0] === 's'), ['s', deletionPubkey])
1573
- }
1574
-
1575
- const routers = []
1576
- for (const event of wrapped) {
1577
- routers.push(await decryptPrivateBroadcast(alice, await alice.getPublicKey(), event.content))
1578
- }
1579
- assert.equal(routers[0].kind, ROUTER_KIND)
1580
- assert.equal(routers[0].tags.find(t => t[0] === 'r')?.[1], bobPubkey)
1581
- assert.equal(routers[0].tags.find(t => t[0] === 'imkc')?.[1], imkcPubkey)
1582
- assert.ok(routers[0].tags.find(t => t[0] === 'imkc')?.[2])
1583
- assert.equal(routers.length, Number(routers[0].tags.find(t => t[0] === 'c')[2]))
1584
- })
1585
-
1586
- test('wrapEvents cleans temporary chunks when the stream is stopped early', async () => {
1587
- const alice = signer()
1588
- const bob = signer()
1589
- const bobPubkey = await bob.getPublicKey()
1590
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1591
- const stream = wrapEvents({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1592
-
1593
- const first = await stream.next()
1594
- assert.equal(first.done, false)
1595
- assert.notEqual(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1596
-
1597
- await stream.return()
1598
-
1599
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1600
- })
1601
-
1602
- test('wrapEvents uses a caller-supplied temporary storage area', async () => {
1603
- const alice = signer()
1604
- const bob = signer()
1605
- const bobPubkey = await bob.getPublicKey()
1606
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1607
- const stream = wrapEvents({
1608
- senderSigner: alice,
1609
- receivers: [bobPubkey],
1610
- event: original,
1611
- temporaryStorageArea: globalThis.localStorage,
1612
- _getIykcProofs: noContentKeys
1613
- })
1614
-
1615
- await stream.next()
1616
-
1617
- assert.notEqual(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1618
- assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1619
-
1620
- await stream.return()
1621
-
1622
- assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
1623
- })
1624
-
1625
- test('subscribe buffers out-of-order chunks and unwraps when the missing chunk arrives', async () => {
1626
- const originalSubscribeMany = pool.subscribeMany
1627
- let handlers = null
1628
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1629
- handlers = nextHandlers
1630
- return { close: () => {} }
1631
- }
1632
-
1633
- try {
1634
- const alice = signer()
1635
- const bob = signer()
1636
- const bobPubkey = await bob.getPublicKey()
1637
- const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1638
- const wrapped = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
1639
- const events = []
1640
- const chunks = []
1641
-
1642
- assert.ok(wrapped.length > 1)
1643
- subscribe({
1644
- receiverSigner: bob,
1645
- privateChannelSigner: alice,
1646
- receiverPubkey: bobPubkey,
1647
- relays: ['wss://relay.example'],
1648
- onChunk: chunk => chunks.push(chunk),
1649
- onEvent: event => events.push(event),
1650
- _getIykcProofs: noContentKeys
1651
- })
1652
-
1653
- for (const event of wrapped.slice(1)) await handlers.onevent(event)
1654
-
1655
- assert.equal(events.length, 0)
1656
- assert.ok(chunks.at(-1).missing.includes(0))
1657
-
1658
- await handlers.onevent(wrapped[0])
1659
-
1660
- assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
1661
- } finally {
1662
- pool.subscribeMany = originalSubscribeMany
1663
- }
1664
- })
1665
-
1666
- test('subscribe drops invalid signed inner events and emits an error', async () => {
1667
- const originalSubscribeMany = pool.subscribeMany
1668
- let handlers = null
1669
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1670
- handlers = nextHandlers
1671
- return { close: () => {} }
1672
- }
1673
-
1674
- try {
1675
- const alice = signer()
1676
- const bob = signer()
1677
- const bobPubkey = await bob.getPublicKey()
1678
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
1679
- const [wrapped] = await wrapEvent({
1680
- senderSigner: alice,
1681
- receivers: [bobPubkey],
1682
- event: { ...signed, content: 'tampered' },
1683
- _getIykcProofs: noContentKeys
1684
- })
1685
- const events = []
1686
- const errors = []
1687
-
1688
- subscribe({
1689
- receiverSigner: bob,
1690
- privateChannelSigner: alice,
1691
- receiverPubkey: bobPubkey,
1692
- relays: ['wss://relay.example'],
1693
- onEvent: event => events.push(event),
1694
- onError: err => errors.push(err),
1695
- _getIykcProofs: noContentKeys
1696
- })
1697
-
1698
- await handlers.onevent(wrapped)
1699
- await handlers.onevent(wrapped)
1700
-
1701
- assert.equal(events.length, 0)
1702
- assert.equal(errors.length, 1)
1703
- assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
1704
- } finally {
1705
- pool.subscribeMany = originalSubscribeMany
1706
- }
1707
- })
1708
-
1709
- test('subscribe forgets ignored groups after their ttl', async () => {
1710
- const originalSubscribeMany = pool.subscribeMany
1711
- const originalNow = Date.now
1712
- let handlers = null
1713
- let now = 1000
1714
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1715
- handlers = nextHandlers
1716
- return { close: () => {} }
1717
- }
1718
- Date.now = () => now
1719
-
1720
- try {
1721
- const alice = signer()
1722
- const bob = signer()
1723
- const bobPubkey = await bob.getPublicKey()
1724
- const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
1725
- const [wrapped] = await wrapEvent({
1726
- senderSigner: alice,
1727
- receivers: [bobPubkey],
1728
- event: { ...signed, content: 'tampered' },
1729
- _getIykcProofs: noContentKeys
1730
- })
1731
- const errors = []
1732
-
1733
- subscribe({
1734
- receiverSigner: bob,
1735
- privateChannelSigner: alice,
1736
- receiverPubkey: bobPubkey,
1737
- relays: ['wss://relay.example'],
1738
- ignoredGroupTtlMs: 5,
1739
- onError: err => errors.push(err),
1740
- _getIykcProofs: noContentKeys
1741
- })
1742
-
1743
- await handlers.onevent(wrapped)
1744
- now += 4
1745
- await handlers.onevent(wrapped)
1746
- now += 2
1747
- await handlers.onevent(wrapped)
1748
-
1749
- assert.equal(errors.length, 2)
1750
- assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
1751
- assert.equal(errors[1].message, 'INVALID_SIGNED_INNER_EVENT')
1752
- } finally {
1753
- Date.now = originalNow
1754
- pool.subscribeMany = originalSubscribeMany
1755
- }
1756
- })
1757
-
1758
- test('subscribe evicts old ignored groups when the tombstone cache is full', async () => {
1759
- const originalSubscribeMany = pool.subscribeMany
1760
- let handlers = null
1761
- pool.subscribeMany = (_relays, _filter, nextHandlers) => {
1762
- handlers = nextHandlers
1763
- return { close: () => {} }
1764
- }
1765
-
1766
- try {
1767
- const alice = signer()
1768
- const bob = signer()
1769
- const bobPubkey = await bob.getPublicKey()
1770
- const signedA = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'a' }, generateSecretKey())
1771
- const signedB = finalizeEvent({ kind: 9002, created_at: 3, tags: [], content: 'b' }, generateSecretKey())
1772
- const [wrappedA] = await wrapEvent({
1773
- senderSigner: alice,
1774
- receivers: [bobPubkey],
1775
- event: { ...signedA, content: 'tampered-a' },
1776
- _getIykcProofs: noContentKeys
1777
- })
1778
- const [wrappedB] = await wrapEvent({
1779
- senderSigner: alice,
1780
- receivers: [bobPubkey],
1781
- event: { ...signedB, content: 'tampered-b' },
1782
- _getIykcProofs: noContentKeys
1783
- })
1784
- const errors = []
1785
-
1786
- subscribe({
1787
- receiverSigner: bob,
1788
- privateChannelSigner: alice,
1789
- receiverPubkey: bobPubkey,
1790
- relays: ['wss://relay.example'],
1791
- ignoredGroupMaxEntries: 1,
1792
- onError: err => errors.push(err),
1793
- _getIykcProofs: noContentKeys
1794
- })
1795
-
1796
- await handlers.onevent(wrappedA)
1797
- await handlers.onevent(wrappedA)
1798
- await handlers.onevent(wrappedB)
1799
- await handlers.onevent(wrappedA)
1800
-
1801
- assert.equal(errors.length, 3)
1802
- assert.ok(errors.every(err => err.message === 'INVALID_SIGNED_INNER_EVENT'))
1803
- } finally {
1804
- pool.subscribeMany = originalSubscribeMany
1805
- }
1806
- })
1807
-
1808
- test('received chunk store purges stale incomplete groups', () => {
1809
- const originalNow = Date.now
1810
- const storage = (() => {
1811
- const data = new Map()
1812
- return {
1813
- clear: () => data.clear(),
1814
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
1815
- removeItem: key => { data.delete(String(key)) },
1816
- setItem: (key, value) => { data.set(String(key), String(value)) },
1817
- get length () { return data.size }
1818
- }
1819
- })()
1820
- let now = 1000
1821
- Date.now = () => now
1822
-
1823
- try {
1824
- const store = createReceivedChunkStore({
1825
- prefix: 'test:received-chunks',
1826
- storageArea: storage,
1827
- ttlMs: 5
1828
- })
1829
- store.put({
1830
- channelPubkey: 'channel',
1831
- routerPubkey: 'router',
1832
- index: 1,
1833
- total: 2,
1834
- content: 'abc'
1835
- })
1836
-
1837
- assert.ok(storage.length > 0)
1838
-
1839
- now += 6
1840
- store.cleanupStale()
1841
-
1842
- assert.equal(storage.length, 0)
1843
- } finally {
1844
- Date.now = originalNow
1845
- }
1846
- })
1847
-
1848
- test('received chunk store resumes incremental parsing after reload', async () => {
1849
- const storage = (() => {
1850
- const data = new Map()
1851
- return {
1852
- clear: () => data.clear(),
1853
- getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
1854
- removeItem: key => { data.delete(String(key)) },
1855
- setItem: (key, value) => { data.set(String(key), String(value)) },
1856
- get length () { return data.size }
1857
- }
1858
- })()
1859
- const line = `${JSON.stringify(['alice', 'ciphertext'])}\n`
1860
- const first = line.slice(0, 12)
1861
- const second = line.slice(12)
1862
- const firstStore = createReceivedChunkStore({
1863
- prefix: 'test:received-chunks:reload',
1864
- storageArea: storage
1865
- })
1866
- const lines = []
1867
-
1868
- firstStore.put({
1869
- channelPubkey: 'channel',
1870
- routerPubkey: 'router',
1871
- index: 0,
1872
- total: 2,
1873
- content: Buffer.from(first).toString('base64')
1874
- })
1875
-
1876
- const firstDrain = await firstStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
1877
-
1878
- assert.equal(firstDrain.complete, false)
1879
- assert.equal(firstDrain.meta.nextIndex, 1)
1880
- assert.equal(firstDrain.meta.carry, first)
1881
- assert.deepEqual(lines, [])
1882
-
1883
- const secondStore = createReceivedChunkStore({
1884
- prefix: 'test:received-chunks:reload',
1885
- storageArea: storage
1886
- })
1887
- secondStore.put({
1888
- channelPubkey: 'channel',
1889
- routerPubkey: 'router',
1890
- index: 1,
1891
- total: 2,
1892
- content: Buffer.from(second).toString('base64')
1893
- })
1894
-
1895
- const secondDrain = await secondStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
1896
-
1897
- assert.equal(secondDrain.complete, true)
1898
- assert.deepEqual(lines, [line.trim()])
1899
- })