libp2r2p 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base93/index.js +193 -0
- package/index.js +1 -0
- package/nip19/index.js +305 -0
- package/package.json +26 -1
- package/AGENTS.md +0 -20
- package/tests/base16.test.js +0 -19
- package/tests/base64.test.js +0 -18
- package/tests/content-key-event.test.js +0 -48
- package/tests/fixtures/nip44v3-vectors.json +0 -1
- package/tests/helpers/test-signer.js +0 -185
- package/tests/idb-queue.test.js +0 -153
- package/tests/idb.test.js +0 -91
- package/tests/nip44-v3.test.js +0 -73
- package/tests/nip46.test.js +0 -668
- package/tests/private-channel.test.js +0 -1912
- package/tests/private-message.test.js +0 -501
- package/tests/private-messenger.test.js +0 -1737
- package/tests/queries.test.js +0 -101
- package/tests/queue-parity.test.js +0 -105
- package/tests/relay-hll.test.js +0 -32
- package/tests/relay-pool.test.js +0 -2063
- package/tests/temporary-storage.test.js +0 -89
- package/tests/web-storage-queue.test.js +0 -480
|
@@ -1,1737 +0,0 @@
|
|
|
1
|
-
import { afterEach, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
|
|
4
|
-
import { ASK_KIND, REPLY_KIND, TELL_KIND } from '../private-message/index.js'
|
|
5
|
-
import {
|
|
6
|
-
createEventReplyPacker,
|
|
7
|
-
createMissingMessageReplyPacker,
|
|
8
|
-
MISSING_MESSAGES_ASK_CODE,
|
|
9
|
-
MISSING_MESSAGES_REPLY_CODE,
|
|
10
|
-
NYM_CARRIER_SEED_RECORD_TYPE,
|
|
11
|
-
PrivateMessenger,
|
|
12
|
-
ROUTER_SEED_RECORD_TYPE,
|
|
13
|
-
SEEDER_PRESENCE_CODE
|
|
14
|
-
} from '../private-messenger/index.js'
|
|
15
|
-
import { TEMPORARY_STORAGE_KEYS_KEY } from '../temporary-storage/index.js'
|
|
16
|
-
|
|
17
|
-
const data = new Map()
|
|
18
|
-
const sessionData = new Map()
|
|
19
|
-
globalThis.localStorage = {
|
|
20
|
-
clear: () => data.clear(),
|
|
21
|
-
getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
|
|
22
|
-
removeItem: key => { data.delete(String(key)) },
|
|
23
|
-
setItem: (key, value) => { data.set(String(key), String(value)) }
|
|
24
|
-
}
|
|
25
|
-
globalThis.sessionStorage = {
|
|
26
|
-
clear: () => sessionData.clear(),
|
|
27
|
-
getItem: key => sessionData.has(String(key)) ? sessionData.get(String(key)) : null,
|
|
28
|
-
removeItem: key => { sessionData.delete(String(key)) },
|
|
29
|
-
setItem: (key, value) => { sessionData.set(String(key), String(value)) }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function resetIndexedDb () {
|
|
33
|
-
globalThis.indexedDB = new IDBFactory()
|
|
34
|
-
globalThis.IDBKeyRange = IDBKeyRange
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
resetIndexedDb()
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
globalThis.localStorage.clear()
|
|
41
|
-
globalThis.sessionStorage.clear()
|
|
42
|
-
resetIndexedDb()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
function signer (pubkey) {
|
|
46
|
-
return {
|
|
47
|
-
getPublicKey: () => pubkey,
|
|
48
|
-
withSharedKey: () => ({})
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function jsonlContent (...rows) {
|
|
53
|
-
return Buffer.from(`${rows.join('\n')}\n`).toString('base64')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function payloadRow (value = 'payload-ciphertext') {
|
|
57
|
-
return JSON.stringify([value])
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function fakePrivateMessage () {
|
|
61
|
-
const watchCalls = []
|
|
62
|
-
const stopped = []
|
|
63
|
-
const sent = []
|
|
64
|
-
const cleared = []
|
|
65
|
-
return {
|
|
66
|
-
watchCalls,
|
|
67
|
-
stopped,
|
|
68
|
-
sent,
|
|
69
|
-
cleared,
|
|
70
|
-
ASK_KIND,
|
|
71
|
-
REPLY_KIND,
|
|
72
|
-
TELL_KIND,
|
|
73
|
-
watch: async options => {
|
|
74
|
-
watchCalls.push(options)
|
|
75
|
-
return () => stopped.push(options.channels[0])
|
|
76
|
-
},
|
|
77
|
-
ask: async options => {
|
|
78
|
-
sent.push({ method: 'ask', options })
|
|
79
|
-
return { question: { id: 'question-id', kind: ASK_KIND, pubkey: 'user' }, delivery: { reports: [] } }
|
|
80
|
-
},
|
|
81
|
-
reply: async options => {
|
|
82
|
-
sent.push({ method: 'reply', options })
|
|
83
|
-
return { reply: { id: 'reply-id', kind: REPLY_KIND }, delivery: { reports: [] } }
|
|
84
|
-
},
|
|
85
|
-
tell: async options => {
|
|
86
|
-
sent.push({ method: 'tell', options })
|
|
87
|
-
return { tell: { id: 'tell-id', kind: TELL_KIND }, delivery: { reports: [] } }
|
|
88
|
-
},
|
|
89
|
-
yell: async options => {
|
|
90
|
-
sent.push({ method: 'yell', options })
|
|
91
|
-
return { yell: { id: 'yell-id', kind: TELL_KIND }, delivery: { reports: [] } }
|
|
92
|
-
},
|
|
93
|
-
broadcastRumor: async options => {
|
|
94
|
-
sent.push({ method: 'broadcastRumor', options })
|
|
95
|
-
return { rumor: { id: 'raw-id', kind: 9001 }, delivery: { reports: [] } }
|
|
96
|
-
},
|
|
97
|
-
broadcastEvent: async options => {
|
|
98
|
-
sent.push({ method: 'broadcastEvent', options })
|
|
99
|
-
return { event: options.event, delivery: { reports: [] } }
|
|
100
|
-
},
|
|
101
|
-
broadcastNymRumor: async options => {
|
|
102
|
-
sent.push({ method: 'broadcastNymRumor', options })
|
|
103
|
-
return { rumor: { id: 'nym-raw-id', kind: 9003, pubkey: options.nymSigner.getPublicKey() }, delivery: { reports: [] } }
|
|
104
|
-
},
|
|
105
|
-
broadcastNymEvent: async options => {
|
|
106
|
-
sent.push({ method: 'broadcastNymEvent', options })
|
|
107
|
-
return { event: options.event, delivery: { reports: [] } }
|
|
108
|
-
},
|
|
109
|
-
unwatch: channels => stopped.push(channels),
|
|
110
|
-
clearChannelState: channel => cleared.push(channel)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
test('private messenger cleanup uses session storage by default and configured storage on init', async () => {
|
|
115
|
-
globalThis.sessionStorage.setItem('tmp.session', 'encrypted')
|
|
116
|
-
globalThis.sessionStorage.setItem(TEMPORARY_STORAGE_KEYS_KEY, JSON.stringify(['tmp.session']))
|
|
117
|
-
globalThis.localStorage.setItem('permanent', 'keep')
|
|
118
|
-
|
|
119
|
-
PrivateMessenger.cleanupTemporaryStorage()
|
|
120
|
-
|
|
121
|
-
assert.equal(globalThis.sessionStorage.getItem('tmp.session'), null)
|
|
122
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
123
|
-
assert.equal(globalThis.localStorage.getItem('permanent'), 'keep')
|
|
124
|
-
|
|
125
|
-
globalThis.localStorage.setItem('tmp.local', 'encrypted')
|
|
126
|
-
globalThis.localStorage.setItem(TEMPORARY_STORAGE_KEYS_KEY, JSON.stringify(['tmp.local']))
|
|
127
|
-
PrivateMessenger.cleanupTemporaryStorage({ storageArea: globalThis.localStorage })
|
|
128
|
-
|
|
129
|
-
assert.equal(globalThis.localStorage.getItem('tmp.local'), null)
|
|
130
|
-
assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
131
|
-
|
|
132
|
-
globalThis.localStorage.setItem('tmp.local', 'encrypted')
|
|
133
|
-
globalThis.localStorage.setItem(TEMPORARY_STORAGE_KEYS_KEY, JSON.stringify(['tmp.local']))
|
|
134
|
-
await new PrivateMessenger({
|
|
135
|
-
_privateMessage: fakePrivateMessage(),
|
|
136
|
-
temporaryStorageArea: globalThis.localStorage
|
|
137
|
-
}).init({
|
|
138
|
-
userSigner: signer('user'),
|
|
139
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
assert.equal(globalThis.localStorage.getItem('tmp.local'), null)
|
|
143
|
-
assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
test('private messenger defaults to larger bounded IndexedDB queues', () => {
|
|
147
|
-
const messenger = new PrivateMessenger({ _privateMessage: fakePrivateMessage() })
|
|
148
|
-
|
|
149
|
-
assert.equal(messenger.messageQueueMaxBytes, 16 * 1024 * 1024)
|
|
150
|
-
assert.equal(messenger.seedQueueMaxBytes, 64 * 1024 * 1024)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test('private messenger persists queued messages in IndexedDB across instances', async () => {
|
|
154
|
-
const indexedDB = new IDBFactory()
|
|
155
|
-
const firstPrivateMessage = fakePrivateMessage()
|
|
156
|
-
const first = await new PrivateMessenger({
|
|
157
|
-
_privateMessage: firstPrivateMessage,
|
|
158
|
-
_indexedDB: indexedDB
|
|
159
|
-
}).init({
|
|
160
|
-
userSigner: signer('durable-user'),
|
|
161
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
await firstPrivateMessage.watchCalls[0].onTell({
|
|
165
|
-
event: { id: 'durable-id', kind: TELL_KIND, pubkey: 'alice', created_at: 10, tags: [['r', 'durable-user']], content: 'hi' },
|
|
166
|
-
outer: { id: 'durable-outer', created_at: 10 },
|
|
167
|
-
meta: { channelPubkey: 'channel' },
|
|
168
|
-
payload: { payload: 'hi' }
|
|
169
|
-
})
|
|
170
|
-
first.close()
|
|
171
|
-
|
|
172
|
-
const second = await new PrivateMessenger({
|
|
173
|
-
_privateMessage: fakePrivateMessage(),
|
|
174
|
-
_indexedDB: indexedDB
|
|
175
|
-
}).init({
|
|
176
|
-
userSigner: signer('durable-user'),
|
|
177
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
assert.equal((await second.nextMessage()).event.id, 'durable-id')
|
|
181
|
-
assert.equal(await second.nextMessage(), null)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('private messenger leaves legacy localStorage queue records untouched', async () => {
|
|
185
|
-
const prefix = 'libp2r2p:private-messenger:legacy-user'
|
|
186
|
-
const stateKey = `${prefix}:queue`
|
|
187
|
-
const itemKey = `${prefix}:queue:item:0`
|
|
188
|
-
const seedStateKey = `${prefix}:seeds:queue`
|
|
189
|
-
const seedItemKey = `${prefix}:seeds:queue:item:0`
|
|
190
|
-
const oldState = JSON.stringify({ head: 0, tail: 1, usedBytes: 42 })
|
|
191
|
-
const oldItem = JSON.stringify({ id: 0, value: 'manual-cleanup' })
|
|
192
|
-
globalThis.localStorage.setItem(stateKey, oldState)
|
|
193
|
-
globalThis.localStorage.setItem(itemKey, oldItem)
|
|
194
|
-
globalThis.localStorage.setItem(seedStateKey, oldState)
|
|
195
|
-
globalThis.localStorage.setItem(seedItemKey, oldItem)
|
|
196
|
-
|
|
197
|
-
await new PrivateMessenger({ _privateMessage: fakePrivateMessage() }).init({
|
|
198
|
-
userSigner: signer('legacy-user'),
|
|
199
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
assert.equal(globalThis.localStorage.getItem(stateKey), oldState)
|
|
203
|
-
assert.equal(globalThis.localStorage.getItem(itemKey), oldItem)
|
|
204
|
-
assert.equal(globalThis.localStorage.getItem(seedStateKey), oldState)
|
|
205
|
-
assert.equal(globalThis.localStorage.getItem(seedItemKey), oldItem)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('private messenger rejects initialization when IndexedDB is unavailable', async () => {
|
|
209
|
-
await assert.rejects(
|
|
210
|
-
new PrivateMessenger({ _privateMessage: fakePrivateMessage(), _indexedDB: null }).init({
|
|
211
|
-
userSigner: signer('no-idb-user'),
|
|
212
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
213
|
-
}),
|
|
214
|
-
/IDB_UNAVAILABLE/
|
|
215
|
-
)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
function fakeRelayListUpdates () {
|
|
219
|
-
const subscriptions = []
|
|
220
|
-
return {
|
|
221
|
-
subscriptions,
|
|
222
|
-
subscribe: (pubkeys, options) => {
|
|
223
|
-
const subscription = {
|
|
224
|
-
pubkeys,
|
|
225
|
-
options,
|
|
226
|
-
closed: false,
|
|
227
|
-
emit: update => Promise.resolve(options.onChange?.(update))
|
|
228
|
-
}
|
|
229
|
-
subscriptions.push(subscription)
|
|
230
|
-
return () => { subscription.closed = true }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
test('private messenger watches channels and queues received leecher rumors', async () => {
|
|
236
|
-
const pm = fakePrivateMessage()
|
|
237
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
238
|
-
userSigner: signer('user'),
|
|
239
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
assert.equal(pm.watchCalls.length, 1)
|
|
243
|
-
assert.deepEqual(pm.watchCalls[0].channels, ['channel'])
|
|
244
|
-
assert.deepEqual(pm.watchCalls[0].relays, ['wss://relay.example'])
|
|
245
|
-
assert.equal(pm.watchCalls[0].mode, 'leecher')
|
|
246
|
-
assert.equal(pm.watchCalls[0].receivedChunkTtlMs, 7 * 24 * 60 * 60 * 1000)
|
|
247
|
-
|
|
248
|
-
pm.watchCalls[0].onTell({
|
|
249
|
-
event: { id: 'tell-id', kind: TELL_KIND, pubkey: 'alice', created_at: 10, tags: [['r', 'user']], content: 'hi' },
|
|
250
|
-
outer: { id: 'outer-id', created_at: 11 },
|
|
251
|
-
meta: { channelPubkey: 'channel' },
|
|
252
|
-
payload: { payload: 'hi' },
|
|
253
|
-
tell: { id: 'tell-id' }
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
const item = (await messenger.nextMessage())
|
|
257
|
-
assert.equal(item.type, 'tell')
|
|
258
|
-
assert.equal(item.channelPubkey, 'channel')
|
|
259
|
-
assert.equal(item.event.id, 'tell-id')
|
|
260
|
-
assert.deepEqual(item.payload, { payload: 'hi' })
|
|
261
|
-
assert.equal(messenger.readState().channels.channel.lastSeenAt, 11)
|
|
262
|
-
|
|
263
|
-
pm.watchCalls[0].onReply({
|
|
264
|
-
event: { id: 'reply-id', kind: REPLY_KIND, pubkey: 'alice', created_at: 12, tags: [['q', 'question-id']], content: 'pong' },
|
|
265
|
-
outer: { id: 'outer-reply-id', created_at: 13 },
|
|
266
|
-
meta: { channelPubkey: 'channel' },
|
|
267
|
-
payload: { payload: 'pong' },
|
|
268
|
-
questionId: 'question-id',
|
|
269
|
-
reply: { id: 'reply-id' }
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
const reply = (await messenger.nextMessage())
|
|
273
|
-
assert.equal(reply.type, 'reply')
|
|
274
|
-
assert.equal(reply.question, null)
|
|
275
|
-
assert.equal(reply.questionId, 'question-id')
|
|
276
|
-
assert.equal(reply.event.id, 'reply-id')
|
|
277
|
-
|
|
278
|
-
pm.watchCalls[0].onMessage({
|
|
279
|
-
event: { id: 'raw-id', kind: 9001, pubkey: 'alice', created_at: 14, tags: [], content: JSON.stringify(['raw-payload', 'not-a-private-message-code']) },
|
|
280
|
-
outer: { id: 'outer-raw-id', created_at: 15 },
|
|
281
|
-
meta: { channelPubkey: 'channel' },
|
|
282
|
-
payload: ['raw-payload', 'not-a-private-message-code']
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
const raw = (await messenger.nextMessage())
|
|
286
|
-
assert.equal(raw.type, 'message')
|
|
287
|
-
assert.equal(raw.event.id, 'raw-id')
|
|
288
|
-
assert.deepEqual(raw.payload, ['raw-payload', 'not-a-private-message-code'])
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
test('private messenger pauses live watches offline, restarts them before durable recovery, and keeps presence running', async () => {
|
|
292
|
-
const originalWindow = globalThis.window
|
|
293
|
-
const originalDateNow = Date.now
|
|
294
|
-
const events = new EventTarget()
|
|
295
|
-
const pm = fakePrivateMessage()
|
|
296
|
-
const order = []
|
|
297
|
-
const clearedIntervals = []
|
|
298
|
-
const originalWatch = pm.watch
|
|
299
|
-
let now = 1_000_000
|
|
300
|
-
|
|
301
|
-
globalThis.window = {
|
|
302
|
-
addEventListener: (...args) => events.addEventListener(...args),
|
|
303
|
-
removeEventListener: (...args) => events.removeEventListener(...args)
|
|
304
|
-
}
|
|
305
|
-
Date.now = () => now
|
|
306
|
-
pm.watch = async options => {
|
|
307
|
-
order.push(`watch:${options.channels[0]}`)
|
|
308
|
-
return originalWatch(options)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let messenger
|
|
312
|
-
try {
|
|
313
|
-
messenger = await new PrivateMessenger({
|
|
314
|
-
_privateMessage: pm,
|
|
315
|
-
_privateChannel: {
|
|
316
|
-
fetch: async () => {
|
|
317
|
-
order.push('recover')
|
|
318
|
-
return []
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
_setInterval: () => 'presence-timer',
|
|
322
|
-
_clearInterval: timer => clearedIntervals.push(timer)
|
|
323
|
-
}).init({
|
|
324
|
-
userSigner: signer('user'),
|
|
325
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
326
|
-
})
|
|
327
|
-
order.length = 0
|
|
328
|
-
|
|
329
|
-
events.dispatchEvent(new Event('offline'))
|
|
330
|
-
assert.deepEqual(pm.stopped, ['channel'])
|
|
331
|
-
assert.equal(messenger.stopByChannel.size, 0)
|
|
332
|
-
assert.equal(messenger.presenceTimers.get('channel'), 'presence-timer')
|
|
333
|
-
assert.deepEqual(clearedIntervals, [])
|
|
334
|
-
assert.ok(messenger.readState().channels.channel.openOfflineStart)
|
|
335
|
-
|
|
336
|
-
now += 1_000
|
|
337
|
-
events.dispatchEvent(new Event('online'))
|
|
338
|
-
for (let attempt = 0; attempt < 20 && !order.includes('recover'); attempt++) {
|
|
339
|
-
await new Promise(resolve => setImmediate(resolve))
|
|
340
|
-
}
|
|
341
|
-
assert.deepEqual(order, ['watch:channel', 'recover'])
|
|
342
|
-
|
|
343
|
-
const message = {
|
|
344
|
-
event: { id: 'offline-duplicate', kind: TELL_KIND, pubkey: 'alice', created_at: 1001, tags: [['r', 'user']], content: 'hi' },
|
|
345
|
-
outer: { id: 'offline-outer', created_at: 1001 },
|
|
346
|
-
meta: { channelPubkey: 'channel' },
|
|
347
|
-
payload: { payload: 'hi' },
|
|
348
|
-
tell: { id: 'offline-duplicate' }
|
|
349
|
-
}
|
|
350
|
-
await pm.watchCalls[0].onTell(message)
|
|
351
|
-
await pm.watchCalls[1].onTell(message)
|
|
352
|
-
assert.equal((await messenger.nextMessage()).event.id, 'offline-duplicate')
|
|
353
|
-
assert.equal(await messenger.nextMessage(), null)
|
|
354
|
-
} finally {
|
|
355
|
-
messenger?.close()
|
|
356
|
-
Date.now = originalDateNow
|
|
357
|
-
if (originalWindow === undefined) delete globalThis.window
|
|
358
|
-
else globalThis.window = originalWindow
|
|
359
|
-
}
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
test('private messenger forwards watch errors to the configured error handler', async () => {
|
|
363
|
-
const pm = fakePrivateMessage()
|
|
364
|
-
const errors = []
|
|
365
|
-
await new PrivateMessenger({ _privateMessage: pm, onError: err => errors.push(err) }).init({
|
|
366
|
-
userSigner: signer('user'),
|
|
367
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
pm.watchCalls[0].onError(new Error('RECEIVER_DOUBLE_DH_UNSUPPORTED'))
|
|
371
|
-
|
|
372
|
-
assert.equal(errors.length, 1)
|
|
373
|
-
assert.equal(errors[0].message, 'RECEIVER_DOUBLE_DH_UNSUPPORTED')
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
test('private messenger queues nym messages without dispatching helper kinds', async () => {
|
|
377
|
-
const pm = fakePrivateMessage()
|
|
378
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
379
|
-
userSigner: signer('user'),
|
|
380
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
pm.watchCalls[0].onNym({
|
|
384
|
-
event: { id: 'nym-ask-id', kind: ASK_KIND, pubkey: 'nym', created_at: 10, tags: [['r', 'user']], content: 'hi' },
|
|
385
|
-
outer: { id: 'outer-id', created_at: 11 },
|
|
386
|
-
meta: { channelPubkey: 'channel' },
|
|
387
|
-
payload: { payload: 'hi' },
|
|
388
|
-
nym: { id: 'nym-ask-id' }
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
const item = (await messenger.nextMessage())
|
|
392
|
-
assert.equal(item.type, 'nym')
|
|
393
|
-
assert.equal(item.event.kind, ASK_KIND)
|
|
394
|
-
assert.equal(item.event.pubkey, 'nym')
|
|
395
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test('private messenger skips duplicate pending app messages by channel type and event id', async () => {
|
|
399
|
-
const pm = fakePrivateMessage()
|
|
400
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
401
|
-
userSigner: signer('user'),
|
|
402
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
403
|
-
})
|
|
404
|
-
const message = {
|
|
405
|
-
event: { id: 'tell-id', kind: TELL_KIND, pubkey: 'alice', created_at: 10, tags: [['r', 'user']], content: 'hi' },
|
|
406
|
-
outer: { id: 'outer-id', created_at: 11 },
|
|
407
|
-
meta: { channelPubkey: 'channel' },
|
|
408
|
-
payload: { payload: 'hi' },
|
|
409
|
-
tell: { id: 'tell-id' }
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
pm.watchCalls[0].onTell(message)
|
|
413
|
-
pm.watchCalls[0].onTell({ ...message, outer: { id: 'outer-duplicate-id', created_at: 12 } })
|
|
414
|
-
|
|
415
|
-
const queued = await messenger.nextMessage()
|
|
416
|
-
assert.equal(queued.event.id, 'tell-id')
|
|
417
|
-
assert.equal(Object.hasOwn(queued, '__p2r2pMessageDedupeKey'), false)
|
|
418
|
-
assert.equal(Object.hasOwn(queued, 'id'), false)
|
|
419
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
420
|
-
assert.equal(messenger.readState().channels.channel.lastSeenAt, 12)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
test('private messenger reports content key usage changes for sent and received messages', async () => {
|
|
424
|
-
const pm = fakePrivateMessage()
|
|
425
|
-
const changes = []
|
|
426
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm, onContentKeyChange: event => changes.push(event) }).init({
|
|
427
|
-
userSigner: signer('user'),
|
|
428
|
-
contentKeySigner: signer('content'),
|
|
429
|
-
channels: [{ signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
430
|
-
})
|
|
431
|
-
const base = {
|
|
432
|
-
channelPubkey: 'channel',
|
|
433
|
-
outer: { id: 'outer-id', created_at: 20 },
|
|
434
|
-
router: { pubkey: 'router-id', created_at: 19 },
|
|
435
|
-
senderPubkey: 'user',
|
|
436
|
-
receiverPubkeys: ['alice']
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
pm.watchCalls[0].onContentKeyUsage({
|
|
440
|
-
...base,
|
|
441
|
-
direction: 'sent',
|
|
442
|
-
keyRole: 'sender',
|
|
443
|
-
receiverPubkey: 'alice',
|
|
444
|
-
contentKeyPubkey: '',
|
|
445
|
-
isBroadcast: false
|
|
446
|
-
})
|
|
447
|
-
pm.watchCalls[0].onContentKeyUsage({
|
|
448
|
-
...base,
|
|
449
|
-
direction: 'sent',
|
|
450
|
-
keyRole: 'sender',
|
|
451
|
-
receiverPubkey: 'alice',
|
|
452
|
-
contentKeyPubkey: '',
|
|
453
|
-
isBroadcast: false
|
|
454
|
-
})
|
|
455
|
-
pm.watchCalls[0].onContentKeyUsage({
|
|
456
|
-
...base,
|
|
457
|
-
direction: 'sent',
|
|
458
|
-
keyRole: 'sender',
|
|
459
|
-
receiverPubkey: '',
|
|
460
|
-
receiverPubkeys: ['alice', 'bob'],
|
|
461
|
-
contentKeyPubkey: 'unknown-content',
|
|
462
|
-
isBroadcast: true
|
|
463
|
-
})
|
|
464
|
-
pm.watchCalls[0].onContentKeyUsage({
|
|
465
|
-
...base,
|
|
466
|
-
direction: 'received',
|
|
467
|
-
keyRole: 'receiver',
|
|
468
|
-
senderPubkey: 'alice',
|
|
469
|
-
receiverPubkey: 'user',
|
|
470
|
-
contentKeyPubkey: 'content',
|
|
471
|
-
isBroadcast: false
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
assert.equal(changes.length, 3)
|
|
475
|
-
assert.equal(changes[0].direction, 'sent')
|
|
476
|
-
assert.equal(changes[0].contentKeyStatus, 'none')
|
|
477
|
-
assert.equal(changes[0].counterpartyPubkey, 'alice')
|
|
478
|
-
assert.equal(changes[1].direction, 'sent')
|
|
479
|
-
assert.equal(changes[1].contentKeyStatus, 'unknown')
|
|
480
|
-
assert.equal(changes[1].previousContentKeyPubkey, '')
|
|
481
|
-
assert.equal(changes[1].isBroadcast, true)
|
|
482
|
-
assert.deepEqual(changes[1].receiverPubkeys, ['alice', 'bob'])
|
|
483
|
-
assert.equal(changes[2].direction, 'received')
|
|
484
|
-
assert.equal(changes[2].contentKeyStatus, 'known')
|
|
485
|
-
assert.equal(changes[2].counterpartyPubkey, 'alice')
|
|
486
|
-
assert.equal(messenger.readState().channels.channel.contentKeyUsage.sent.contentKeyPubkey, 'unknown-content')
|
|
487
|
-
assert.equal(messenger.readState().channels.channel.contentKeyUsage.received.contentKeyPubkey, 'content')
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
test('private messenger delegates send helpers with scoped signers and relays', async () => {
|
|
491
|
-
const pm = fakePrivateMessage()
|
|
492
|
-
const messenger = await new PrivateMessenger({
|
|
493
|
-
_privateMessage: pm,
|
|
494
|
-
temporaryStorageArea: globalThis.localStorage
|
|
495
|
-
}).init({
|
|
496
|
-
userSigner: signer('user'),
|
|
497
|
-
contentKeySigner: signer('content'),
|
|
498
|
-
nymSigner: signer('global-nym'),
|
|
499
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
await messenger.ask({ receiverPubkey: 'alice', payload: 'ping' })
|
|
503
|
-
await messenger.reply({ question: { id: 'q', pubkey: 'alice' }, payload: 'pong' })
|
|
504
|
-
await messenger.tell({ receiverPubkey: 'alice', payload: 'note' })
|
|
505
|
-
await messenger.yell({ receiverPubkeys: ['alice', 'bob'], payload: 'news' })
|
|
506
|
-
await messenger.broadcastRumor({ receiverPubkeys: ['alice', 'bob'], rumor: { kind: 9001, created_at: 1, tags: [], content: 'raw' } })
|
|
507
|
-
await messenger.broadcastEvent({ receiverPubkeys: ['alice', 'bob'], event: { id: 'signed-id', kind: 9002, pubkey: 'author', created_at: 2, tags: [], content: 'signed', sig: 'sig' } })
|
|
508
|
-
await messenger.broadcastNymRumor({ rumor: { kind: 9003, created_at: 3, tags: [], content: 'nym raw' } })
|
|
509
|
-
await messenger.broadcastNymEvent({ event: { id: 'nym-signed-id', kind: 9004, pubkey: 'author', created_at: 4, tags: [], content: 'nym signed', sig: 'sig' } })
|
|
510
|
-
|
|
511
|
-
assert.deepEqual(pm.sent.map(s => s.method), ['ask', 'reply', 'tell', 'yell', 'broadcastRumor', 'broadcastEvent', 'broadcastNymRumor', 'broadcastNymEvent'])
|
|
512
|
-
for (const sent of pm.sent.slice(0, 6)) {
|
|
513
|
-
assert.equal(sent.options.senderSigner.getPublicKey(), 'user')
|
|
514
|
-
assert.equal(sent.options.imkcSigner.getPublicKey(), 'content')
|
|
515
|
-
assert.equal(sent.options.privateChannelSigner.getPublicKey(), 'channel')
|
|
516
|
-
assert.deepEqual(sent.options.relays, ['wss://relay.example'])
|
|
517
|
-
assert.equal(sent.options.expirationSeconds, 7 * 24 * 60 * 60)
|
|
518
|
-
assert.equal(sent.options.temporaryStorageArea, globalThis.localStorage)
|
|
519
|
-
assert.equal(sent.options.autoDeletionCapability, true)
|
|
520
|
-
}
|
|
521
|
-
assert.equal(pm.sent[5].options.event.id, 'signed-id')
|
|
522
|
-
assert.equal(pm.sent[6].options.nymSigner.getPublicKey(), 'global-nym')
|
|
523
|
-
assert.equal(pm.sent[6].options.privateChannelSigner.getPublicKey(), 'channel')
|
|
524
|
-
assert.deepEqual(pm.sent[6].options.relays, ['wss://relay.example'])
|
|
525
|
-
assert.equal(pm.sent[6].options.expirationSeconds, 7 * 24 * 60 * 60)
|
|
526
|
-
assert.equal(pm.sent[6].options.autoDeletionCapability, true)
|
|
527
|
-
assert.equal(pm.sent[7].options.nymSigner.getPublicKey(), 'global-nym')
|
|
528
|
-
assert.equal(pm.sent[7].options.event.pubkey, 'author')
|
|
529
|
-
assert.equal(pm.sent[7].options.autoDeletionCapability, true)
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
test('private messenger configures automatic deletion capabilities and forwards caller pubkeys', async () => {
|
|
533
|
-
const pm = fakePrivateMessage()
|
|
534
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm, autoDeletionCapability: true }).init({
|
|
535
|
-
userSigner: signer('user'),
|
|
536
|
-
channels: [
|
|
537
|
-
{ pubkey: 'inherited', signer: signer('inherited'), relays: ['wss://relay.example'] },
|
|
538
|
-
{ pubkey: 'disabled', signer: signer('disabled'), relays: ['wss://relay.example'], autoDeletionCapability: false }
|
|
539
|
-
]
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
await messenger.tell({ channelPubkey: 'inherited', receiverPubkey: 'alice', payload: 'default' })
|
|
543
|
-
await messenger.tell({ channelPubkey: 'disabled', receiverPubkey: 'alice', payload: 'disabled' })
|
|
544
|
-
await messenger.tell({ channelPubkey: 'disabled', receiverPubkey: 'alice', payload: 'caller-managed', deletionPubkey: 'a'.repeat(64) })
|
|
545
|
-
|
|
546
|
-
assert.equal(pm.sent[0].options.autoDeletionCapability, true)
|
|
547
|
-
assert.equal(pm.sent[1].options.autoDeletionCapability, false)
|
|
548
|
-
assert.equal(pm.sent[2].options.autoDeletionCapability, false)
|
|
549
|
-
assert.equal(pm.sent[2].options.deletionPubkey, 'a'.repeat(64))
|
|
550
|
-
|
|
551
|
-
const enabledPm = fakePrivateMessage()
|
|
552
|
-
const enabledMessenger = await new PrivateMessenger({ _privateMessage: enabledPm, autoDeletionCapability: false }).init({
|
|
553
|
-
userSigner: signer('other-user'),
|
|
554
|
-
channels: [{ pubkey: 'enabled', signer: signer('enabled'), relays: ['wss://relay.example'], autoDeletionCapability: true }]
|
|
555
|
-
})
|
|
556
|
-
await enabledMessenger.tell({ receiverPubkey: 'alice', payload: 'enabled' })
|
|
557
|
-
assert.equal(enabledPm.sent[0].options.autoDeletionCapability, true)
|
|
558
|
-
|
|
559
|
-
await messenger.tell({
|
|
560
|
-
channelPubkey: 'inherited',
|
|
561
|
-
receiverPubkey: 'alice',
|
|
562
|
-
payload: 'ignored fields',
|
|
563
|
-
deletionSeckey: 'b'.repeat(64),
|
|
564
|
-
autoDeletionCapability: false
|
|
565
|
-
})
|
|
566
|
-
assert.equal(pm.sent[3].options.deletionPubkey, undefined)
|
|
567
|
-
assert.equal(pm.sent[3].options.autoDeletionCapability, true)
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
test('private messenger update accepts only same-user replacement signers', async () => {
|
|
571
|
-
const pm = fakePrivateMessage()
|
|
572
|
-
const originalUser = signer('user')
|
|
573
|
-
const replacementUser = signer('user')
|
|
574
|
-
const otherUser = signer('other-user')
|
|
575
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
576
|
-
userSigner: originalUser,
|
|
577
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
await messenger.update({ userSigner: replacementUser })
|
|
581
|
-
|
|
582
|
-
assert.equal(messenger.userSigner, replacementUser)
|
|
583
|
-
assert.equal(messenger.userPubkey, 'user')
|
|
584
|
-
await assert.rejects(
|
|
585
|
-
() => messenger.update({ userSigner: otherUser }),
|
|
586
|
-
/USER_SIGNER_MISMATCH/
|
|
587
|
-
)
|
|
588
|
-
assert.equal(messenger.userSigner, replacementUser)
|
|
589
|
-
assert.equal(messenger.userPubkey, 'user')
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
test('private messenger falls back to recipient read relays when no relay set is configured', async () => {
|
|
593
|
-
const pm = fakePrivateMessage()
|
|
594
|
-
const relayLookups = []
|
|
595
|
-
const readRelays = pubkey => [
|
|
596
|
-
`wss://${pubkey}.read-one.example`,
|
|
597
|
-
`wss://${pubkey}.read-two.example`,
|
|
598
|
-
`wss://${pubkey}.read-three.example`
|
|
599
|
-
]
|
|
600
|
-
const messenger = await new PrivateMessenger({
|
|
601
|
-
_privateMessage: pm,
|
|
602
|
-
_getRelaysByPubkey: async pubkeys => {
|
|
603
|
-
relayLookups.push(pubkeys)
|
|
604
|
-
return Object.fromEntries(pubkeys.map(pubkey => [pubkey, {
|
|
605
|
-
read: readRelays(pubkey),
|
|
606
|
-
write: [`wss://${pubkey}.write.example`]
|
|
607
|
-
}]))
|
|
608
|
-
}
|
|
609
|
-
}).init({
|
|
610
|
-
userSigner: signer('user'),
|
|
611
|
-
channels: [{ pubkey: 'channel', signer: signer('channel') }]
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
assert.deepEqual(pm.watchCalls[0].relays, readRelays('user'))
|
|
615
|
-
|
|
616
|
-
await messenger.tell({ receiverPubkey: 'alice', payload: 'note' })
|
|
617
|
-
await messenger.broadcastNymRumor({
|
|
618
|
-
receiverPubkeys: ['bob'],
|
|
619
|
-
nymSigner: signer('nym'),
|
|
620
|
-
rumor: { kind: 9001, created_at: 1, tags: [], content: 'nym rumor' }
|
|
621
|
-
})
|
|
622
|
-
await messenger.broadcastNymEvent({
|
|
623
|
-
receiverPubkeys: ['carol'],
|
|
624
|
-
nymSigner: signer('nym'),
|
|
625
|
-
event: { id: 'nym-signed-id', kind: 9002, pubkey: 'author', created_at: 2, tags: [], content: 'nym event', sig: 'sig' }
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
assert.deepEqual(relayLookups, [['user'], ['alice'], ['bob'], ['carol']])
|
|
629
|
-
assert.equal(pm.sent[0].options.relays, undefined)
|
|
630
|
-
assert.deepEqual([...pm.sent[0].options.relayToReceivers.entries()], [
|
|
631
|
-
['wss://alice.read-one.example', ['alice']],
|
|
632
|
-
['wss://alice.read-two.example', ['alice']]
|
|
633
|
-
])
|
|
634
|
-
assert.equal(pm.sent[1].options.relays, undefined)
|
|
635
|
-
assert.deepEqual([...pm.sent[1].options.relayToReceivers.entries()], [
|
|
636
|
-
['wss://bob.read-one.example', ['bob']],
|
|
637
|
-
['wss://bob.read-two.example', ['bob']]
|
|
638
|
-
])
|
|
639
|
-
assert.equal(Object.prototype.hasOwnProperty.call(pm.sent[1].options, 'receiverPubkeys'), false)
|
|
640
|
-
assert.equal(pm.sent[2].options.relays, undefined)
|
|
641
|
-
assert.deepEqual([...pm.sent[2].options.relayToReceivers.entries()], [
|
|
642
|
-
['wss://carol.read-one.example', ['carol']],
|
|
643
|
-
['wss://carol.read-two.example', ['carol']]
|
|
644
|
-
])
|
|
645
|
-
assert.equal(Object.prototype.hasOwnProperty.call(pm.sent[2].options, 'receiverPubkeys'), false)
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
test('private messenger reload-gap fetch uses all local read relays when channel relays are absent', async () => {
|
|
649
|
-
const pm = fakePrivateMessage()
|
|
650
|
-
const fetches = []
|
|
651
|
-
let scheduled = null
|
|
652
|
-
const now = Math.floor(Date.now() / 1000)
|
|
653
|
-
const userReadRelays = [
|
|
654
|
-
'wss://user.read-one.example',
|
|
655
|
-
'wss://user.read-two.example',
|
|
656
|
-
'wss://user.read-three.example'
|
|
657
|
-
]
|
|
658
|
-
globalThis.localStorage.setItem('libp2r2p:private-messenger:user:state', JSON.stringify({
|
|
659
|
-
channels: {
|
|
660
|
-
channel: { lastSeenAt: now - 10, lastWatchedAt: now - 10 }
|
|
661
|
-
}
|
|
662
|
-
}))
|
|
663
|
-
|
|
664
|
-
const messenger = await new PrivateMessenger({
|
|
665
|
-
_privateMessage: pm,
|
|
666
|
-
_privateChannel: {
|
|
667
|
-
fetch: async options => {
|
|
668
|
-
fetches.push(options)
|
|
669
|
-
return []
|
|
670
|
-
}
|
|
671
|
-
},
|
|
672
|
-
_getRelaysByPubkey: async pubkeys => Object.fromEntries(pubkeys.map(pubkey => [pubkey, {
|
|
673
|
-
read: pubkey === 'user' ? userReadRelays : [`wss://${pubkey}.read.example`],
|
|
674
|
-
write: []
|
|
675
|
-
}])),
|
|
676
|
-
_setTimeout: fn => { scheduled = fn }
|
|
677
|
-
}).init({
|
|
678
|
-
userSigner: signer('user'),
|
|
679
|
-
channels: [{ pubkey: 'channel', signer: signer('channel') }]
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
assert.deepEqual(pm.watchCalls[0].relays, userReadRelays)
|
|
683
|
-
|
|
684
|
-
await scheduled()
|
|
685
|
-
|
|
686
|
-
assert.deepEqual(fetches[0].relays, userReadRelays)
|
|
687
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
test('private messenger refreshes NIP-65-derived watch relays from relay-list updates', async () => {
|
|
691
|
-
const pm = fakePrivateMessage()
|
|
692
|
-
const relayUpdates = fakeRelayListUpdates()
|
|
693
|
-
const fetches = []
|
|
694
|
-
const now = Math.floor(Date.now() / 1000)
|
|
695
|
-
let userReadRelays = ['wss://user.old-one.example', 'wss://user.old-two.example']
|
|
696
|
-
const messenger = await new PrivateMessenger({
|
|
697
|
-
_privateMessage: pm,
|
|
698
|
-
_privateChannel: {
|
|
699
|
-
fetch: async options => {
|
|
700
|
-
fetches.push(options)
|
|
701
|
-
options.onEvent({
|
|
702
|
-
id: 'missed-id',
|
|
703
|
-
kind: TELL_KIND,
|
|
704
|
-
pubkey: 'alice',
|
|
705
|
-
created_at: now - 5,
|
|
706
|
-
tags: [['r', 'user']],
|
|
707
|
-
content: 'missed'
|
|
708
|
-
}, { id: 'outer-id', created_at: now - 5 }, { channelPubkey: 'derived' })
|
|
709
|
-
return []
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
_getRelaysByPubkey: async pubkeys => Object.fromEntries(pubkeys.map(pubkey => [pubkey, {
|
|
713
|
-
read: pubkey === 'user' ? userReadRelays : [`wss://${pubkey}.read.example`],
|
|
714
|
-
write: []
|
|
715
|
-
}])),
|
|
716
|
-
_subscribeRelayListUpdates: relayUpdates.subscribe
|
|
717
|
-
}).init({
|
|
718
|
-
userSigner: signer('user'),
|
|
719
|
-
channels: [
|
|
720
|
-
{ pubkey: 'derived', signer: signer('derived') },
|
|
721
|
-
{ pubkey: 'explicit', signer: signer('explicit'), relays: ['wss://explicit.example'] }
|
|
722
|
-
]
|
|
723
|
-
})
|
|
724
|
-
messenger.updateChannelState('derived', { lastSeenAt: now - 20 })
|
|
725
|
-
messenger.updateChannelState('explicit', { lastSeenAt: now - 20 })
|
|
726
|
-
|
|
727
|
-
assert.equal(relayUpdates.subscriptions.length, 1)
|
|
728
|
-
assert.deepEqual(relayUpdates.subscriptions[0].pubkeys, ['user'])
|
|
729
|
-
assert.equal(relayUpdates.subscriptions[0].options.relayType, 'read')
|
|
730
|
-
assert.deepEqual(pm.watchCalls[0].channels, ['derived'])
|
|
731
|
-
assert.deepEqual(pm.watchCalls[0].relays, ['wss://user.old-one.example', 'wss://user.old-two.example'])
|
|
732
|
-
assert.deepEqual(pm.watchCalls[1].channels, ['explicit'])
|
|
733
|
-
assert.deepEqual(pm.watchCalls[1].relays, ['wss://explicit.example'])
|
|
734
|
-
|
|
735
|
-
userReadRelays = ['wss://user.old-two.example', 'wss://user.new.example']
|
|
736
|
-
await relayUpdates.subscriptions[0].emit({ pubkey: 'user' })
|
|
737
|
-
|
|
738
|
-
assert.equal(pm.watchCalls.length, 3)
|
|
739
|
-
assert.deepEqual(pm.watchCalls[2].channels, ['derived'])
|
|
740
|
-
assert.deepEqual(pm.watchCalls[2].relays, ['wss://user.old-two.example', 'wss://user.new.example'])
|
|
741
|
-
assert.deepEqual(pm.stopped, [])
|
|
742
|
-
assert.equal(fetches.length, 1)
|
|
743
|
-
assert.deepEqual(fetches[0].privateChannelPubkeys, ['derived'])
|
|
744
|
-
assert.deepEqual(fetches[0].relays, ['wss://user.old-two.example', 'wss://user.new.example'])
|
|
745
|
-
assert.ok(fetches[0].since <= now - 20)
|
|
746
|
-
assert.ok(fetches[0].until >= now)
|
|
747
|
-
assert.equal((await messenger.nextMessage()).event.id, 'missed-id')
|
|
748
|
-
assert.deepEqual(messenger.readState().channels.derived.relays, ['wss://user.old-two.example', 'wss://user.new.example'])
|
|
749
|
-
assert.deepEqual(messenger.readState().channels.explicit.relays, ['wss://explicit.example'])
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
test('private messenger does not subscribe to relay-list updates for explicit-only channels', async () => {
|
|
753
|
-
const pm = fakePrivateMessage()
|
|
754
|
-
const relayUpdates = fakeRelayListUpdates()
|
|
755
|
-
await new PrivateMessenger({
|
|
756
|
-
_privateMessage: pm,
|
|
757
|
-
_subscribeRelayListUpdates: relayUpdates.subscribe
|
|
758
|
-
}).init({
|
|
759
|
-
userSigner: signer('user'),
|
|
760
|
-
channels: [{ pubkey: 'explicit', signer: signer('explicit'), relays: ['wss://explicit.example'] }]
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
assert.equal(relayUpdates.subscriptions.length, 0)
|
|
764
|
-
assert.deepEqual(pm.watchCalls[0].relays, ['wss://explicit.example'])
|
|
765
|
-
})
|
|
766
|
-
|
|
767
|
-
test('private messenger prefers explicit relay receiver maps over channel relays', async () => {
|
|
768
|
-
const pm = fakePrivateMessage()
|
|
769
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
770
|
-
userSigner: signer('user'),
|
|
771
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://channel.example'] }]
|
|
772
|
-
})
|
|
773
|
-
const relayToReceivers = new Map([
|
|
774
|
-
['wss://alice.example', ['alice']],
|
|
775
|
-
['wss://bob.example', ['bob']]
|
|
776
|
-
])
|
|
777
|
-
|
|
778
|
-
await messenger.broadcastRumor({
|
|
779
|
-
receiverPubkeys: ['alice', 'bob'],
|
|
780
|
-
relayToReceivers,
|
|
781
|
-
rumor: { kind: 9001, created_at: 1, tags: [], content: 'raw' }
|
|
782
|
-
})
|
|
783
|
-
|
|
784
|
-
assert.equal(pm.sent[0].options.relays, undefined)
|
|
785
|
-
assert.equal(pm.sent[0].options.relayToReceivers, relayToReceivers)
|
|
786
|
-
})
|
|
787
|
-
|
|
788
|
-
test('private messenger uses channel sendRelays after per-call routing overrides', async () => {
|
|
789
|
-
const pm = fakePrivateMessage()
|
|
790
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
791
|
-
userSigner: signer('user'),
|
|
792
|
-
channels: [{
|
|
793
|
-
pubkey: 'channel',
|
|
794
|
-
signer: signer('channel'),
|
|
795
|
-
relays: ['wss://watch-one.example', 'wss://watch-two.example', 'wss://watch-three.example'],
|
|
796
|
-
sendRelays: ['wss://send-one.example', 'wss://send-two.example']
|
|
797
|
-
}]
|
|
798
|
-
})
|
|
799
|
-
const relayToReceivers = new Map([['wss://mapped.example', ['alice']]])
|
|
800
|
-
|
|
801
|
-
await messenger.tell({ receiverPubkey: 'alice', payload: 'default send relays' })
|
|
802
|
-
await messenger.tell({ receiverPubkey: 'alice', relays: ['wss://per-call.example'], payload: 'per call relays' })
|
|
803
|
-
await messenger.tell({ receiverPubkey: 'alice', relayToReceivers, payload: 'mapped relays' })
|
|
804
|
-
|
|
805
|
-
assert.deepEqual(pm.watchCalls[0].relays, ['wss://watch-one.example', 'wss://watch-two.example', 'wss://watch-three.example'])
|
|
806
|
-
assert.deepEqual(pm.sent[0].options.relays, ['wss://send-one.example', 'wss://send-two.example'])
|
|
807
|
-
assert.deepEqual(pm.sent[1].options.relays, ['wss://per-call.example'])
|
|
808
|
-
assert.equal(pm.sent[2].options.relays, undefined)
|
|
809
|
-
assert.equal(pm.sent[2].options.relayToReceivers, relayToReceivers)
|
|
810
|
-
})
|
|
811
|
-
|
|
812
|
-
test('private messenger mirrors routed and nym sends to recovery seeder relays', async () => {
|
|
813
|
-
const pm = fakePrivateMessage()
|
|
814
|
-
const relayLookups = []
|
|
815
|
-
const messenger = await new PrivateMessenger({
|
|
816
|
-
_privateMessage: pm,
|
|
817
|
-
_getRelaysByPubkey: async pubkeys => {
|
|
818
|
-
relayLookups.push(pubkeys)
|
|
819
|
-
return Object.fromEntries(pubkeys.map(pubkey => [pubkey, {
|
|
820
|
-
read: [`wss://${pubkey}.read.example`],
|
|
821
|
-
write: [`wss://${pubkey}.write.example`]
|
|
822
|
-
}]))
|
|
823
|
-
}
|
|
824
|
-
}).init({
|
|
825
|
-
userSigner: signer('user'),
|
|
826
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), seeders: ['seed1'] }]
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
await messenger.tell({ receiverPubkey: 'alice', payload: 'note' })
|
|
830
|
-
await messenger.broadcastNymRumor({
|
|
831
|
-
receiverPubkeys: ['bob'],
|
|
832
|
-
nymSigner: signer('nym'),
|
|
833
|
-
rumor: { kind: 9001, created_at: 1, tags: [], content: 'nym rumor' }
|
|
834
|
-
})
|
|
835
|
-
await messenger.broadcastRumor({
|
|
836
|
-
receiverPubkeys: ['carol'],
|
|
837
|
-
relayToReceivers: new Map([['wss://carol.custom.example', ['carol']]]),
|
|
838
|
-
rumor: { kind: 9002, created_at: 2, tags: [], content: 'explicit map' }
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
assert.deepEqual(relayLookups, [['user'], ['seed1'], ['alice'], ['seed1'], ['bob'], ['seed1']])
|
|
842
|
-
assert.deepEqual(pm.sent[0].options.recoveryRelays, ['wss://seed1.read.example'])
|
|
843
|
-
assert.deepEqual([...pm.sent[0].options.relayToReceivers.entries()], [['wss://alice.read.example', ['alice']]])
|
|
844
|
-
assert.deepEqual(pm.sent[1].options.recoveryRelays, ['wss://seed1.read.example'])
|
|
845
|
-
assert.deepEqual([...pm.sent[1].options.relayToReceivers.entries()], [['wss://bob.read.example', ['bob']]])
|
|
846
|
-
assert.deepEqual(pm.sent[2].options.recoveryRelays, ['wss://seed1.read.example'])
|
|
847
|
-
assert.deepEqual([...pm.sent[2].options.relayToReceivers.entries()], [['wss://carol.custom.example', ['carol']]])
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
test('private messenger reader-only channels watch and drain but reject sends', async () => {
|
|
851
|
-
const pm = fakePrivateMessage()
|
|
852
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
853
|
-
userSigner: signer('user'),
|
|
854
|
-
channels: [{ pubkey: 'channel', readerSigner: signer('reader'), relays: ['wss://relay.example'] }]
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
assert.equal(pm.watchCalls[0].privateChannelSigner, null)
|
|
858
|
-
assert.equal(pm.watchCalls[0].privateChannelReaderSigner.getPublicKey(), 'reader')
|
|
859
|
-
|
|
860
|
-
pm.watchCalls[0].onTell({
|
|
861
|
-
event: { id: 'tell-id', kind: TELL_KIND, pubkey: 'alice', created_at: 10, tags: [['r', 'user']], content: 'hi' },
|
|
862
|
-
outer: { id: 'outer-id', created_at: 11 },
|
|
863
|
-
meta: { channelPubkey: 'channel' },
|
|
864
|
-
payload: { payload: 'hi' },
|
|
865
|
-
tell: { id: 'tell-id' }
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
assert.equal((await messenger.nextMessage()).event.id, 'tell-id')
|
|
869
|
-
await assert.rejects(
|
|
870
|
-
() => messenger.tell({ channelPubkey: 'channel', receiverPubkey: 'alice', payload: 'note' }),
|
|
871
|
-
/PRIVATE_CHANNEL_WRITER_REQUIRED/
|
|
872
|
-
)
|
|
873
|
-
await assert.rejects(
|
|
874
|
-
() => messenger.broadcastNymRumor({ channelPubkey: 'channel', nymSigner: signer('nym'), rumor: { kind: 1, tags: [], content: 'note' } }),
|
|
875
|
-
/PRIVATE_CHANNEL_WRITER_REQUIRED/
|
|
876
|
-
)
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
test('private messenger resolves nym signers by method channel then global order', async () => {
|
|
880
|
-
const pm = fakePrivateMessage()
|
|
881
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
882
|
-
userSigner: signer('user'),
|
|
883
|
-
nymSigner: signer('global-nym'),
|
|
884
|
-
channels: [
|
|
885
|
-
{ pubkey: 'global-channel', signer: signer('global-channel'), relays: ['wss://relay.example'] },
|
|
886
|
-
{ pubkey: 'channel-nym', signer: signer('channel-nym'), nymSigner: signer('channel-nym-signer'), relays: ['wss://relay.example'] }
|
|
887
|
-
]
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
await messenger.broadcastNymRumor({ channelPubkey: 'global-channel', rumor: { kind: 1, tags: [], content: 'global' } })
|
|
891
|
-
await messenger.broadcastNymRumor({ channelPubkey: 'channel-nym', rumor: { kind: 1, tags: [], content: 'channel' } })
|
|
892
|
-
await messenger.broadcastNymRumor({ channelPubkey: 'channel-nym', nymSigner: signer('method-nym'), rumor: { kind: 1, tags: [], content: 'method' } })
|
|
893
|
-
|
|
894
|
-
assert.deepEqual(
|
|
895
|
-
pm.sent.filter(sent => sent.method === 'broadcastNymRumor').map(sent => sent.options.nymSigner.getPublicKey()),
|
|
896
|
-
['global-nym', 'channel-nym-signer', 'method-nym']
|
|
897
|
-
)
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
test('private messenger writer channels can encrypt to a reader key', async () => {
|
|
901
|
-
const pm = fakePrivateMessage()
|
|
902
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
903
|
-
userSigner: signer('user'),
|
|
904
|
-
channels: [{
|
|
905
|
-
pubkey: 'channel',
|
|
906
|
-
signer: signer('channel'),
|
|
907
|
-
readerSigner: signer('reader'),
|
|
908
|
-
relays: ['wss://relay.example']
|
|
909
|
-
}]
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
await messenger.tell({ channelPubkey: 'channel', receiverPubkey: 'alice', payload: 'note' })
|
|
913
|
-
|
|
914
|
-
assert.equal(pm.watchCalls[0].privateChannelSigner.getPublicKey(), 'channel')
|
|
915
|
-
assert.equal(pm.watchCalls[0].privateChannelReaderSigner.getPublicKey(), 'reader')
|
|
916
|
-
assert.equal(pm.watchCalls[0].privateChannelReaderPubkey, 'reader')
|
|
917
|
-
assert.equal(pm.sent[0].options.privateChannelSigner.getPublicKey(), 'channel')
|
|
918
|
-
assert.equal(pm.sent[0].options.privateChannelReaderPubkey, 'reader')
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
test('private messenger writer channels can read with only a reader pubkey', async () => {
|
|
922
|
-
const pm = fakePrivateMessage()
|
|
923
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
924
|
-
userSigner: signer('user'),
|
|
925
|
-
channels: [{
|
|
926
|
-
pubkey: 'channel',
|
|
927
|
-
signer: signer('channel'),
|
|
928
|
-
readerPubkey: 'reader',
|
|
929
|
-
relays: ['wss://relay.example']
|
|
930
|
-
}]
|
|
931
|
-
})
|
|
932
|
-
|
|
933
|
-
await messenger.tell({ channelPubkey: 'channel', receiverPubkey: 'alice', payload: 'note' })
|
|
934
|
-
|
|
935
|
-
assert.equal(pm.watchCalls[0].privateChannelSigner.getPublicKey(), 'channel')
|
|
936
|
-
assert.equal(pm.watchCalls[0].privateChannelReaderSigner.getPublicKey(), 'channel')
|
|
937
|
-
assert.equal(pm.watchCalls[0].privateChannelReaderPubkey, 'reader')
|
|
938
|
-
assert.equal(pm.sent[0].options.privateChannelReaderPubkey, 'reader')
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
test('reader-only channels cannot use recovery seed modes', async () => {
|
|
942
|
-
const pm = fakePrivateMessage()
|
|
943
|
-
await assert.rejects(
|
|
944
|
-
() => new PrivateMessenger({ _privateMessage: pm }).init({
|
|
945
|
-
userSigner: signer('user'),
|
|
946
|
-
channels: [{ pubkey: 'channel', readerSigner: signer('reader'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
947
|
-
}),
|
|
948
|
-
/PRIVATE_CHANNEL_WRITER_REQUIRED/
|
|
949
|
-
)
|
|
950
|
-
await assert.rejects(
|
|
951
|
-
() => new PrivateMessenger({ _privateMessage: pm }).init({
|
|
952
|
-
userSigner: signer('user'),
|
|
953
|
-
channels: [{ pubkey: 'channel', readerSigner: signer('reader'), relays: ['wss://relay.example'], mode: 'watchtower' }]
|
|
954
|
-
}),
|
|
955
|
-
/PRIVATE_CHANNEL_WRITER_REQUIRED/
|
|
956
|
-
)
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
test('private messenger debug reports send and enqueue events without payload secrets', async () => {
|
|
960
|
-
const pm = fakePrivateMessage()
|
|
961
|
-
const debugEvents = []
|
|
962
|
-
const messenger = await new PrivateMessenger({
|
|
963
|
-
_privateMessage: pm,
|
|
964
|
-
onDebug: event => debugEvents.push(event)
|
|
965
|
-
}).init({
|
|
966
|
-
userSigner: signer('user'),
|
|
967
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
968
|
-
})
|
|
969
|
-
|
|
970
|
-
await messenger.yell({
|
|
971
|
-
receiverPubkeys: ['alice', 'bob'],
|
|
972
|
-
code: 'contentKeys_reply_v1',
|
|
973
|
-
payload: { keys: [{ pubkey: 'pubkey', seckey: 'sent-secret' }] }
|
|
974
|
-
})
|
|
975
|
-
await pm.watchCalls[0].onReply({
|
|
976
|
-
event: { id: 'reply-id', kind: REPLY_KIND, pubkey: 'alice', created_at: 12, tags: [['q', 'question-id']], content: 'pong' },
|
|
977
|
-
outer: { id: 'outer-reply-id', created_at: 13 },
|
|
978
|
-
meta: { channelPubkey: 'channel' },
|
|
979
|
-
payload: { code: 'contentKeys_reply_v1', payload: { keys: [{ pubkey: 'pubkey', seckey: 'received-secret' }] } },
|
|
980
|
-
questionId: 'question-id',
|
|
981
|
-
reply: { id: 'reply-id' }
|
|
982
|
-
})
|
|
983
|
-
|
|
984
|
-
const send = debugEvents.find(event => event.action === 'send' && event.method === 'yell')
|
|
985
|
-
const enqueue = debugEvents.find(event => event.action === 'enqueue' && event.type === 'reply')
|
|
986
|
-
assert.ok(debugEvents.some(event => event.action === 'watch'))
|
|
987
|
-
assert.equal(send.code, 'contentKeys_reply_v1')
|
|
988
|
-
assert.deepEqual(send.receiverPubkeys, ['alice', 'bob'])
|
|
989
|
-
assert.equal(send.receiverCount, 2)
|
|
990
|
-
assert.equal(enqueue.code, 'contentKeys_reply_v1')
|
|
991
|
-
assert.equal(enqueue.channelPubkey, 'channel')
|
|
992
|
-
assert.equal(enqueue.senderPubkey, 'alice')
|
|
993
|
-
assert.equal(JSON.stringify(debugEvents).includes('sent-secret'), false)
|
|
994
|
-
assert.equal(JSON.stringify(debugEvents).includes('received-secret'), false)
|
|
995
|
-
})
|
|
996
|
-
|
|
997
|
-
test('private messenger can disable receiver content-key lookup for identity-only traffic', async () => {
|
|
998
|
-
const pm = fakePrivateMessage()
|
|
999
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm, useContentKeys: false }).init({
|
|
1000
|
-
userSigner: signer('user'),
|
|
1001
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
1002
|
-
})
|
|
1003
|
-
|
|
1004
|
-
await messenger.tell({ receiverPubkey: 'alice', payload: 'identity only' })
|
|
1005
|
-
|
|
1006
|
-
assert.equal(typeof pm.sent[0].options._getIykcProofs, 'function')
|
|
1007
|
-
assert.deepEqual(await pm.sent[0].options._getIykcProofs(['alice']), {})
|
|
1008
|
-
})
|
|
1009
|
-
|
|
1010
|
-
test('clearChannel removes queued items and channel state without clearing other channels', async () => {
|
|
1011
|
-
const pm = fakePrivateMessage()
|
|
1012
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1013
|
-
userSigner: signer('user'),
|
|
1014
|
-
channels: [
|
|
1015
|
-
{ pubkey: 'one', signer: signer('one'), relays: ['wss://relay.example'] },
|
|
1016
|
-
{ pubkey: 'two', signer: signer('two'), relays: ['wss://relay.example'] }
|
|
1017
|
-
]
|
|
1018
|
-
})
|
|
1019
|
-
messenger.queue.enqueue({ type: 'tell', channelPubkey: 'one', event: { id: 'one' } })
|
|
1020
|
-
messenger.queue.enqueue({ type: 'tell', channelPubkey: 'two', event: { id: 'two' } })
|
|
1021
|
-
|
|
1022
|
-
await messenger.clearChannel('one')
|
|
1023
|
-
|
|
1024
|
-
const item = (await messenger.nextMessage())
|
|
1025
|
-
assert.equal(item.channelPubkey, 'two')
|
|
1026
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1027
|
-
assert.equal(messenger.channels.has('one'), false)
|
|
1028
|
-
assert.equal(messenger.readState().channels.one, undefined)
|
|
1029
|
-
assert.ok(messenger.readState().channels.two)
|
|
1030
|
-
assert.deepEqual(pm.cleared, ['one'])
|
|
1031
|
-
})
|
|
1032
|
-
|
|
1033
|
-
test('watch schedules reload-gap recovery and fetches missing channel window', async () => {
|
|
1034
|
-
const pm = fakePrivateMessage()
|
|
1035
|
-
const fetches = []
|
|
1036
|
-
let scheduled = null
|
|
1037
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1038
|
-
globalThis.localStorage.setItem('libp2r2p:private-messenger:user:state', JSON.stringify({
|
|
1039
|
-
channels: {
|
|
1040
|
-
channel: { lastSeenAt: now - 10, lastWatchedAt: now - 10 }
|
|
1041
|
-
}
|
|
1042
|
-
}))
|
|
1043
|
-
const messenger = await new PrivateMessenger({
|
|
1044
|
-
_privateMessage: pm,
|
|
1045
|
-
_privateChannel: {
|
|
1046
|
-
fetch: async options => {
|
|
1047
|
-
fetches.push(options)
|
|
1048
|
-
options.onEvent({
|
|
1049
|
-
id: 'ask-id',
|
|
1050
|
-
kind: ASK_KIND,
|
|
1051
|
-
pubkey: 'alice',
|
|
1052
|
-
created_at: now - 5,
|
|
1053
|
-
tags: [['r', 'user']],
|
|
1054
|
-
content: 'missed'
|
|
1055
|
-
}, { id: 'outer-id', created_at: now - 5 }, { channelPubkey: 'channel' })
|
|
1056
|
-
}
|
|
1057
|
-
},
|
|
1058
|
-
_setTimeout: fn => { scheduled = fn }
|
|
1059
|
-
}).init({
|
|
1060
|
-
userSigner: signer('user'),
|
|
1061
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
|
|
1062
|
-
})
|
|
1063
|
-
|
|
1064
|
-
await scheduled()
|
|
1065
|
-
|
|
1066
|
-
assert.equal(fetches.length, 1)
|
|
1067
|
-
assert.equal(fetches[0].privateChannelPubkeys[0], 'channel')
|
|
1068
|
-
assert.ok(fetches[0].since <= now - 10)
|
|
1069
|
-
assert.ok(fetches[0].until >= now)
|
|
1070
|
-
assert.equal(fetches[0].receivedChunkTtlMs, 7 * 24 * 60 * 60 * 1000)
|
|
1071
|
-
assert.equal((await messenger.nextMessage()).event.id, 'ask-id')
|
|
1072
|
-
assert.deepEqual(messenger.readState().channels.channel.offlineRanges, [])
|
|
1073
|
-
})
|
|
1074
|
-
|
|
1075
|
-
test('reader-only channels fetch reload gaps with the reader signer', async () => {
|
|
1076
|
-
const pm = fakePrivateMessage()
|
|
1077
|
-
const fetches = []
|
|
1078
|
-
let scheduled = null
|
|
1079
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1080
|
-
globalThis.localStorage.setItem('libp2r2p:private-messenger:user:state', JSON.stringify({
|
|
1081
|
-
channels: {
|
|
1082
|
-
channel: { lastSeenAt: now - 10, lastWatchedAt: now - 10 }
|
|
1083
|
-
}
|
|
1084
|
-
}))
|
|
1085
|
-
const messenger = await new PrivateMessenger({
|
|
1086
|
-
_privateMessage: pm,
|
|
1087
|
-
_privateChannel: {
|
|
1088
|
-
fetch: async options => {
|
|
1089
|
-
fetches.push(options)
|
|
1090
|
-
options.onEvent({
|
|
1091
|
-
id: 'missed-id',
|
|
1092
|
-
kind: TELL_KIND,
|
|
1093
|
-
pubkey: 'alice',
|
|
1094
|
-
created_at: now - 5,
|
|
1095
|
-
tags: [['r', 'user']],
|
|
1096
|
-
content: 'missed'
|
|
1097
|
-
}, { id: 'outer-id', created_at: now - 5 }, { channelPubkey: 'channel' })
|
|
1098
|
-
}
|
|
1099
|
-
},
|
|
1100
|
-
_setTimeout: fn => { scheduled = fn }
|
|
1101
|
-
}).init({
|
|
1102
|
-
userSigner: signer('user'),
|
|
1103
|
-
channels: [{ pubkey: 'channel', readerSigner: signer('reader'), relays: ['wss://relay.example'] }]
|
|
1104
|
-
})
|
|
1105
|
-
|
|
1106
|
-
await scheduled()
|
|
1107
|
-
|
|
1108
|
-
assert.equal(fetches.length, 1)
|
|
1109
|
-
assert.equal(fetches[0].privateChannelSigner, null)
|
|
1110
|
-
assert.equal(fetches[0].privateChannelReaderSigner.getPublicKey(), 'reader')
|
|
1111
|
-
assert.equal((await messenger.nextMessage()).event.id, 'missed-id')
|
|
1112
|
-
})
|
|
1113
|
-
|
|
1114
|
-
test('seeder channels publish presence immediately and on interval', async () => {
|
|
1115
|
-
const pm = fakePrivateMessage()
|
|
1116
|
-
const intervals = []
|
|
1117
|
-
const cleared = []
|
|
1118
|
-
const messenger = await new PrivateMessenger({
|
|
1119
|
-
_privateMessage: pm,
|
|
1120
|
-
_setInterval: (fn, ms) => {
|
|
1121
|
-
const timer = { fn, ms }
|
|
1122
|
-
intervals.push(timer)
|
|
1123
|
-
return timer
|
|
1124
|
-
},
|
|
1125
|
-
_clearInterval: timer => cleared.push(timer)
|
|
1126
|
-
}).init({
|
|
1127
|
-
userSigner: signer('user'),
|
|
1128
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder', seeders: ['alice'], autoDeletionCapability: false }]
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1131
|
-
assert.equal(pm.sent[0].method, 'yell')
|
|
1132
|
-
assert.equal(pm.sent[0].options.code, SEEDER_PRESENCE_CODE)
|
|
1133
|
-
assert.deepEqual(pm.sent[0].options.receiverPubkeys, ['alice', 'user'])
|
|
1134
|
-
assert.equal(pm.sent[0].options.autoDeletionCapability, false)
|
|
1135
|
-
assert.equal(intervals[0].ms, 10 * 60 * 1000)
|
|
1136
|
-
|
|
1137
|
-
await intervals[0].fn()
|
|
1138
|
-
|
|
1139
|
-
assert.equal(pm.sent[1].method, 'yell')
|
|
1140
|
-
assert.equal(pm.sent[1].options.code, SEEDER_PRESENCE_CODE)
|
|
1141
|
-
assert.equal(pm.sent[1].options.autoDeletionCapability, false)
|
|
1142
|
-
|
|
1143
|
-
messenger.close()
|
|
1144
|
-
assert.deepEqual(cleared, intervals)
|
|
1145
|
-
})
|
|
1146
|
-
|
|
1147
|
-
test('seeder channels store router seeds separately, consume messages, and answer missing-message asks', async () => {
|
|
1148
|
-
const pm = fakePrivateMessage()
|
|
1149
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1150
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1151
|
-
userSigner: signer('seeder'),
|
|
1152
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
1153
|
-
})
|
|
1154
|
-
const userRow = JSON.stringify(['user', 'ciphertext'])
|
|
1155
|
-
const otherRow = JSON.stringify(['other', 'ciphertext'])
|
|
1156
|
-
|
|
1157
|
-
await pm.watchCalls[0].onSeed({
|
|
1158
|
-
channelPubkey: 'channel',
|
|
1159
|
-
outer: { id: 'outer-id', kind: 3560, pubkey: 'channel', created_at: now },
|
|
1160
|
-
router: {
|
|
1161
|
-
kind: 26300,
|
|
1162
|
-
pubkey: 'router',
|
|
1163
|
-
created_at: now,
|
|
1164
|
-
tags: [['f', 'alice'], ['c', '0', '1']],
|
|
1165
|
-
content: jsonlContent(payloadRow(), userRow, otherRow)
|
|
1166
|
-
}
|
|
1167
|
-
})
|
|
1168
|
-
await pm.watchCalls[0].onSeed({
|
|
1169
|
-
channelPubkey: 'channel',
|
|
1170
|
-
outer: { id: 'outer-duplicate-id', kind: 3560, pubkey: 'channel', created_at: now + 100 },
|
|
1171
|
-
router: {
|
|
1172
|
-
kind: 26300,
|
|
1173
|
-
pubkey: 'router-duplicate',
|
|
1174
|
-
created_at: now + 100,
|
|
1175
|
-
tags: [['f', 'alice'], ['c', '0', '1']],
|
|
1176
|
-
content: jsonlContent(payloadRow(), userRow, otherRow)
|
|
1177
|
-
}
|
|
1178
|
-
})
|
|
1179
|
-
|
|
1180
|
-
const storedSeeds = []
|
|
1181
|
-
for await (const seed of messenger.seedQueue.storedItems()) storedSeeds.push(seed)
|
|
1182
|
-
const routerRows = storedSeeds.filter(seed => seed.recordType === ROUTER_SEED_RECORD_TYPE)
|
|
1183
|
-
assert.equal(routerRows.length, 2)
|
|
1184
|
-
assert.equal(routerRows.find(seed => seed.receiverPubkey === 'user').firstSeenAt, now)
|
|
1185
|
-
assert.equal(routerRows.find(seed => seed.receiverPubkey === 'user').lastSeenAt, now + 100)
|
|
1186
|
-
|
|
1187
|
-
pm.watchCalls[0].onTell({
|
|
1188
|
-
event: { id: 'tell-id', kind: TELL_KIND, pubkey: 'alice', created_at: now, tags: [['r', 'seeder']], content: 'hi' },
|
|
1189
|
-
outer: { id: 'tell-outer-id', created_at: now },
|
|
1190
|
-
meta: { channelPubkey: 'channel' },
|
|
1191
|
-
payload: { payload: 'hi' },
|
|
1192
|
-
tell: { id: 'tell-id' }
|
|
1193
|
-
})
|
|
1194
|
-
|
|
1195
|
-
const item = (await messenger.nextMessage())
|
|
1196
|
-
assert.equal(item.type, 'tell')
|
|
1197
|
-
assert.equal(item.event.id, 'tell-id')
|
|
1198
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1199
|
-
|
|
1200
|
-
await pm.watchCalls[0].onAsk({
|
|
1201
|
-
event: {
|
|
1202
|
-
id: 'question-id',
|
|
1203
|
-
kind: ASK_KIND,
|
|
1204
|
-
pubkey: 'user',
|
|
1205
|
-
created_at: now,
|
|
1206
|
-
tags: [['r', 'seeder'], ['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1207
|
-
content: JSON.stringify({ since: now + 50, until: now + 60 })
|
|
1208
|
-
},
|
|
1209
|
-
outer: { id: 'ask-outer-id', created_at: now },
|
|
1210
|
-
meta: { channelPubkey: 'channel' },
|
|
1211
|
-
payload: { code: MISSING_MESSAGES_ASK_CODE, payload: { since: now + 50, until: now + 60 } },
|
|
1212
|
-
question: { id: 'question-id' }
|
|
1213
|
-
})
|
|
1214
|
-
|
|
1215
|
-
const reply = pm.sent.find(sent => sent.method === 'reply' && sent.options.code === MISSING_MESSAGES_REPLY_CODE)
|
|
1216
|
-
assert.equal(reply.options.receiverPubkey, 'user')
|
|
1217
|
-
assert.equal(reply.options.payload.isLast, true)
|
|
1218
|
-
const records = reply.options.payload.jsonl.trim().split('\n').map(line => JSON.parse(line))
|
|
1219
|
-
assert.equal(records.length, 1)
|
|
1220
|
-
assert.equal(records[0].recordType, ROUTER_SEED_RECORD_TYPE)
|
|
1221
|
-
assert.equal(records[0].router.kind, 26300)
|
|
1222
|
-
assert.equal(Buffer.from(records[0].router.content, 'base64').toString(), `${payloadRow()}\n${userRow}\n`)
|
|
1223
|
-
assert.deepEqual(records[0].router.tags, [['f', 'alice'], ['c', '0', '1']])
|
|
1224
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1225
|
-
})
|
|
1226
|
-
|
|
1227
|
-
test('router seed rows dedupe by proven inner id without content-key pubkey', async () => {
|
|
1228
|
-
const pm = fakePrivateMessage()
|
|
1229
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1230
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1231
|
-
userSigner: signer('user'),
|
|
1232
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
1233
|
-
})
|
|
1234
|
-
const oldContentRow = JSON.stringify(['user', 'old-ciphertext', 'old-content-key', 'old-proof'])
|
|
1235
|
-
const newContentRow = JSON.stringify(['user', 'new-ciphertext', 'new-content-key', 'new-proof'])
|
|
1236
|
-
|
|
1237
|
-
await pm.watchCalls[0].onSeed({
|
|
1238
|
-
channelPubkey: 'channel',
|
|
1239
|
-
outer: { id: 'outer-id', kind: 3560, pubkey: 'channel', created_at: now },
|
|
1240
|
-
router: {
|
|
1241
|
-
kind: 26300,
|
|
1242
|
-
pubkey: 'router',
|
|
1243
|
-
created_at: now,
|
|
1244
|
-
tags: [['f', 'alice'], ['c', '0', '1']],
|
|
1245
|
-
content: jsonlContent(payloadRow(), oldContentRow, newContentRow)
|
|
1246
|
-
},
|
|
1247
|
-
innerEventIdsByRowIndex: { 1: 'same-inner-id', 2: 'same-inner-id' }
|
|
1248
|
-
})
|
|
1249
|
-
|
|
1250
|
-
const storedSeeds = []
|
|
1251
|
-
for await (const seed of messenger.seedQueue.storedItems()) storedSeeds.push(seed)
|
|
1252
|
-
|
|
1253
|
-
assert.equal(storedSeeds.filter(seed => seed.recordType === ROUTER_SEED_RECORD_TYPE).length, 1)
|
|
1254
|
-
assert.equal(storedSeeds[0].receiverPubkey, 'user')
|
|
1255
|
-
assert.equal(storedSeeds[0].innerEventId, 'same-inner-id')
|
|
1256
|
-
assert.equal(storedSeeds[0].iykcPubkey, 'new-content-key')
|
|
1257
|
-
assert.equal(storedSeeds[0].row, newContentRow)
|
|
1258
|
-
})
|
|
1259
|
-
|
|
1260
|
-
test('watchtower channels store router seeds without consuming normal messages', async () => {
|
|
1261
|
-
const pm = fakePrivateMessage()
|
|
1262
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1263
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1264
|
-
userSigner: signer('watchtower'),
|
|
1265
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'watchtower' }]
|
|
1266
|
-
})
|
|
1267
|
-
const userRow = JSON.stringify(['user', 'ciphertext'])
|
|
1268
|
-
|
|
1269
|
-
assert.equal(pm.watchCalls[0].mode, 'watchtower')
|
|
1270
|
-
|
|
1271
|
-
pm.watchCalls[0].onSeed({
|
|
1272
|
-
channelPubkey: 'channel',
|
|
1273
|
-
outer: { id: 'outer-id', kind: 3560, pubkey: 'channel', created_at: now },
|
|
1274
|
-
router: {
|
|
1275
|
-
kind: 26300,
|
|
1276
|
-
pubkey: 'router',
|
|
1277
|
-
created_at: now,
|
|
1278
|
-
tags: [['f', 'alice'], ['c', '0', '1']],
|
|
1279
|
-
content: jsonlContent(payloadRow(), userRow)
|
|
1280
|
-
}
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
pm.watchCalls[0].onTell({
|
|
1284
|
-
event: { id: 'tell-id', kind: TELL_KIND, pubkey: 'alice', created_at: now, tags: [['r', 'watchtower']], content: 'hi' },
|
|
1285
|
-
outer: { id: 'tell-outer-id', created_at: now },
|
|
1286
|
-
meta: { channelPubkey: 'channel' },
|
|
1287
|
-
payload: { payload: 'hi' },
|
|
1288
|
-
tell: { id: 'tell-id' }
|
|
1289
|
-
})
|
|
1290
|
-
|
|
1291
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1292
|
-
|
|
1293
|
-
await pm.watchCalls[0].onAsk({
|
|
1294
|
-
event: {
|
|
1295
|
-
id: 'question-id',
|
|
1296
|
-
kind: ASK_KIND,
|
|
1297
|
-
pubkey: 'user',
|
|
1298
|
-
created_at: now,
|
|
1299
|
-
tags: [['r', 'watchtower'], ['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1300
|
-
content: JSON.stringify({ since: now - 5, until: now + 5 })
|
|
1301
|
-
},
|
|
1302
|
-
outer: { id: 'ask-outer-id', created_at: now },
|
|
1303
|
-
meta: { channelPubkey: 'channel' },
|
|
1304
|
-
payload: { code: MISSING_MESSAGES_ASK_CODE, payload: { since: now - 5, until: now + 5 } },
|
|
1305
|
-
question: { id: 'question-id' }
|
|
1306
|
-
})
|
|
1307
|
-
|
|
1308
|
-
const reply = pm.sent.find(sent => sent.method === 'reply' && sent.options.code === MISSING_MESSAGES_REPLY_CODE)
|
|
1309
|
-
assert.equal(reply.options.receiverPubkey, 'user')
|
|
1310
|
-
assert.equal(reply.options.payload.isLast, true)
|
|
1311
|
-
const records = reply.options.payload.jsonl.trim().split('\n').map(line => JSON.parse(line))
|
|
1312
|
-
assert.equal(records.length, 1)
|
|
1313
|
-
assert.equal(records[0].recordType, ROUTER_SEED_RECORD_TYPE)
|
|
1314
|
-
assert.equal(Buffer.from(records[0].router.content, 'base64').toString(), `${payloadRow()}\n${userRow}\n`)
|
|
1315
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1316
|
-
})
|
|
1317
|
-
|
|
1318
|
-
test('missing-message asks without stored seeds do not send empty replies', async () => {
|
|
1319
|
-
const pm = fakePrivateMessage()
|
|
1320
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1321
|
-
await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1322
|
-
userSigner: signer('seeder'),
|
|
1323
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
1324
|
-
})
|
|
1325
|
-
|
|
1326
|
-
await pm.watchCalls[0].onAsk({
|
|
1327
|
-
event: {
|
|
1328
|
-
id: 'question-id',
|
|
1329
|
-
kind: ASK_KIND,
|
|
1330
|
-
pubkey: 'user',
|
|
1331
|
-
created_at: now,
|
|
1332
|
-
tags: [['r', 'seeder'], ['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1333
|
-
content: JSON.stringify({ since: now - 5, until: now + 5 })
|
|
1334
|
-
},
|
|
1335
|
-
outer: { id: 'ask-outer-id', created_at: now },
|
|
1336
|
-
meta: { channelPubkey: 'channel' },
|
|
1337
|
-
payload: { code: MISSING_MESSAGES_ASK_CODE, payload: { since: now - 5, until: now + 5 } },
|
|
1338
|
-
question: { id: 'question-id' }
|
|
1339
|
-
})
|
|
1340
|
-
|
|
1341
|
-
assert.equal(pm.sent.some(sent => sent.method === 'reply' && sent.options.code === MISSING_MESSAGES_REPLY_CODE), false)
|
|
1342
|
-
})
|
|
1343
|
-
|
|
1344
|
-
test('recovery asks online seeders for the relay-uncovered left edge', async () => {
|
|
1345
|
-
const pm = fakePrivateMessage()
|
|
1346
|
-
const fetches = []
|
|
1347
|
-
let scheduled = null
|
|
1348
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1349
|
-
globalThis.localStorage.setItem('libp2r2p:private-messenger:user:state', JSON.stringify({
|
|
1350
|
-
channels: {
|
|
1351
|
-
channel: { lastSeenAt: now - 20, lastWatchedAt: now - 20 }
|
|
1352
|
-
}
|
|
1353
|
-
}))
|
|
1354
|
-
const messenger = await new PrivateMessenger({
|
|
1355
|
-
_privateMessage: pm,
|
|
1356
|
-
_privateChannel: {
|
|
1357
|
-
fetch: async options => {
|
|
1358
|
-
fetches.push(options)
|
|
1359
|
-
options.onEvent({
|
|
1360
|
-
id: 'relay-id',
|
|
1361
|
-
kind: TELL_KIND,
|
|
1362
|
-
pubkey: 'alice',
|
|
1363
|
-
created_at: now - 5,
|
|
1364
|
-
tags: [['r', 'user']],
|
|
1365
|
-
content: 'relay'
|
|
1366
|
-
}, { id: 'outer-id', created_at: now - 5 }, { channelPubkey: 'channel' })
|
|
1367
|
-
return [{ id: 'outer-id', created_at: now - 5 }]
|
|
1368
|
-
}
|
|
1369
|
-
},
|
|
1370
|
-
_setTimeout: fn => { scheduled = fn }
|
|
1371
|
-
}).init({
|
|
1372
|
-
userSigner: signer('user'),
|
|
1373
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], seeders: ['seeder'] }]
|
|
1374
|
-
})
|
|
1375
|
-
|
|
1376
|
-
pm.watchCalls[0].onYell({
|
|
1377
|
-
event: { id: 'presence-id', kind: TELL_KIND, pubkey: 'seeder', created_at: now - 2, tags: [['h', SEEDER_PRESENCE_CODE]], content: '{}' },
|
|
1378
|
-
outer: { id: 'presence-outer-id', created_at: now - 2 },
|
|
1379
|
-
meta: { channelPubkey: 'channel' },
|
|
1380
|
-
payload: { code: SEEDER_PRESENCE_CODE, payload: {} },
|
|
1381
|
-
yell: { id: 'presence-id' }
|
|
1382
|
-
})
|
|
1383
|
-
|
|
1384
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1385
|
-
|
|
1386
|
-
await scheduled()
|
|
1387
|
-
|
|
1388
|
-
const ask = pm.sent.find(sent => sent.method === 'ask' && sent.options.code === MISSING_MESSAGES_ASK_CODE)
|
|
1389
|
-
assert.equal(fetches.length, 1)
|
|
1390
|
-
assert.equal(ask.options.receiverPubkey, 'seeder')
|
|
1391
|
-
assert.ok(ask.options.payload.since <= now - 20)
|
|
1392
|
-
assert.equal(ask.options.payload.until, now - 5)
|
|
1393
|
-
assert.equal((await messenger.nextMessage()).event.id, 'relay-id')
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1396
|
-
test('recovery asks all configured seeders but caps discovered seeders', async () => {
|
|
1397
|
-
const pm = fakePrivateMessage()
|
|
1398
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1399
|
-
const configuredSeeders = Array.from({ length: 10 }, (_v, index) => `configured-${index}`)
|
|
1400
|
-
const discoveredSeeders = Array.from({ length: 12 }, (_v, index) => `discovered-${index}`)
|
|
1401
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1402
|
-
userSigner: signer('user'),
|
|
1403
|
-
channels: [
|
|
1404
|
-
{ pubkey: 'configured', signer: signer('configured'), relays: ['wss://relay.example'], seeders: configuredSeeders },
|
|
1405
|
-
{ pubkey: 'discovered', signer: signer('discovered'), relays: ['wss://relay.example'] }
|
|
1406
|
-
]
|
|
1407
|
-
})
|
|
1408
|
-
|
|
1409
|
-
for (const [index, seeder] of discoveredSeeders.entries()) {
|
|
1410
|
-
messenger.markSeederActive('discovered', seeder, { at: now - index })
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
await messenger.askSeedersForMissingRange('configured', now - 20, now - 10)
|
|
1414
|
-
await messenger.askSeedersForMissingRange('discovered', now - 20, now - 10)
|
|
1415
|
-
|
|
1416
|
-
const configuredAsks = pm.sent.filter(sent => sent.method === 'ask' && sent.options.privateChannelSigner.getPublicKey() === 'configured')
|
|
1417
|
-
const discoveredAsks = pm.sent.filter(sent => sent.method === 'ask' && sent.options.privateChannelSigner.getPublicKey() === 'discovered')
|
|
1418
|
-
|
|
1419
|
-
assert.deepEqual(configuredAsks.map(sent => sent.options.receiverPubkey), configuredSeeders)
|
|
1420
|
-
assert.deepEqual(discoveredAsks.map(sent => sent.options.receiverPubkey), discoveredSeeders.slice(0, 8))
|
|
1421
|
-
})
|
|
1422
|
-
|
|
1423
|
-
test('missing-message replies ignore raw event rows', async () => {
|
|
1424
|
-
const pm = fakePrivateMessage()
|
|
1425
|
-
const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
|
|
1426
|
-
userSigner: signer('user'),
|
|
1427
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], seeders: ['seeder'] }]
|
|
1428
|
-
})
|
|
1429
|
-
const jsonl = `${JSON.stringify({
|
|
1430
|
-
id: 'missed-id',
|
|
1431
|
-
kind: TELL_KIND,
|
|
1432
|
-
pubkey: 'alice',
|
|
1433
|
-
created_at: 1,
|
|
1434
|
-
tags: [['r', 'user']],
|
|
1435
|
-
content: 'old'
|
|
1436
|
-
})}\n`
|
|
1437
|
-
await pm.watchCalls[0].onReply({
|
|
1438
|
-
event: { id: 'reply-id', kind: REPLY_KIND, pubkey: 'seeder', created_at: 2, tags: [['q', 'question-id']], content: '' },
|
|
1439
|
-
outer: { id: 'reply-outer-id', created_at: 3 },
|
|
1440
|
-
meta: { channelPubkey: 'channel' },
|
|
1441
|
-
payload: { code: MISSING_MESSAGES_REPLY_CODE, payload: { index: 0, isLast: true, jsonl } },
|
|
1442
|
-
questionId: 'question-id',
|
|
1443
|
-
reply: { id: 'reply-id' }
|
|
1444
|
-
})
|
|
1445
|
-
|
|
1446
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1447
|
-
})
|
|
1448
|
-
|
|
1449
|
-
test('missing-message replies can recover router-only seed records', async () => {
|
|
1450
|
-
const pm = fakePrivateMessage()
|
|
1451
|
-
let unwrapCall = null
|
|
1452
|
-
let encryptedTo = null
|
|
1453
|
-
let encryptedKind = null
|
|
1454
|
-
let encryptedScope = null
|
|
1455
|
-
const channel = {
|
|
1456
|
-
...signer('channel'),
|
|
1457
|
-
nip44v3Encrypt: async (pubkey, kind, scope, content) => {
|
|
1458
|
-
encryptedTo = pubkey
|
|
1459
|
-
encryptedKind = kind
|
|
1460
|
-
encryptedScope = scope
|
|
1461
|
-
return content
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
const messenger = await new PrivateMessenger({
|
|
1465
|
-
_privateMessage: pm,
|
|
1466
|
-
_privateChannel: {
|
|
1467
|
-
unwrapEvent: async options => {
|
|
1468
|
-
unwrapCall = options
|
|
1469
|
-
return {
|
|
1470
|
-
id: 'missed-id',
|
|
1471
|
-
kind: TELL_KIND,
|
|
1472
|
-
pubkey: 'alice',
|
|
1473
|
-
created_at: 1,
|
|
1474
|
-
tags: [['r', 'user']],
|
|
1475
|
-
content: 'old'
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
}).init({
|
|
1480
|
-
userSigner: signer('user'),
|
|
1481
|
-
channels: [{ pubkey: 'channel', signer: channel, readerPubkey: 'reader', relays: ['wss://relay.example'], seeders: ['seeder'] }]
|
|
1482
|
-
})
|
|
1483
|
-
const userRow = JSON.stringify(['user', 'ciphertext'])
|
|
1484
|
-
const jsonl = `${JSON.stringify({
|
|
1485
|
-
recordType: ROUTER_SEED_RECORD_TYPE,
|
|
1486
|
-
router: {
|
|
1487
|
-
kind: 26300,
|
|
1488
|
-
pubkey: 'router',
|
|
1489
|
-
created_at: 1,
|
|
1490
|
-
tags: [['f', 'alice'], ['c', '0', '1']],
|
|
1491
|
-
content: jsonlContent(payloadRow(), userRow)
|
|
1492
|
-
}
|
|
1493
|
-
})}\n`
|
|
1494
|
-
await pm.watchCalls[0].onReply({
|
|
1495
|
-
event: { id: 'reply-id', kind: REPLY_KIND, pubkey: 'seeder', created_at: 2, tags: [['q', 'question-id']], content: '' },
|
|
1496
|
-
outer: { id: 'reply-outer-id', created_at: 3 },
|
|
1497
|
-
meta: { channelPubkey: 'channel' },
|
|
1498
|
-
payload: { code: MISSING_MESSAGES_REPLY_CODE, payload: { index: 0, isLast: true, jsonl } },
|
|
1499
|
-
questionId: 'question-id',
|
|
1500
|
-
reply: { id: 'reply-id' }
|
|
1501
|
-
})
|
|
1502
|
-
|
|
1503
|
-
const syntheticRouter = JSON.parse(Buffer.from(unwrapCall.event.content, 'base64').toString())
|
|
1504
|
-
assert.equal(encryptedTo, 'reader')
|
|
1505
|
-
assert.equal(encryptedKind, 3560)
|
|
1506
|
-
assert.equal(encryptedScope, '')
|
|
1507
|
-
assert.equal(unwrapCall.privateChannelReaderPubkey, 'reader')
|
|
1508
|
-
assert.equal(syntheticRouter.content, jsonlContent(payloadRow(), userRow))
|
|
1509
|
-
assert.deepEqual(syntheticRouter.tags, [['f', 'alice'], ['c', '0', '1']])
|
|
1510
|
-
assert.equal((await messenger.nextMessage()).event.id, 'missed-id')
|
|
1511
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1512
|
-
})
|
|
1513
|
-
|
|
1514
|
-
test('nym carrier seeds are replied to and recovered as nym queue items', async () => {
|
|
1515
|
-
const pm = fakePrivateMessage()
|
|
1516
|
-
const now = Math.floor(Date.now() / 1000)
|
|
1517
|
-
const carriers = [
|
|
1518
|
-
{
|
|
1519
|
-
id: 'carrier-id',
|
|
1520
|
-
kind: 26400,
|
|
1521
|
-
pubkey: 'nym',
|
|
1522
|
-
created_at: now,
|
|
1523
|
-
tags: [['id', 'inner-id'], ['c', '0', '1']],
|
|
1524
|
-
content: 'payload',
|
|
1525
|
-
sig: 'sig'
|
|
1526
|
-
}
|
|
1527
|
-
]
|
|
1528
|
-
const messenger = await new PrivateMessenger({
|
|
1529
|
-
_privateMessage: pm,
|
|
1530
|
-
_privateChannel: {
|
|
1531
|
-
eventFromNymCarriers: input => {
|
|
1532
|
-
assert.deepEqual(input, carriers)
|
|
1533
|
-
return { id: 'inner-id', kind: ASK_KIND, pubkey: 'inner-author', created_at: now, tags: [['r', 'user']], content: 'nym ask' }
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}).init({
|
|
1537
|
-
userSigner: signer('seeder'),
|
|
1538
|
-
channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder' }]
|
|
1539
|
-
})
|
|
1540
|
-
|
|
1541
|
-
pm.watchCalls[0].onSeed({
|
|
1542
|
-
recordType: NYM_CARRIER_SEED_RECORD_TYPE,
|
|
1543
|
-
channelPubkey: 'channel',
|
|
1544
|
-
outer: { id: 'outer-id', kind: 3560, pubkey: 'channel', created_at: now },
|
|
1545
|
-
carriers
|
|
1546
|
-
})
|
|
1547
|
-
pm.watchCalls[0].onSeed({
|
|
1548
|
-
recordType: NYM_CARRIER_SEED_RECORD_TYPE,
|
|
1549
|
-
channelPubkey: 'channel',
|
|
1550
|
-
outer: { id: 'outer-duplicate-id', kind: 3560, pubkey: 'channel', created_at: now },
|
|
1551
|
-
carriers
|
|
1552
|
-
})
|
|
1553
|
-
|
|
1554
|
-
await pm.watchCalls[0].onAsk({
|
|
1555
|
-
event: {
|
|
1556
|
-
id: 'question-id',
|
|
1557
|
-
kind: ASK_KIND,
|
|
1558
|
-
pubkey: 'user',
|
|
1559
|
-
created_at: now,
|
|
1560
|
-
tags: [['r', 'seeder'], ['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1561
|
-
content: JSON.stringify({ since: now - 5, until: now + 5 })
|
|
1562
|
-
},
|
|
1563
|
-
outer: { id: 'ask-outer-id', created_at: now },
|
|
1564
|
-
meta: { channelPubkey: 'channel' },
|
|
1565
|
-
payload: { code: MISSING_MESSAGES_ASK_CODE, payload: { since: now - 5, until: now + 5 } },
|
|
1566
|
-
question: { id: 'question-id' }
|
|
1567
|
-
})
|
|
1568
|
-
|
|
1569
|
-
const reply = pm.sent.find(sent => sent.method === 'reply' && sent.options.code === MISSING_MESSAGES_REPLY_CODE)
|
|
1570
|
-
const records = reply.options.payload.jsonl.trim().split('\n').map(line => JSON.parse(line))
|
|
1571
|
-
assert.equal(records.length, 1)
|
|
1572
|
-
assert.equal(records[0].recordType, NYM_CARRIER_SEED_RECORD_TYPE)
|
|
1573
|
-
assert.deepEqual(records[0].carriers, carriers)
|
|
1574
|
-
|
|
1575
|
-
await pm.watchCalls[0].onReply({
|
|
1576
|
-
event: { id: 'reply-id', kind: REPLY_KIND, pubkey: 'seeder', created_at: now, tags: [['q', 'question-id']], content: '' },
|
|
1577
|
-
outer: { id: 'reply-outer-id', created_at: now },
|
|
1578
|
-
meta: { channelPubkey: 'channel' },
|
|
1579
|
-
payload: { code: MISSING_MESSAGES_REPLY_CODE, payload: { index: 0, isLast: true, jsonl: reply.options.payload.jsonl } },
|
|
1580
|
-
questionId: 'question-id',
|
|
1581
|
-
reply: { id: 'reply-id' }
|
|
1582
|
-
})
|
|
1583
|
-
|
|
1584
|
-
const item = (await messenger.nextMessage())
|
|
1585
|
-
assert.equal(item.type, 'nym')
|
|
1586
|
-
assert.equal(item.event.kind, ASK_KIND)
|
|
1587
|
-
assert.equal(item.meta.recoveredFromSeeder, 'seeder')
|
|
1588
|
-
assert.equal((await messenger.nextMessage()), null)
|
|
1589
|
-
})
|
|
1590
|
-
|
|
1591
|
-
test('missing-message reply packer streams compact seed routers only', async () => {
|
|
1592
|
-
const replies = []
|
|
1593
|
-
const question = {
|
|
1594
|
-
id: 'question-id',
|
|
1595
|
-
pubkey: 'user',
|
|
1596
|
-
tags: [['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1597
|
-
content: JSON.stringify({ since: 5, until: 20 })
|
|
1598
|
-
}
|
|
1599
|
-
const packer = createMissingMessageReplyPacker({
|
|
1600
|
-
messenger: { reply: async options => replies.push(options) },
|
|
1601
|
-
channelPubkey: 'channel',
|
|
1602
|
-
question,
|
|
1603
|
-
eventsPerChunk: 1
|
|
1604
|
-
})
|
|
1605
|
-
const userRow = JSON.stringify(['user', 'ciphertext'])
|
|
1606
|
-
|
|
1607
|
-
await packer.update({
|
|
1608
|
-
id: 'event-id',
|
|
1609
|
-
kind: TELL_KIND,
|
|
1610
|
-
pubkey: 'alice',
|
|
1611
|
-
created_at: 6,
|
|
1612
|
-
tags: [['r', 'user']],
|
|
1613
|
-
content: 'first'
|
|
1614
|
-
})
|
|
1615
|
-
await packer.finalize({
|
|
1616
|
-
type: 'seed',
|
|
1617
|
-
recordType: ROUTER_SEED_RECORD_TYPE,
|
|
1618
|
-
channelPubkey: 'channel',
|
|
1619
|
-
router: {
|
|
1620
|
-
kind: 26300,
|
|
1621
|
-
pubkey: 'router',
|
|
1622
|
-
created_at: 10,
|
|
1623
|
-
tags: [['f', 'sender'], ['c', '0', '1']],
|
|
1624
|
-
content: ''
|
|
1625
|
-
},
|
|
1626
|
-
receiverPubkey: 'user',
|
|
1627
|
-
iykcPubkey: '',
|
|
1628
|
-
innerEventId: 'seeded-id',
|
|
1629
|
-
payloadRow: payloadRow(),
|
|
1630
|
-
row: userRow,
|
|
1631
|
-
firstSeenAt: 10,
|
|
1632
|
-
lastSeenAt: 10
|
|
1633
|
-
})
|
|
1634
|
-
|
|
1635
|
-
assert.equal(replies.length, 1)
|
|
1636
|
-
assert.equal(replies[0].code, MISSING_MESSAGES_REPLY_CODE)
|
|
1637
|
-
assert.equal(replies[0].receiverPubkey, 'user')
|
|
1638
|
-
assert.equal(replies[0].payload.since, 5)
|
|
1639
|
-
assert.equal(replies[0].payload.until, 20)
|
|
1640
|
-
assert.equal(replies[0].payload.isLast, true)
|
|
1641
|
-
const lines = replies[0].payload.jsonl.trim().split('\n')
|
|
1642
|
-
assert.equal(lines.length, 1)
|
|
1643
|
-
const record = JSON.parse(lines[0])
|
|
1644
|
-
assert.equal(record.recordType, ROUTER_SEED_RECORD_TYPE)
|
|
1645
|
-
assert.equal(record.router.kind, 26300)
|
|
1646
|
-
assert.equal(Buffer.from(record.router.content, 'base64').toString(), `${payloadRow()}\n${userRow}\n`)
|
|
1647
|
-
assert.deepEqual(record.router.tags, [['f', 'sender'], ['c', '0', '1']])
|
|
1648
|
-
})
|
|
1649
|
-
|
|
1650
|
-
test('missing-message reply packer skips empty replies by default', async () => {
|
|
1651
|
-
const replies = []
|
|
1652
|
-
const question = {
|
|
1653
|
-
id: 'question-id',
|
|
1654
|
-
pubkey: 'user',
|
|
1655
|
-
tags: [['h', MISSING_MESSAGES_ASK_CODE]],
|
|
1656
|
-
content: JSON.stringify({ since: 5, until: 20 })
|
|
1657
|
-
}
|
|
1658
|
-
const packer = createMissingMessageReplyPacker({
|
|
1659
|
-
messenger: { reply: async options => replies.push(options) },
|
|
1660
|
-
channelPubkey: 'channel',
|
|
1661
|
-
question
|
|
1662
|
-
})
|
|
1663
|
-
|
|
1664
|
-
await packer.finalize()
|
|
1665
|
-
|
|
1666
|
-
assert.deepEqual(replies, [])
|
|
1667
|
-
})
|
|
1668
|
-
|
|
1669
|
-
test('event reply packer streams regular event lists', async () => {
|
|
1670
|
-
const replies = []
|
|
1671
|
-
const question = { id: 'question-id', pubkey: 'peer', content: '' }
|
|
1672
|
-
const packer = createEventReplyPacker({
|
|
1673
|
-
messenger: { reply: async options => replies.push(options) },
|
|
1674
|
-
channelPubkey: 'channel',
|
|
1675
|
-
question,
|
|
1676
|
-
code: 'eventSync_test',
|
|
1677
|
-
payload: { collection: 'local-db' },
|
|
1678
|
-
eventsPerChunk: 2
|
|
1679
|
-
})
|
|
1680
|
-
|
|
1681
|
-
await packer.update({ id: 'event-1', kind: 1, pubkey: 'alice', created_at: 1, tags: [], content: 'one' })
|
|
1682
|
-
await packer.update({ id: 'event-2', kind: 1, pubkey: 'alice', created_at: 2, tags: [], content: 'two' })
|
|
1683
|
-
await packer.finalize({ id: 'event-3', kind: 1, pubkey: 'alice', created_at: 3, tags: [], content: 'three' })
|
|
1684
|
-
|
|
1685
|
-
assert.equal(replies.length, 2)
|
|
1686
|
-
assert.equal(replies[0].code, 'eventSync_test')
|
|
1687
|
-
assert.equal(replies[0].receiverPubkey, 'peer')
|
|
1688
|
-
assert.deepEqual(replies[0].payload.collection, 'local-db')
|
|
1689
|
-
assert.equal(replies[0].payload.index, 0)
|
|
1690
|
-
assert.equal(replies[0].payload.isLast, false)
|
|
1691
|
-
assert.deepEqual(replies[0].payload.jsonl.trim().split('\n').map(line => JSON.parse(line).id), ['event-1', 'event-2'])
|
|
1692
|
-
|
|
1693
|
-
assert.equal(replies[1].payload.index, 1)
|
|
1694
|
-
assert.equal(replies[1].payload.isLast, true)
|
|
1695
|
-
assert.deepEqual(replies[1].payload.jsonl.trim().split('\n').map(line => JSON.parse(line).id), ['event-3'])
|
|
1696
|
-
})
|
|
1697
|
-
|
|
1698
|
-
test('event reply packer can send configured empty replies', async () => {
|
|
1699
|
-
const replies = []
|
|
1700
|
-
const question = { id: 'question-id', pubkey: 'peer', content: '' }
|
|
1701
|
-
const packer = createEventReplyPacker({
|
|
1702
|
-
messenger: { reply: async options => replies.push(options) },
|
|
1703
|
-
channelPubkey: 'channel',
|
|
1704
|
-
question,
|
|
1705
|
-
code: 'eventSync_empty',
|
|
1706
|
-
sendEmptyReply: true
|
|
1707
|
-
})
|
|
1708
|
-
|
|
1709
|
-
await packer.finalize()
|
|
1710
|
-
|
|
1711
|
-
assert.equal(replies.length, 1)
|
|
1712
|
-
assert.equal(replies[0].payload.index, 0)
|
|
1713
|
-
assert.equal(replies[0].payload.isLast, true)
|
|
1714
|
-
assert.equal(replies[0].payload.jsonl, '')
|
|
1715
|
-
})
|
|
1716
|
-
|
|
1717
|
-
test('event reply packer still sends an empty final marker after prior chunks', async () => {
|
|
1718
|
-
const replies = []
|
|
1719
|
-
const question = { id: 'question-id', pubkey: 'peer', content: '' }
|
|
1720
|
-
const packer = createEventReplyPacker({
|
|
1721
|
-
messenger: { reply: async options => replies.push(options) },
|
|
1722
|
-
channelPubkey: 'channel',
|
|
1723
|
-
question,
|
|
1724
|
-
code: 'eventSync_marker',
|
|
1725
|
-
eventsPerChunk: 1
|
|
1726
|
-
})
|
|
1727
|
-
|
|
1728
|
-
await packer.update({ id: 'event-1', kind: 1, pubkey: 'alice', created_at: 1, tags: [], content: 'one' })
|
|
1729
|
-
await packer.finalize()
|
|
1730
|
-
|
|
1731
|
-
assert.equal(replies.length, 2)
|
|
1732
|
-
assert.equal(replies[0].payload.isLast, false)
|
|
1733
|
-
assert.equal(JSON.parse(replies[0].payload.jsonl).id, 'event-1')
|
|
1734
|
-
assert.equal(replies[1].payload.index, 1)
|
|
1735
|
-
assert.equal(replies[1].payload.isLast, true)
|
|
1736
|
-
assert.equal(replies[1].payload.jsonl, '')
|
|
1737
|
-
})
|