libp2r2p 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +20 -0
- package/README.md +129 -0
- package/base16/index.js +14 -0
- package/base64/index.js +29 -0
- package/content-key/event/index.js +85 -0
- package/content-key/index.js +1 -0
- package/content-key/services/iykc-proof.js +129 -0
- package/double-dh/index.js +166 -0
- package/ecdh/index.js +8 -0
- package/idb/index.js +64 -0
- package/idb-queue/index.js +793 -0
- package/index.js +21 -0
- package/key/index.js +139 -0
- package/network/index.js +68 -0
- package/nip44-v3/index.js +175 -0
- package/nip46/constants/index.js +5 -0
- package/nip46/helpers/frame.js +51 -0
- package/nip46/helpers/url.js +96 -0
- package/nip46/index.js +5 -0
- package/nip46/services/bunker-signer.js +49 -0
- package/nip46/services/client.js +252 -0
- package/nip46/services/server-session.js +225 -0
- package/nip46/services/transport.js +265 -0
- package/package.json +49 -0
- package/private-channel/constants/index.js +5 -0
- package/private-channel/helpers/chunk-size.js +61 -0
- package/private-channel/helpers/chunks.js +282 -0
- package/private-channel/helpers/event.js +68 -0
- package/private-channel/index.js +942 -0
- package/private-channel/services/received-chunks.js +398 -0
- package/private-message/index.js +672 -0
- package/private-messenger/constants/index.js +1 -0
- package/private-messenger/index.js +1431 -0
- package/private-messenger/recovery/index.js +316 -0
- package/relay/constants/index.js +18 -0
- package/relay/helpers/hll.js +62 -0
- package/relay/helpers/publish.js +100 -0
- package/relay/helpers/routing.js +36 -0
- package/relay/helpers/timer.js +4 -0
- package/relay/index.js +4 -0
- package/relay/services/query.js +224 -0
- package/relay/services/relay-connection.js +156 -0
- package/relay/services/relay-pool.js +908 -0
- package/temporary-storage/index.js +85 -0
- package/tests/base16.test.js +19 -0
- package/tests/base64.test.js +18 -0
- package/tests/content-key-event.test.js +48 -0
- package/tests/fixtures/nip44v3-vectors.json +1 -0
- package/tests/helpers/test-signer.js +185 -0
- package/tests/idb-queue.test.js +153 -0
- package/tests/idb.test.js +91 -0
- package/tests/nip44-v3.test.js +73 -0
- package/tests/nip46.test.js +668 -0
- package/tests/private-channel.test.js +1899 -0
- package/tests/private-message.test.js +460 -0
- package/tests/private-messenger.test.js +1715 -0
- package/tests/queries.test.js +101 -0
- package/tests/queue-parity.test.js +105 -0
- package/tests/relay-hll.test.js +32 -0
- package/tests/relay-pool.test.js +2067 -0
- package/tests/temporary-storage.test.js +89 -0
- package/tests/web-storage-queue.test.js +480 -0
- package/web-storage-queue/index.js +652 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import { generateSecretKey, getPublicKey, finalizeEvent, getEventHash, validateEvent, verifyEvent } from 'nostr-tools'
|
|
2
|
+
import { bytesToBase64, base64ToBytes } from '../base64/index.js'
|
|
3
|
+
import { hexToBytes } from '../base16/index.js'
|
|
4
|
+
import { makeContentKeyEventForPubkey, parseContentKeyEvent, verifyContentKeyProof, verifyIykcProof } from '../content-key/event/index.js'
|
|
5
|
+
import { getIykcProofs } from '../content-key/index.js'
|
|
6
|
+
import * as nip44v3 from '../nip44-v3/index.js'
|
|
7
|
+
import { relayPool } from '../relay/index.js'
|
|
8
|
+
import { JSONL_CHUNK_BYTES, NYM_CARRIER_CHUNK_CHARS } from './helpers/chunk-size.js'
|
|
9
|
+
import {
|
|
10
|
+
cleanupChunks,
|
|
11
|
+
cleanupEnvelopeRows,
|
|
12
|
+
decodeChunkLines,
|
|
13
|
+
prepareEnvelopeRows,
|
|
14
|
+
preparedRowIndexesForReceivers,
|
|
15
|
+
readChunkContent,
|
|
16
|
+
receiverPubkeys,
|
|
17
|
+
receiverPubkeysWithoutContentKeys,
|
|
18
|
+
writeChunksFromPreparedRows
|
|
19
|
+
} from './helpers/chunks.js'
|
|
20
|
+
import { EXPIRATION_SECONDS, MAX_EVENT_BYTES, NYM_CARRIER_KIND, PRIVATE_BROADCAST_KIND, ROUTER_KIND } from './constants/index.js'
|
|
21
|
+
import { eventByteLength, hasImkcTag, makeNymCarrierEvent, makeRouterEvent, nowSeconds, readChunkTag, readIdTag, readImkcProof, readImkcTag, readReceiverTag, readSenderTag } from './helpers/event.js'
|
|
22
|
+
import { createReceivedChunkStore, DEFAULT_RECEIVED_CHUNK_MAX_BYTES, DEFAULT_RECEIVED_CHUNK_TTL_MS } from './services/received-chunks.js'
|
|
23
|
+
|
|
24
|
+
export { EXPIRATION_SECONDS, MAX_EVENT_BYTES, NYM_CARRIER_KIND, PRIVATE_BROADCAST_KIND, ROUTER_KIND } from './constants/index.js'
|
|
25
|
+
|
|
26
|
+
const DEFAULT_IGNORED_GROUP_TTL_MS = 30 * 60 * 1000
|
|
27
|
+
const DEFAULT_IGNORED_GROUP_MAX_ENTRIES = 5000
|
|
28
|
+
const HEX_SECKEY = /^[0-9a-f]{64}$/i
|
|
29
|
+
const HEX_PUBKEY = /^[0-9a-f]{64}$/i
|
|
30
|
+
const encoder = new TextEncoder()
|
|
31
|
+
const decoder = new TextDecoder()
|
|
32
|
+
const NIP44_V3_SCOPE = ''
|
|
33
|
+
|
|
34
|
+
const sendToRelays = (...args) => relayPool.sendEvent(...args)
|
|
35
|
+
const getEvents = (...args) => relayPool.getEvents(...args)
|
|
36
|
+
const getLiveEventsGenerator = (...args) => relayPool.getLiveEventsGenerator(...args)
|
|
37
|
+
const getEventsFeedGenerator = (...args) => relayPool.getEventsFeedGenerator(...args)
|
|
38
|
+
|
|
39
|
+
function uniq (values) {
|
|
40
|
+
return [...new Set((values || []).filter(Boolean))]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeDeletionPubkey (deletionPubkey) {
|
|
44
|
+
if (deletionPubkey === undefined) return undefined
|
|
45
|
+
if (typeof deletionPubkey !== 'string' || !HEX_PUBKEY.test(deletionPubkey)) {
|
|
46
|
+
throw new Error('INVALID_DELETION_PUBKEY')
|
|
47
|
+
}
|
|
48
|
+
return deletionPubkey.toLowerCase()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function privateBroadcastTags ({ deletionPubkey, createdAt, expirationSeconds }) {
|
|
52
|
+
return [
|
|
53
|
+
...(deletionPubkey ? [['s', deletionPubkey]] : []),
|
|
54
|
+
['expiration', String(createdAt + expirationSeconds)]
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getJsonlChunkByteSize () {
|
|
59
|
+
return JSONL_CHUNK_BYTES
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getNymCarrierChunkSize () {
|
|
63
|
+
return NYM_CARRIER_CHUNK_CHARS
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function textToBase64 (text) {
|
|
67
|
+
return bytesToBase64(encoder.encode(text))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function base64ToText (b64) {
|
|
71
|
+
return decoder.decode(base64ToBytes(b64))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function nip44v3EncryptText (signer, peerPubkey, kind, plaintext) {
|
|
75
|
+
return signer.nip44v3Encrypt(peerPubkey, kind, NIP44_V3_SCOPE, textToBase64(plaintext))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function nip44v3DecryptText (signer, peerPubkey, kind, ciphertext) {
|
|
79
|
+
return base64ToText(await signer.nip44v3Decrypt(peerPubkey, kind, NIP44_V3_SCOPE, ciphertext))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function storesRecoverySeeds (mode) {
|
|
83
|
+
return mode === 'seeder' || mode === 'watchtower'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function makeImkcProof ({ senderSigner, senderPubkey, imkcPubkey }) {
|
|
87
|
+
const event = await makeContentKeyEventForPubkey({ userSigner: senderSigner, contentPubkey: imkcPubkey })
|
|
88
|
+
const parsed = parseContentKeyEvent(event)
|
|
89
|
+
if (
|
|
90
|
+
event.pubkey !== senderPubkey ||
|
|
91
|
+
parsed?.iykcPubkey !== imkcPubkey ||
|
|
92
|
+
!verifyContentKeyProof({ ownerPubkey: senderPubkey, contentPubkey: imkcPubkey, proof: parsed?.iykcProof })
|
|
93
|
+
) throw new Error('INVALID_IMKC_PROOF')
|
|
94
|
+
return parsed.iykcProof
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function prepareRoutedMessage ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, event, temporaryStorageArea, _getIykcProofs = getIykcProofs }) {
|
|
98
|
+
if (!senderSigner?.getPublicKey) throw new Error('SENDER_SIGNER_REQUIRED')
|
|
99
|
+
if (!senderSigner?.nip44EncryptDoubleDH && !senderSigner?.nip44v3Encrypt) throw new Error('SIGNER_NIP44V3_ENCRYPT_UNSUPPORTED')
|
|
100
|
+
if (!privateChannelSigner?.getPublicKey || !privateChannelSigner?.nip44v3Encrypt || !privateChannelSigner?.signEvent) throw new Error('PRIVATE_CHANNEL_SIGNER_REQUIRED')
|
|
101
|
+
if (!Array.isArray(receivers) || !receivers.length) throw new Error('NO_RECEIVERS')
|
|
102
|
+
|
|
103
|
+
const senderPubkey = await senderSigner.getPublicKey()
|
|
104
|
+
const useDoubleDh = typeof senderSigner.nip44EncryptDoubleDH === 'function'
|
|
105
|
+
const channelPubkey = await privateChannelSigner.getPublicKey()
|
|
106
|
+
const channelReaderPubkey = privateChannelReaderPubkey || channelPubkey
|
|
107
|
+
const receiverContentKeys = useDoubleDh ? await _getIykcProofs(receiverPubkeysWithoutContentKeys(receivers)) : {}
|
|
108
|
+
const preparedRows = await prepareEnvelopeRows({
|
|
109
|
+
senderSigner,
|
|
110
|
+
imkcSigner: useDoubleDh ? imkcSigner : null,
|
|
111
|
+
receivers,
|
|
112
|
+
receiverContentKeys,
|
|
113
|
+
event,
|
|
114
|
+
rowScope: channelPubkey,
|
|
115
|
+
temporaryStorageArea
|
|
116
|
+
})
|
|
117
|
+
const imkcPubkey = preparedRows.ownContentPubkey || ''
|
|
118
|
+
const imkcProof = imkcPubkey ? await makeImkcProof({ senderSigner, senderPubkey, imkcPubkey }) : ''
|
|
119
|
+
return {
|
|
120
|
+
senderPubkey,
|
|
121
|
+
channelPubkey,
|
|
122
|
+
channelReaderPubkey,
|
|
123
|
+
preparedRows,
|
|
124
|
+
imkcPubkey,
|
|
125
|
+
imkcProof
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function * wrapPreparedEvents ({ privateChannelSigner, receivers, receiverTag, deletionPubkey, expirationSeconds = EXPIRATION_SECONDS, context }) {
|
|
130
|
+
const routerSeckey = generateSecretKey()
|
|
131
|
+
const routerPubkey = getPublicKey(routerSeckey)
|
|
132
|
+
const receiverPubkeyList = receiverPubkeys(receivers)
|
|
133
|
+
const routerReceiverTag = receiverTag ?? (receiverPubkeyList.length === 1 ? receiverPubkeyList[0] : '')
|
|
134
|
+
const rowIndexes = preparedRowIndexesForReceivers(context.preparedRows, receivers)
|
|
135
|
+
const {
|
|
136
|
+
id,
|
|
137
|
+
total
|
|
138
|
+
} = writeChunksFromPreparedRows(context.preparedRows, rowIndexes)
|
|
139
|
+
const temporaryStorage = context.preparedRows.temporaryStorage
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
for (let index = 0; index < total; index++) {
|
|
143
|
+
const content = readChunkContent(id, index, temporaryStorage)
|
|
144
|
+
const router = finalizeEvent(makeRouterEvent({
|
|
145
|
+
pubkey: routerPubkey,
|
|
146
|
+
senderPubkey: context.senderPubkey,
|
|
147
|
+
imkcPubkey: context.imkcPubkey,
|
|
148
|
+
imkcProof: context.imkcProof,
|
|
149
|
+
receiverPubkey: routerReceiverTag,
|
|
150
|
+
chunkIndex: index,
|
|
151
|
+
chunkTotal: total,
|
|
152
|
+
content
|
|
153
|
+
}), routerSeckey)
|
|
154
|
+
const createdAt = nowSeconds()
|
|
155
|
+
const outer = await privateChannelSigner.signEvent({
|
|
156
|
+
kind: PRIVATE_BROADCAST_KIND,
|
|
157
|
+
created_at: createdAt,
|
|
158
|
+
tags: privateBroadcastTags({ deletionPubkey, createdAt, expirationSeconds }),
|
|
159
|
+
content: await nip44v3EncryptText(privateChannelSigner, context.channelReaderPubkey, PRIVATE_BROADCAST_KIND, JSON.stringify(router))
|
|
160
|
+
})
|
|
161
|
+
if (eventByteLength(outer) > MAX_EVENT_BYTES) throw new Error('EVENT_TOO_LARGE')
|
|
162
|
+
yield outer
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
cleanupChunks(id, total, temporaryStorage)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Streaming version of wrapEvent
|
|
170
|
+
export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS, temporaryStorageArea, _getIykcProofs = getIykcProofs }) {
|
|
171
|
+
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
172
|
+
const context = await prepareRoutedMessage({
|
|
173
|
+
senderSigner,
|
|
174
|
+
imkcSigner,
|
|
175
|
+
privateChannelSigner,
|
|
176
|
+
privateChannelReaderPubkey,
|
|
177
|
+
receivers,
|
|
178
|
+
event,
|
|
179
|
+
temporaryStorageArea,
|
|
180
|
+
_getIykcProofs
|
|
181
|
+
})
|
|
182
|
+
try {
|
|
183
|
+
yield * wrapPreparedEvents({ privateChannelSigner, receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })
|
|
184
|
+
} finally {
|
|
185
|
+
cleanupEnvelopeRows(context.preparedRows)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function wrapEvent (options) {
|
|
190
|
+
const events = []
|
|
191
|
+
for await (const event of wrapEvents(options)) events.push(event)
|
|
192
|
+
return events
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function * wrapNymEvents ({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS }) {
|
|
196
|
+
if (!nymSigner?.getPublicKey || !nymSigner?.signEvent) throw new Error('NYM_SIGNER_REQUIRED')
|
|
197
|
+
if (!privateChannelSigner?.getPublicKey || !privateChannelSigner?.nip44v3Encrypt || !privateChannelSigner?.signEvent) throw new Error('PRIVATE_CHANNEL_SIGNER_REQUIRED')
|
|
198
|
+
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
199
|
+
|
|
200
|
+
const nymPubkey = await nymSigner.getPublicKey()
|
|
201
|
+
const channelPubkey = await privateChannelSigner.getPublicKey()
|
|
202
|
+
const channelReaderPubkey = privateChannelReaderPubkey || channelPubkey
|
|
203
|
+
const wireEvent = isSignedEvent(event)
|
|
204
|
+
? assertValidSignedInnerEvent(event)
|
|
205
|
+
: wireNymRumor({ ...event, created_at: event?.created_at !== undefined ? event.created_at : nowSeconds() })
|
|
206
|
+
const innerEvent = isSignedEvent(wireEvent) ? wireEvent : normalizeNymRumor(wireEvent, nymPubkey)
|
|
207
|
+
const encoded = bytesToBase64(encoder.encode(JSON.stringify(wireEvent)))
|
|
208
|
+
const total = Math.max(1, Math.ceil(encoded.length / NYM_CARRIER_CHUNK_CHARS))
|
|
209
|
+
const carrierCreatedAt = nowSeconds()
|
|
210
|
+
|
|
211
|
+
for (let index = 0; index < total; index++) {
|
|
212
|
+
const carrier = assertValidNymCarrierEvent(await nymSigner.signEvent(makeNymCarrierEvent({
|
|
213
|
+
innerId: innerEvent.id,
|
|
214
|
+
chunkIndex: index,
|
|
215
|
+
chunkTotal: total,
|
|
216
|
+
content: encoded.slice(index * NYM_CARRIER_CHUNK_CHARS, (index + 1) * NYM_CARRIER_CHUNK_CHARS),
|
|
217
|
+
createdAt: carrierCreatedAt
|
|
218
|
+
})))
|
|
219
|
+
const createdAt = nowSeconds()
|
|
220
|
+
const outer = await privateChannelSigner.signEvent({
|
|
221
|
+
kind: PRIVATE_BROADCAST_KIND,
|
|
222
|
+
created_at: createdAt,
|
|
223
|
+
tags: privateBroadcastTags({ deletionPubkey: normalizedDeletionPubkey, createdAt, expirationSeconds }),
|
|
224
|
+
content: await nip44v3EncryptText(privateChannelSigner, channelReaderPubkey, PRIVATE_BROADCAST_KIND, JSON.stringify(carrier))
|
|
225
|
+
})
|
|
226
|
+
if (eventByteLength(outer) > MAX_EVENT_BYTES) throw new Error('EVENT_TOO_LARGE')
|
|
227
|
+
yield outer
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function wrapNymEvent (options) {
|
|
232
|
+
const events = []
|
|
233
|
+
for await (const event of wrapNymEvents(options)) events.push(event)
|
|
234
|
+
return events
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function joinedRouter (router, content = '') {
|
|
238
|
+
return {
|
|
239
|
+
...router,
|
|
240
|
+
content,
|
|
241
|
+
tags: router.tags.filter(t => t[0] !== 'c').concat([['c', '0', '1']])
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parsePayloadEnvelope (line, index = 0) {
|
|
246
|
+
const record = JSON.parse(line)
|
|
247
|
+
if (!Array.isArray(record) || record.length !== 1 || typeof record[0] !== 'string') throw new Error('INVALID_PAYLOAD_ENVELOPE')
|
|
248
|
+
return { index, type: 'payload', ciphertext: record[0] }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseRecipientEnvelope (line, index = 0) {
|
|
252
|
+
const record = JSON.parse(line)
|
|
253
|
+
if (!Array.isArray(record) || (record.length !== 2 && record.length !== 4)) throw new Error('INVALID_RECIPIENT_ENVELOPE')
|
|
254
|
+
const [receiverPubkey, ciphertext, iykcPubkey = '', iykcProof = ''] = record
|
|
255
|
+
return { index, receiverPubkey, ciphertext, iykcPubkey, iykcProof }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isSignedEvent (event) {
|
|
259
|
+
return Object.prototype.hasOwnProperty.call(event || {}, 'sig')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function assertValidSignedInnerEvent (event) {
|
|
263
|
+
if (!validateEvent(event) || event.id !== getEventHash(event) || !verifyEvent(event)) {
|
|
264
|
+
throw new Error('INVALID_SIGNED_INNER_EVENT')
|
|
265
|
+
}
|
|
266
|
+
return event
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeNymRumor (event, pubkey) {
|
|
270
|
+
const normalized = { ...event, pubkey }
|
|
271
|
+
if (!validateEvent(normalized)) throw new Error('INVALID_NYM_RUMOR')
|
|
272
|
+
return { ...normalized, id: getEventHash(normalized) }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function wireNymRumor (event = {}) {
|
|
276
|
+
return {
|
|
277
|
+
kind: event.kind,
|
|
278
|
+
tags: event.tags,
|
|
279
|
+
content: event.content,
|
|
280
|
+
created_at: event.created_at
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function assertValidNymCarrierEvent (carrier) {
|
|
285
|
+
if (!validateEvent(carrier) || carrier.id !== getEventHash(carrier) || !verifyEvent(carrier)) {
|
|
286
|
+
throw new Error('INVALID_NYM_CARRIER')
|
|
287
|
+
}
|
|
288
|
+
if (carrier.kind !== NYM_CARRIER_KIND) throw new Error('INVALID_NYM_CARRIER_KIND')
|
|
289
|
+
if (!readIdTag(carrier)) throw new Error('MISSING_NYM_CARRIER_ID')
|
|
290
|
+
readChunkTag(carrier)
|
|
291
|
+
return carrier
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function nymCarrierGroupId (carrier) {
|
|
295
|
+
return `nym:${carrier.pubkey}:${readIdTag(carrier)}:${readChunkTag(carrier).total}`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateNymCarriers (carriers) {
|
|
299
|
+
if (!Array.isArray(carriers) || !carriers.length) throw new Error('NYM_CARRIERS_REQUIRED')
|
|
300
|
+
const chunks = []
|
|
301
|
+
let nymPubkey = ''
|
|
302
|
+
let innerId = ''
|
|
303
|
+
let total = 0
|
|
304
|
+
|
|
305
|
+
for (const carrier of carriers) {
|
|
306
|
+
assertValidNymCarrierEvent(carrier)
|
|
307
|
+
const nextInnerId = readIdTag(carrier)
|
|
308
|
+
const { index, total: nextTotal } = readChunkTag(carrier)
|
|
309
|
+
if (!nymPubkey) {
|
|
310
|
+
nymPubkey = carrier.pubkey
|
|
311
|
+
innerId = nextInnerId
|
|
312
|
+
total = nextTotal
|
|
313
|
+
}
|
|
314
|
+
if (carrier.pubkey !== nymPubkey || nextInnerId !== innerId || nextTotal !== total) {
|
|
315
|
+
throw new Error('MISMATCHED_NYM_CARRIER_CHUNKS')
|
|
316
|
+
}
|
|
317
|
+
if (chunks[index] !== undefined) throw new Error('DUPLICATE_NYM_CARRIER_CHUNK')
|
|
318
|
+
chunks[index] = carrier.content
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (chunks.length !== total) throw new Error('MISSING_NYM_CARRIER_CHUNK')
|
|
322
|
+
for (let index = 0; index < total; index++) {
|
|
323
|
+
if (chunks[index] == null) throw new Error('MISSING_NYM_CARRIER_CHUNK')
|
|
324
|
+
}
|
|
325
|
+
return { nymPubkey, innerId, content: chunks.join('') }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function eventFromNymCarriers (carriers) {
|
|
329
|
+
const { nymPubkey, innerId, content } = validateNymCarriers(carriers)
|
|
330
|
+
let parsed
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(decoder.decode(base64ToBytes(content)))
|
|
333
|
+
} catch {
|
|
334
|
+
throw new Error('INVALID_NYM_CARRIER_PAYLOAD')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (isSignedEvent(parsed)) {
|
|
338
|
+
const event = assertValidSignedInnerEvent(parsed)
|
|
339
|
+
if (event.id !== innerId) throw new Error('INVALID_NYM_CARRIER_INNER_ID')
|
|
340
|
+
return event
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const event = normalizeNymRumor(parsed, nymPubkey)
|
|
344
|
+
if (event.id !== innerId) throw new Error('INVALID_NYM_CARRIER_INNER_ID')
|
|
345
|
+
return event
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assertValidEnvelopeIykcProof (envelope) {
|
|
349
|
+
if (!envelope.iykcPubkey) return
|
|
350
|
+
if (!verifyIykcProof({
|
|
351
|
+
receiverPubkey: envelope.receiverPubkey,
|
|
352
|
+
iykcPubkey: envelope.iykcPubkey,
|
|
353
|
+
iykcProof: envelope.iykcProof
|
|
354
|
+
})) throw new Error('INVALID_IYKC_PROOF')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function assertValidRouterImkcProof ({ router, senderPubkey, imkcPubkey, imkcProof }) {
|
|
358
|
+
if (!hasImkcTag(router)) return
|
|
359
|
+
if (!verifyContentKeyProof({
|
|
360
|
+
ownerPubkey: senderPubkey,
|
|
361
|
+
contentPubkey: imkcPubkey,
|
|
362
|
+
proof: imkcProof
|
|
363
|
+
})) throw new Error('INVALID_IMKC_PROOF')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function eventFromPayload ({ payloadCiphertext, messageSeckey, senderPubkey }) {
|
|
367
|
+
if (!HEX_SECKEY.test(messageSeckey || '')) throw new Error('INVALID_MESSAGE_SECKEY')
|
|
368
|
+
const messageSecretKey = hexToBytes(messageSeckey)
|
|
369
|
+
const messagePubkey = getPublicKey(messageSecretKey)
|
|
370
|
+
const decrypted = JSON.parse(nip44v3.decrypt(messageSecretKey, messagePubkey, ROUTER_KIND, NIP44_V3_SCOPE, payloadCiphertext))
|
|
371
|
+
if (isSignedEvent(decrypted)) return assertValidSignedInnerEvent(decrypted)
|
|
372
|
+
const normalized = { ...decrypted, pubkey: senderPubkey }
|
|
373
|
+
return { ...normalized, id: getEventHash(normalized) }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function unwrapRecipientEnvelope ({ payloadCiphertext, envelope, receiverSigner, receiverPubkey, senderPubkey, imkcPubkey, rowScope = '' }) {
|
|
377
|
+
if (receiverPubkey && envelope.receiverPubkey !== receiverPubkey) return null
|
|
378
|
+
let messageSeckey
|
|
379
|
+
if (envelope.iykcPubkey || imkcPubkey) {
|
|
380
|
+
if (!receiverSigner?.nip44DecryptDoubleDH) throw new Error('RECEIVER_DOUBLE_DH_UNSUPPORTED')
|
|
381
|
+
if (envelope.iykcPubkey) {
|
|
382
|
+
assertValidEnvelopeIykcProof(envelope)
|
|
383
|
+
}
|
|
384
|
+
messageSeckey = base64ToText(await receiverSigner.nip44DecryptDoubleDH(
|
|
385
|
+
senderPubkey,
|
|
386
|
+
ROUTER_KIND,
|
|
387
|
+
rowScope,
|
|
388
|
+
envelope.ciphertext,
|
|
389
|
+
imkcPubkey,
|
|
390
|
+
envelope.iykcPubkey || ''
|
|
391
|
+
))
|
|
392
|
+
} else {
|
|
393
|
+
if (!receiverSigner?.nip44v3Decrypt) throw new Error('RECEIVER_SIGNER_NIP44V3_DECRYPT_UNSUPPORTED')
|
|
394
|
+
messageSeckey = base64ToText(await receiverSigner.nip44v3Decrypt(senderPubkey, ROUTER_KIND, rowScope, envelope.ciphertext))
|
|
395
|
+
}
|
|
396
|
+
return eventFromPayload({ payloadCiphertext, messageSeckey, senderPubkey })
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function unwrapEvent ({ receiverSigner, privateChannelSigner = receiverSigner, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderPubkey, event, receiverPubkey }) {
|
|
400
|
+
if (!event || event.kind !== PRIVATE_BROADCAST_KIND) return null
|
|
401
|
+
if (!receiverSigner?.nip44DecryptDoubleDH && !receiverSigner?.nip44v3Decrypt) throw new Error('RECEIVER_SIGNER_NIP44V3_DECRYPT_UNSUPPORTED')
|
|
402
|
+
const channelReaderSigner = privateChannelReaderSigner || privateChannelSigner
|
|
403
|
+
if (!channelReaderSigner?.nip44v3Decrypt) throw new Error('PRIVATE_CHANNEL_READER_REQUIRED')
|
|
404
|
+
|
|
405
|
+
const channelPubkey = event.pubkey || await privateChannelSigner?.getPublicKey?.()
|
|
406
|
+
if (!channelPubkey) throw new Error('PRIVATE_CHANNEL_PUBKEY_REQUIRED')
|
|
407
|
+
const router = await decryptRouter({
|
|
408
|
+
content: event.content,
|
|
409
|
+
channelPubkey,
|
|
410
|
+
channelSigner: privateChannelSigner,
|
|
411
|
+
channelReaderSigner,
|
|
412
|
+
channelReaderPubkey: privateChannelReaderPubkey
|
|
413
|
+
})
|
|
414
|
+
if (router.kind !== ROUTER_KIND) throw new Error('INVALID_ROUTER_KIND')
|
|
415
|
+
if (receiverPubkey && readReceiverTag(router) && readReceiverTag(router) !== receiverPubkey) return null
|
|
416
|
+
|
|
417
|
+
const senderPubkey = readSenderTag(router)
|
|
418
|
+
const imkcPubkey = readImkcTag(router)
|
|
419
|
+
assertValidRouterImkcProof({ router, senderPubkey, imkcPubkey, imkcProof: readImkcProof(router) })
|
|
420
|
+
const lines = decodeChunkLines(router.content)
|
|
421
|
+
if (!lines.length) throw new Error('MISSING_PAYLOAD_ENVELOPE')
|
|
422
|
+
const payload = parsePayloadEnvelope(lines[0], 0)
|
|
423
|
+
for (let index = 1; index < lines.length; index++) {
|
|
424
|
+
const event = await unwrapRecipientEnvelope({
|
|
425
|
+
payloadCiphertext: payload.ciphertext,
|
|
426
|
+
envelope: parseRecipientEnvelope(lines[index], index),
|
|
427
|
+
receiverSigner,
|
|
428
|
+
receiverPubkey,
|
|
429
|
+
senderPubkey,
|
|
430
|
+
imkcPubkey,
|
|
431
|
+
rowScope: channelPubkey
|
|
432
|
+
})
|
|
433
|
+
if (event) return event
|
|
434
|
+
}
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function receiverPubkeyFor (receiver) {
|
|
439
|
+
return receiverPubkeys([receiver])[0] || ''
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function relayReceiverEntries (relayToReceivers) {
|
|
443
|
+
if (!relayToReceivers) return []
|
|
444
|
+
if (relayToReceivers instanceof Map) return [...relayToReceivers.entries()]
|
|
445
|
+
if (typeof relayToReceivers === 'object') return Object.entries(relayToReceivers)
|
|
446
|
+
throw new Error('INVALID_RELAY_RECEIVERS')
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function groupedRelayReceivers ({ relayToReceivers, receivers }) {
|
|
450
|
+
const entries = relayReceiverEntries(relayToReceivers)
|
|
451
|
+
if (!entries.length) return null
|
|
452
|
+
|
|
453
|
+
const receiverByPubkey = new Map()
|
|
454
|
+
const orderedPubkeys = []
|
|
455
|
+
for (const receiver of receivers || []) {
|
|
456
|
+
const pubkey = receiverPubkeyFor(receiver)
|
|
457
|
+
if (!pubkey || receiverByPubkey.has(pubkey)) continue
|
|
458
|
+
receiverByPubkey.set(pubkey, receiver)
|
|
459
|
+
orderedPubkeys.push(pubkey)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const wanted = new Set(orderedPubkeys)
|
|
463
|
+
const covered = new Set()
|
|
464
|
+
const groupsByKey = new Map()
|
|
465
|
+
for (const [relay, value] of entries) {
|
|
466
|
+
if (!relay) continue
|
|
467
|
+
const pubkeys = uniq(Array.isArray(value) ? value : [value])
|
|
468
|
+
if (!pubkeys.length) continue
|
|
469
|
+
for (const pubkey of pubkeys) {
|
|
470
|
+
if (!wanted.has(pubkey)) throw new Error('RELAY_RECEIVER_NOT_REQUESTED')
|
|
471
|
+
covered.add(pubkey)
|
|
472
|
+
}
|
|
473
|
+
const key = [...pubkeys].sort().join(',')
|
|
474
|
+
if (!groupsByKey.has(key)) {
|
|
475
|
+
const set = new Set(pubkeys)
|
|
476
|
+
groupsByKey.set(key, {
|
|
477
|
+
pubkeys: set,
|
|
478
|
+
relays: []
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
groupsByKey.get(key).relays.push(relay)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!groupsByKey.size) throw new Error('NO_RELAYS')
|
|
485
|
+
for (const pubkey of orderedPubkeys) {
|
|
486
|
+
if (!covered.has(pubkey)) throw new Error('RELAY_RECEIVER_MISSING')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return [...groupsByKey.values()].map(group => ({
|
|
490
|
+
relays: uniq(group.relays),
|
|
491
|
+
receivers: orderedPubkeys
|
|
492
|
+
.filter(pubkey => group.pubkeys.has(pubkey))
|
|
493
|
+
.map(pubkey => receiverByPubkey.get(pubkey))
|
|
494
|
+
}))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function relaysFromRelayReceivers (relayToReceivers) {
|
|
498
|
+
return uniq(relayReceiverEntries(relayToReceivers).map(([relay]) => relay))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function withRecoveryRelays (relays, recoveryRelays) {
|
|
502
|
+
return uniq([...(relays || []), ...(recoveryRelays || [])])
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export async function publish ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs = getIykcProofs, _publish = sendToRelays }) {
|
|
506
|
+
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
507
|
+
const results = []
|
|
508
|
+
// Relays are grouped only when they have the exact same recipient pubkey set
|
|
509
|
+
const groups = groupedRelayReceivers({ relayToReceivers, receivers })
|
|
510
|
+
if (groups) {
|
|
511
|
+
const context = await prepareRoutedMessage({
|
|
512
|
+
senderSigner,
|
|
513
|
+
imkcSigner,
|
|
514
|
+
privateChannelSigner,
|
|
515
|
+
privateChannelReaderPubkey,
|
|
516
|
+
receivers,
|
|
517
|
+
event,
|
|
518
|
+
temporaryStorageArea,
|
|
519
|
+
_getIykcProofs
|
|
520
|
+
})
|
|
521
|
+
try {
|
|
522
|
+
for (const group of groups) {
|
|
523
|
+
for await (const wrappedEvent of wrapPreparedEvents({ privateChannelSigner, receivers: group.receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context })) {
|
|
524
|
+
results.push(await _publish(wrappedEvent, withRecoveryRelays(group.relays, recoveryRelays)))
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} finally {
|
|
528
|
+
cleanupEnvelopeRows(context.preparedRows)
|
|
529
|
+
}
|
|
530
|
+
return { results }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for await (const wrappedEvent of wrapEvents({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds, temporaryStorageArea, _getIykcProofs })) {
|
|
534
|
+
results.push(await _publish(wrappedEvent, withRecoveryRelays(relays, recoveryRelays)))
|
|
535
|
+
}
|
|
536
|
+
return { results }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export async function publishNymEvent ({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish = sendToRelays }) {
|
|
540
|
+
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
541
|
+
const results = []
|
|
542
|
+
const publishRelays = withRecoveryRelays(relayToReceivers ? relaysFromRelayReceivers(relayToReceivers) : relays, recoveryRelays)
|
|
543
|
+
for await (const wrappedEvent of wrapNymEvents({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds })) {
|
|
544
|
+
results.push(await _publish(wrappedEvent, publishRelays))
|
|
545
|
+
}
|
|
546
|
+
return { results }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function readSignerFromMap (signersByPubkey, pubkey) {
|
|
550
|
+
if (!signersByPubkey || !pubkey) return null
|
|
551
|
+
if (signersByPubkey instanceof Map) return signersByPubkey.get(pubkey) || null
|
|
552
|
+
return signersByPubkey[pubkey] || null
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function decryptRouter ({ content, channelPubkey, channelSigner, channelReaderSigner, channelReaderPubkey }) {
|
|
556
|
+
const signer = channelReaderSigner || channelSigner
|
|
557
|
+
if (!signer?.nip44v3Decrypt) throw new Error('PRIVATE_CHANNEL_READER_REQUIRED')
|
|
558
|
+
|
|
559
|
+
const readerPubkey = channelReaderPubkey || channelPubkey
|
|
560
|
+
const signerPubkey = await signer.getPublicKey?.()
|
|
561
|
+
// Writer-side reads use writer secret + reader pubkey; reader-side reads use reader secret + channel pubkey.
|
|
562
|
+
const isWriterSide = readerPubkey !== channelPubkey && (signer === channelSigner || signerPubkey === channelPubkey)
|
|
563
|
+
const peerPubkey = isWriterSide ? readerPubkey : channelPubkey
|
|
564
|
+
return JSON.parse(await nip44v3DecryptText(signer, peerPubkey, PRIVATE_BROADCAST_KIND, content))
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function readValueFromMap (map, key) {
|
|
568
|
+
if (!map || !key) return null
|
|
569
|
+
if (map instanceof Map) return map.get(key) || null
|
|
570
|
+
return map[key] || null
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function privateChannelPubkeyList ({ privateChannelPubkey, privateChannelPubkeys }) {
|
|
574
|
+
return [...new Set([
|
|
575
|
+
...(privateChannelPubkeys || []),
|
|
576
|
+
...(privateChannelPubkey ? [privateChannelPubkey] : [])
|
|
577
|
+
].filter(Boolean))]
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function createTtlSet ({ ttlMs, maxEntries }) {
|
|
581
|
+
const entries = new Map()
|
|
582
|
+
|
|
583
|
+
function prune (now = Date.now()) {
|
|
584
|
+
if (Number.isFinite(ttlMs)) {
|
|
585
|
+
for (const [key, expiresAt] of entries) {
|
|
586
|
+
if (expiresAt > now) break
|
|
587
|
+
entries.delete(key)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (Number.isFinite(maxEntries)) {
|
|
591
|
+
while (entries.size > maxEntries) entries.delete(entries.keys().next().value)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
add (key) {
|
|
597
|
+
if (!key || maxEntries <= 0 || ttlMs <= 0) return
|
|
598
|
+
const now = Date.now()
|
|
599
|
+
const expiresAt = Number.isFinite(ttlMs) ? now + ttlMs : Infinity
|
|
600
|
+
if (entries.has(key)) entries.delete(key)
|
|
601
|
+
entries.set(key, expiresAt)
|
|
602
|
+
prune(now)
|
|
603
|
+
},
|
|
604
|
+
has (key) {
|
|
605
|
+
prune()
|
|
606
|
+
return entries.has(key)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function contentKeyUsageBase ({ outer, router, channelPubkey, receiverPubkeys = [] }) {
|
|
612
|
+
const senderPubkey = readSenderTag(router)
|
|
613
|
+
const routerReceiverPubkey = readReceiverTag(router)
|
|
614
|
+
return {
|
|
615
|
+
outer,
|
|
616
|
+
router,
|
|
617
|
+
channelPubkey,
|
|
618
|
+
senderPubkey,
|
|
619
|
+
routerReceiverPubkey,
|
|
620
|
+
receiverPubkeys,
|
|
621
|
+
isBroadcast: !routerReceiverPubkey
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function emitSentContentKeyUsage ({ outer, router, channelPubkey, receiverPubkey, receiverPubkeys, onContentKeyUsage }) {
|
|
626
|
+
if (!receiverPubkey || !onContentKeyUsage) return
|
|
627
|
+
|
|
628
|
+
const base = contentKeyUsageBase({ outer, router, channelPubkey, receiverPubkeys })
|
|
629
|
+
if (base.senderPubkey !== receiverPubkey) return
|
|
630
|
+
onContentKeyUsage({
|
|
631
|
+
...base,
|
|
632
|
+
direction: 'sent',
|
|
633
|
+
keyRole: 'sender',
|
|
634
|
+
receiverPubkey: base.routerReceiverPubkey || '',
|
|
635
|
+
contentKeyPubkey: readImkcTag(router),
|
|
636
|
+
contentKeyProof: readImkcProof(router)
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function emitReceivedContentKeyUsage ({ outer, router, channelPubkey, receiverPubkey, receiverPubkeys, envelope, onContentKeyUsage }) {
|
|
641
|
+
if (!receiverPubkey || !onContentKeyUsage || envelope.receiverPubkey !== receiverPubkey) return
|
|
642
|
+
|
|
643
|
+
onContentKeyUsage({
|
|
644
|
+
...contentKeyUsageBase({ outer, router, channelPubkey, receiverPubkeys }),
|
|
645
|
+
direction: 'received',
|
|
646
|
+
keyRole: 'receiver',
|
|
647
|
+
receiverPubkey: envelope.receiverPubkey,
|
|
648
|
+
contentKeyPubkey: envelope.iykcPubkey,
|
|
649
|
+
contentKeyProof: envelope.iykcProof,
|
|
650
|
+
rowIndex: envelope.index
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function createProcessor ({
|
|
655
|
+
receiverSigner,
|
|
656
|
+
privateChannelSigner,
|
|
657
|
+
privateChannelSignersByPubkey,
|
|
658
|
+
privateChannelReaderSigner = privateChannelSigner,
|
|
659
|
+
privateChannelReaderSignersByPubkey,
|
|
660
|
+
privateChannelReaderPubkey,
|
|
661
|
+
privateChannelReaderPubkeysByPubkey,
|
|
662
|
+
receiverPubkey,
|
|
663
|
+
mode = 'leecher',
|
|
664
|
+
modeByPubkey,
|
|
665
|
+
onChunk,
|
|
666
|
+
onEvent,
|
|
667
|
+
onNymEvent,
|
|
668
|
+
onSeedEvent,
|
|
669
|
+
onContentKeyUsage,
|
|
670
|
+
onError,
|
|
671
|
+
receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS,
|
|
672
|
+
receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES,
|
|
673
|
+
receivedChunkStorageArea,
|
|
674
|
+
ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS,
|
|
675
|
+
ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES
|
|
676
|
+
}) {
|
|
677
|
+
const receivedChunks = createReceivedChunkStore({
|
|
678
|
+
ttlMs: receivedChunkTtlMs,
|
|
679
|
+
maxBytes: receivedChunkMaxBytes,
|
|
680
|
+
storageArea: receivedChunkStorageArea
|
|
681
|
+
})
|
|
682
|
+
const ignoredGroups = createTtlSet({
|
|
683
|
+
ttlMs: ignoredGroupTtlMs,
|
|
684
|
+
maxEntries: ignoredGroupMaxEntries
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
return async function processOuterEvent (outer) {
|
|
688
|
+
let groupKey = ''
|
|
689
|
+
try {
|
|
690
|
+
const channelPubkey = outer.pubkey || await privateChannelSigner?.getPublicKey?.()
|
|
691
|
+
const channelSigner = readSignerFromMap(privateChannelSignersByPubkey, channelPubkey) || privateChannelSigner
|
|
692
|
+
const channelReaderSigner = readSignerFromMap(privateChannelReaderSignersByPubkey, channelPubkey) || privateChannelReaderSigner || channelSigner
|
|
693
|
+
const channelReaderPubkey = readValueFromMap(privateChannelReaderPubkeysByPubkey, channelPubkey) || privateChannelReaderPubkey || channelPubkey
|
|
694
|
+
const channelMode = readValueFromMap(modeByPubkey, channelPubkey) || mode
|
|
695
|
+
if (!channelPubkey) throw new Error('PRIVATE_CHANNEL_PUBKEY_REQUIRED')
|
|
696
|
+
|
|
697
|
+
const decrypted = await decryptRouter({
|
|
698
|
+
content: outer.content,
|
|
699
|
+
channelPubkey,
|
|
700
|
+
channelSigner,
|
|
701
|
+
channelReaderSigner,
|
|
702
|
+
channelReaderPubkey
|
|
703
|
+
})
|
|
704
|
+
if (decrypted.kind === NYM_CARRIER_KIND) {
|
|
705
|
+
const carrier = assertValidNymCarrierEvent(decrypted)
|
|
706
|
+
const { index, total } = readChunkTag(carrier)
|
|
707
|
+
groupKey = receivedChunks.groupKeyFor(channelPubkey, nymCarrierGroupId(carrier))
|
|
708
|
+
if (ignoredGroups.has(groupKey)) return
|
|
709
|
+
|
|
710
|
+
const meta = receivedChunks.put({
|
|
711
|
+
channelPubkey,
|
|
712
|
+
routerPubkey: nymCarrierGroupId(carrier),
|
|
713
|
+
index,
|
|
714
|
+
total,
|
|
715
|
+
content: JSON.stringify(carrier)
|
|
716
|
+
})
|
|
717
|
+
const status = receivedChunks.status(meta)
|
|
718
|
+
onChunk?.({
|
|
719
|
+
outer,
|
|
720
|
+
nymCarrier: carrier,
|
|
721
|
+
channelPubkey,
|
|
722
|
+
index,
|
|
723
|
+
total,
|
|
724
|
+
received: status.received,
|
|
725
|
+
missing: status.missing
|
|
726
|
+
})
|
|
727
|
+
if (status.received < total) return
|
|
728
|
+
|
|
729
|
+
const carriers = receivedChunks.readChunkContents(groupKey).map(raw => JSON.parse(raw))
|
|
730
|
+
const event = eventFromNymCarriers(carriers)
|
|
731
|
+
const shouldSeed = storesRecoverySeeds(channelMode)
|
|
732
|
+
if (shouldSeed) {
|
|
733
|
+
await onSeedEvent?.({
|
|
734
|
+
recordType: 'nymCarrier_v1',
|
|
735
|
+
outer,
|
|
736
|
+
carriers,
|
|
737
|
+
carrier: carriers[0],
|
|
738
|
+
channelPubkey,
|
|
739
|
+
event
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
await onNymEvent?.(event, outer, {
|
|
743
|
+
carrier: carriers[0],
|
|
744
|
+
carriers,
|
|
745
|
+
channelPubkey
|
|
746
|
+
})
|
|
747
|
+
receivedChunks.removeGroup(groupKey)
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
const router = decrypted
|
|
751
|
+
if (router.kind !== ROUTER_KIND) return
|
|
752
|
+
const senderPubkey = readSenderTag(router)
|
|
753
|
+
if (receiverPubkey && readReceiverTag(router) && readReceiverTag(router) !== receiverPubkey && senderPubkey !== receiverPubkey) return
|
|
754
|
+
const { index, total } = readChunkTag(router)
|
|
755
|
+
groupKey = receivedChunks.groupKeyFor(channelPubkey, router.pubkey)
|
|
756
|
+
if (ignoredGroups.has(groupKey)) return
|
|
757
|
+
|
|
758
|
+
const imkcPubkey = readImkcTag(router)
|
|
759
|
+
assertValidRouterImkcProof({ router, senderPubkey, imkcPubkey, imkcProof: readImkcProof(router) })
|
|
760
|
+
const meta = receivedChunks.put({
|
|
761
|
+
channelPubkey,
|
|
762
|
+
routerPubkey: router.pubkey,
|
|
763
|
+
index,
|
|
764
|
+
total,
|
|
765
|
+
content: router.content
|
|
766
|
+
})
|
|
767
|
+
const status = receivedChunks.status(meta)
|
|
768
|
+
onChunk?.({
|
|
769
|
+
outer,
|
|
770
|
+
router,
|
|
771
|
+
channelPubkey,
|
|
772
|
+
index,
|
|
773
|
+
total,
|
|
774
|
+
received: status.received,
|
|
775
|
+
missing: status.missing
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
const shouldSeed = storesRecoverySeeds(channelMode)
|
|
779
|
+
const sentByReceiver = receiverPubkey && senderPubkey === receiverPubkey
|
|
780
|
+
// Recovery seeders and own-sent messages need the full recipient list;
|
|
781
|
+
// regular leechers can stop as soon as their envelope is decrypted.
|
|
782
|
+
const mustScanWholeBundle = shouldSeed || sentByReceiver
|
|
783
|
+
let event = null
|
|
784
|
+
const innerEventIdsByRowIndex = {}
|
|
785
|
+
|
|
786
|
+
const drained = await receivedChunks.drainAvailable(groupKey, {
|
|
787
|
+
onLine: async (line, rowIndex, groupMeta, helpers) => {
|
|
788
|
+
if (rowIndex === 0) {
|
|
789
|
+
helpers.rememberPayloadCiphertext(groupMeta, parsePayloadEnvelope(line, rowIndex).ciphertext)
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
if (!groupMeta.payloadCiphertext) throw new Error('MISSING_PAYLOAD_ENVELOPE')
|
|
793
|
+
const envelope = parseRecipientEnvelope(line, rowIndex)
|
|
794
|
+
helpers.rememberReceiverPubkey(groupMeta, envelope.receiverPubkey)
|
|
795
|
+
|
|
796
|
+
if (receiverPubkey && envelope.receiverPubkey === receiverPubkey) {
|
|
797
|
+
assertValidEnvelopeIykcProof(envelope)
|
|
798
|
+
emitReceivedContentKeyUsage({
|
|
799
|
+
outer,
|
|
800
|
+
router,
|
|
801
|
+
channelPubkey,
|
|
802
|
+
receiverPubkey,
|
|
803
|
+
receiverPubkeys: groupMeta.receiverPubkeys,
|
|
804
|
+
envelope,
|
|
805
|
+
onContentKeyUsage
|
|
806
|
+
})
|
|
807
|
+
if (receiverSigner && !event) {
|
|
808
|
+
event = await unwrapRecipientEnvelope({
|
|
809
|
+
payloadCiphertext: groupMeta.payloadCiphertext,
|
|
810
|
+
envelope,
|
|
811
|
+
receiverSigner,
|
|
812
|
+
receiverPubkey,
|
|
813
|
+
senderPubkey,
|
|
814
|
+
imkcPubkey,
|
|
815
|
+
rowScope: channelPubkey
|
|
816
|
+
})
|
|
817
|
+
if (event) {
|
|
818
|
+
innerEventIdsByRowIndex[rowIndex] = event.id
|
|
819
|
+
if (!mustScanWholeBundle) return { stop: true }
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
if (event && !mustScanWholeBundle) {
|
|
827
|
+
await onEvent?.(event, outer, { router: joinedRouter(router), channelPubkey })
|
|
828
|
+
ignoredGroups.add(groupKey)
|
|
829
|
+
receivedChunks.removeGroup(groupKey)
|
|
830
|
+
return
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!drained.complete) return
|
|
834
|
+
|
|
835
|
+
const receiverPubkeys = drained.meta?.receiverPubkeys || []
|
|
836
|
+
const content = shouldSeed ? receivedChunks.readEnvelopeBundleContent(groupKey) : ''
|
|
837
|
+
const jsonl = shouldSeed ? receivedChunks.readEnvelopeBundleText(groupKey) : ''
|
|
838
|
+
const completeRouter = joinedRouter(router, content)
|
|
839
|
+
|
|
840
|
+
emitSentContentKeyUsage({
|
|
841
|
+
outer,
|
|
842
|
+
router: completeRouter,
|
|
843
|
+
channelPubkey,
|
|
844
|
+
receiverPubkey,
|
|
845
|
+
receiverPubkeys,
|
|
846
|
+
onContentKeyUsage
|
|
847
|
+
})
|
|
848
|
+
if (shouldSeed) await onSeedEvent?.({ recordType: 'routerRow_v1', outer, router: completeRouter, channelPubkey, jsonl, innerEventIdsByRowIndex })
|
|
849
|
+
if (event) await onEvent?.(event, outer, { router: completeRouter, channelPubkey, jsonl })
|
|
850
|
+
|
|
851
|
+
receivedChunks.removeGroup(groupKey)
|
|
852
|
+
} catch (err) {
|
|
853
|
+
if (shouldIgnoreGroupError(err) && groupKey) {
|
|
854
|
+
ignoredGroups.add(groupKey)
|
|
855
|
+
receivedChunks.removeGroup(groupKey)
|
|
856
|
+
}
|
|
857
|
+
onError?.(err)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function shouldIgnoreGroupError (err) {
|
|
863
|
+
return [
|
|
864
|
+
'DUPLICATE_NYM_CARRIER_CHUNK',
|
|
865
|
+
'INVALID_IYKC_PROOF',
|
|
866
|
+
'INVALID_IMKC_PROOF',
|
|
867
|
+
'INVALID_MESSAGE_SECKEY',
|
|
868
|
+
'INVALID_NYM_CARRIER',
|
|
869
|
+
'INVALID_NYM_CARRIER_ID',
|
|
870
|
+
'INVALID_NYM_CARRIER_INNER_ID',
|
|
871
|
+
'INVALID_NYM_CARRIER_KIND',
|
|
872
|
+
'INVALID_NYM_CARRIER_PAYLOAD',
|
|
873
|
+
'INVALID_NYM_RUMOR',
|
|
874
|
+
'INVALID_PAYLOAD_ENVELOPE',
|
|
875
|
+
'INVALID_RECIPIENT_ENVELOPE',
|
|
876
|
+
'INVALID_SIGNED_INNER_EVENT',
|
|
877
|
+
'MISSING_PAYLOAD_ENVELOPE',
|
|
878
|
+
'MISMATCHED_NYM_CARRIER_CHUNKS',
|
|
879
|
+
'MISSING_NYM_CARRIER_CHUNK',
|
|
880
|
+
'MISSING_NYM_CARRIER_ID'
|
|
881
|
+
].includes(err?.message)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export async function fetch ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since, until, limit, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkStorageArea, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _getEvents = getEvents }) {
|
|
885
|
+
if (!relays?.length) throw new Error('NO_RELAYS')
|
|
886
|
+
const authors = privateChannelPubkeyList({ privateChannelPubkey, privateChannelPubkeys })
|
|
887
|
+
const filter = { kinds: [PRIVATE_BROADCAST_KIND] }
|
|
888
|
+
if (authors.length) filter.authors = authors
|
|
889
|
+
if (since != null) filter.since = since
|
|
890
|
+
if (until != null) filter.until = until
|
|
891
|
+
if (limit != null) filter.limit = limit
|
|
892
|
+
|
|
893
|
+
const { result: events } = await _getEvents(filter, relays, {
|
|
894
|
+
timeout: 5000,
|
|
895
|
+
timeoutAfterFirstEose: null
|
|
896
|
+
})
|
|
897
|
+
events.sort((a, b) => a.created_at - b.created_at)
|
|
898
|
+
const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkMaxBytes, receivedChunkStorageArea, ignoredGroupTtlMs, ignoredGroupMaxEntries })
|
|
899
|
+
for (const event of events) await processOuterEvent(event)
|
|
900
|
+
return events
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since = nowSeconds() - 5, limit, liveOnly = false, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkStorageArea, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _liveEventsGenerator = getLiveEventsGenerator, _eventsFeedGenerator = getEventsFeedGenerator }) {
|
|
904
|
+
if (!relays?.length) throw new Error('NO_RELAYS')
|
|
905
|
+
if (receiverSigner && !receiverSigner?.nip44DecryptDoubleDH && !receiverSigner?.nip44v3Decrypt) throw new Error('RECEIVER_SIGNER_NIP44V3_DECRYPT_UNSUPPORTED')
|
|
906
|
+
if (!privateChannelReaderSigner && !privateChannelReaderSignersByPubkey && !privateChannelSigner && !privateChannelSignersByPubkey) throw new Error('PRIVATE_CHANNEL_READER_REQUIRED')
|
|
907
|
+
|
|
908
|
+
const authors = privateChannelPubkeyList({ privateChannelPubkey, privateChannelPubkeys })
|
|
909
|
+
const filter = { kinds: [PRIVATE_BROADCAST_KIND], since }
|
|
910
|
+
if (authors.length) filter.authors = authors
|
|
911
|
+
if (limit != null) filter.limit = limit
|
|
912
|
+
const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkMaxBytes, receivedChunkStorageArea, ignoredGroupTtlMs, ignoredGroupMaxEntries })
|
|
913
|
+
const controller = new AbortController()
|
|
914
|
+
const events = liveOnly
|
|
915
|
+
? _liveEventsGenerator(filter, relays, {
|
|
916
|
+
signal: controller.signal
|
|
917
|
+
})
|
|
918
|
+
: _eventsFeedGenerator(filter, relays, {
|
|
919
|
+
signal: controller.signal,
|
|
920
|
+
timeout: 5000,
|
|
921
|
+
timeoutAfterFirstEose: null
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
async function consumeEvents () {
|
|
925
|
+
try {
|
|
926
|
+
for await (const outer of events) {
|
|
927
|
+
if (controller.signal.aborted) continue
|
|
928
|
+
await processOuterEvent(outer)
|
|
929
|
+
}
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (!controller.signal.aborted && error?.message !== 'Aborted') onError?.(error)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
consumeEvents()
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
close () {
|
|
939
|
+
controller.abort()
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|