libp2r2p 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +129 -0
  3. package/base16/index.js +14 -0
  4. package/base64/index.js +29 -0
  5. package/content-key/event/index.js +85 -0
  6. package/content-key/index.js +1 -0
  7. package/content-key/services/iykc-proof.js +129 -0
  8. package/double-dh/index.js +166 -0
  9. package/ecdh/index.js +8 -0
  10. package/idb/index.js +64 -0
  11. package/idb-queue/index.js +793 -0
  12. package/index.js +21 -0
  13. package/key/index.js +139 -0
  14. package/network/index.js +68 -0
  15. package/nip44-v3/index.js +175 -0
  16. package/nip46/constants/index.js +5 -0
  17. package/nip46/helpers/frame.js +51 -0
  18. package/nip46/helpers/url.js +96 -0
  19. package/nip46/index.js +5 -0
  20. package/nip46/services/bunker-signer.js +49 -0
  21. package/nip46/services/client.js +252 -0
  22. package/nip46/services/server-session.js +225 -0
  23. package/nip46/services/transport.js +265 -0
  24. package/package.json +49 -0
  25. package/private-channel/constants/index.js +5 -0
  26. package/private-channel/helpers/chunk-size.js +61 -0
  27. package/private-channel/helpers/chunks.js +282 -0
  28. package/private-channel/helpers/event.js +68 -0
  29. package/private-channel/index.js +942 -0
  30. package/private-channel/services/received-chunks.js +398 -0
  31. package/private-message/index.js +672 -0
  32. package/private-messenger/constants/index.js +1 -0
  33. package/private-messenger/index.js +1431 -0
  34. package/private-messenger/recovery/index.js +316 -0
  35. package/relay/constants/index.js +18 -0
  36. package/relay/helpers/hll.js +62 -0
  37. package/relay/helpers/publish.js +100 -0
  38. package/relay/helpers/routing.js +36 -0
  39. package/relay/helpers/timer.js +4 -0
  40. package/relay/index.js +4 -0
  41. package/relay/services/query.js +224 -0
  42. package/relay/services/relay-connection.js +156 -0
  43. package/relay/services/relay-pool.js +908 -0
  44. package/temporary-storage/index.js +85 -0
  45. package/tests/base16.test.js +19 -0
  46. package/tests/base64.test.js +18 -0
  47. package/tests/content-key-event.test.js +48 -0
  48. package/tests/fixtures/nip44v3-vectors.json +1 -0
  49. package/tests/helpers/test-signer.js +185 -0
  50. package/tests/idb-queue.test.js +153 -0
  51. package/tests/idb.test.js +91 -0
  52. package/tests/nip44-v3.test.js +73 -0
  53. package/tests/nip46.test.js +668 -0
  54. package/tests/private-channel.test.js +1899 -0
  55. package/tests/private-message.test.js +460 -0
  56. package/tests/private-messenger.test.js +1715 -0
  57. package/tests/queries.test.js +101 -0
  58. package/tests/queue-parity.test.js +105 -0
  59. package/tests/relay-hll.test.js +32 -0
  60. package/tests/relay-pool.test.js +2067 -0
  61. package/tests/temporary-storage.test.js +89 -0
  62. package/tests/web-storage-queue.test.js +480 -0
  63. package/web-storage-queue/index.js +652 -0
@@ -0,0 +1,398 @@
1
+ import { base64ToBytes, bytesToBase64 } from '../../base64/index.js'
2
+ import { JSONL_CHUNK_BYTES } from '../helpers/chunk-size.js'
3
+
4
+ export const DEFAULT_RECEIVED_CHUNK_TTL_MS = 60 * 60 * 1000 // 1 hour
5
+ // For illustration purposes: A 280-character rumor would take approximately
6
+ // 23 chunks (~0.65 MiB) to be sent if encrypted to 1000 participants.
7
+ export const DEFAULT_RECEIVED_CHUNK_MAX_BYTES = Math.min(JSONL_CHUNK_BYTES * 64, 3 * 1024 * 1024 /* 3 MiB cap */)
8
+
9
+ const DEFAULT_PREFIX = 'libp2r2p:private-channel:received'
10
+ const encoder = new TextEncoder()
11
+ const decoder = new TextDecoder()
12
+
13
+ // Receive buffers survive reloads briefly, unlike send-side temporary chunks.
14
+ // Stale cleanup handles peers that never publish the remaining chunks.
15
+
16
+ function byteLength (value) {
17
+ return encoder.encode(String(value)).length
18
+ }
19
+
20
+ function parseJson (raw, fallback) {
21
+ try { return JSON.parse(raw || '') } catch { return fallback }
22
+ }
23
+
24
+ function uniq (values) {
25
+ return [...new Set((values || []).filter(Boolean))]
26
+ }
27
+
28
+ function normalizeGroupKeys (value) {
29
+ return Array.isArray(value) ? uniq(value.filter(key => typeof key === 'string' && key)) : []
30
+ }
31
+
32
+ function normalizeReceived (received) {
33
+ if (!received || typeof received !== 'object' || Array.isArray(received)) return {}
34
+ return Object.fromEntries(
35
+ Object.entries(received)
36
+ .filter(([index, hasChunk]) => hasChunk && Number.isSafeInteger(Number(index)) && Number(index) >= 0)
37
+ .map(([index]) => [String(Number(index)), true])
38
+ )
39
+ }
40
+
41
+ function normalizeMeta (meta, groupKey) {
42
+ if (!meta || typeof meta !== 'object') return null
43
+ const total = Number(meta.total)
44
+ const nextIndex = Number(meta.nextIndex)
45
+ const rowIndex = Number(meta.rowIndex)
46
+ if (!Number.isSafeInteger(total) || total < 1) return null
47
+ return {
48
+ groupKey,
49
+ channelPubkey: String(meta.channelPubkey || ''),
50
+ routerPubkey: String(meta.routerPubkey || ''),
51
+ total,
52
+ received: normalizeReceived(meta.received),
53
+ receivedCount: Math.max(0, Number(meta.receivedCount) || 0),
54
+ nextIndex: Number.isSafeInteger(nextIndex) && nextIndex >= 0 ? nextIndex : 0,
55
+ rowIndex: Number.isSafeInteger(rowIndex) && rowIndex >= 0 ? rowIndex : 0,
56
+ carry: typeof meta.carry === 'string' ? meta.carry : '',
57
+ payloadCiphertext: typeof meta.payloadCiphertext === 'string' ? meta.payloadCiphertext : '',
58
+ receiverPubkeys: uniq(meta.receiverPubkeys || []),
59
+ byteSize: Math.max(0, Number(meta.byteSize) || 0),
60
+ createdAt: Number(meta.createdAt) || Date.now(),
61
+ updatedAt: Number(meta.updatedAt) || Date.now()
62
+ }
63
+ }
64
+
65
+ function isQuotaExceeded (err) {
66
+ return err?.name === 'QuotaExceededError' ||
67
+ err?.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
68
+ err?.code === 22 ||
69
+ err?.code === 1014 ||
70
+ /quota/i.test(err?.message || '')
71
+ }
72
+
73
+ function joinBase64Chunks (parts) {
74
+ let out = ''
75
+ let carry = new Uint8Array()
76
+
77
+ for (const part of parts) {
78
+ const bytes = base64ToBytes(part)
79
+ const joined = new Uint8Array(carry.length + bytes.length)
80
+ joined.set(carry)
81
+ joined.set(bytes, carry.length)
82
+
83
+ const completeLength = joined.length - (joined.length % 3)
84
+ if (completeLength) out += bytesToBase64(joined.slice(0, completeLength))
85
+ carry = joined.slice(completeLength)
86
+ }
87
+
88
+ if (carry.length) out += bytesToBase64(carry)
89
+ return out
90
+ }
91
+
92
+ export function createReceivedChunkStore ({
93
+ prefix = DEFAULT_PREFIX,
94
+ storageArea = globalThis.localStorage,
95
+ ttlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS,
96
+ maxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES
97
+ } = {}) {
98
+ const groupsKey = `${prefix}:groups`
99
+ const configuredTtlMs = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : DEFAULT_RECEIVED_CHUNK_TTL_MS
100
+ const configuredMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : Infinity
101
+
102
+ function storage () {
103
+ return storageArea
104
+ }
105
+
106
+ function metaKey (groupKey) {
107
+ return `${prefix}:group:${groupKey}:meta`
108
+ }
109
+
110
+ function chunkKey (groupKey, index) {
111
+ return `${prefix}:group:${groupKey}:chunk:${index}`
112
+ }
113
+
114
+ function groupKeyFor (channelPubkey, routerPubkey) {
115
+ return `${channelPubkey}:${routerPubkey}`
116
+ }
117
+
118
+ function readGroupKeys () {
119
+ return normalizeGroupKeys(parseJson(storage().getItem(groupsKey), []))
120
+ }
121
+
122
+ function writeGroupKeys (keys) {
123
+ const normalized = normalizeGroupKeys(keys)
124
+ if (normalized.length) storage().setItem(groupsKey, JSON.stringify(normalized))
125
+ else storage().removeItem(groupsKey)
126
+ }
127
+
128
+ function addGroupKey (groupKey) {
129
+ const keys = readGroupKeys()
130
+ if (!keys.includes(groupKey)) writeGroupKeys(keys.concat(groupKey))
131
+ }
132
+
133
+ function removeGroupKey (groupKey) {
134
+ writeGroupKeys(readGroupKeys().filter(key => key !== groupKey))
135
+ }
136
+
137
+ function readMeta (groupKey) {
138
+ return normalizeMeta(parseJson(storage().getItem(metaKey(groupKey)), null), groupKey)
139
+ }
140
+
141
+ function writeMeta (meta) {
142
+ storage().setItem(metaKey(meta.groupKey), JSON.stringify(meta))
143
+ }
144
+
145
+ function removeGroup (groupKey) {
146
+ const meta = readMeta(groupKey)
147
+ if (meta) {
148
+ for (const index of Object.keys(meta.received)) {
149
+ storage().removeItem(chunkKey(groupKey, index))
150
+ }
151
+ }
152
+ storage().removeItem(metaKey(groupKey))
153
+ removeGroupKey(groupKey)
154
+ }
155
+
156
+ function allMetas () {
157
+ const keys = readGroupKeys()
158
+ const metas = []
159
+ const liveKeys = []
160
+ for (const groupKey of keys) {
161
+ const meta = readMeta(groupKey)
162
+ if (!meta) continue
163
+ metas.push(meta)
164
+ liveKeys.push(groupKey)
165
+ }
166
+ if (liveKeys.length !== keys.length) writeGroupKeys(liveKeys)
167
+ return metas
168
+ }
169
+
170
+ function totalStoredBytes () {
171
+ return allMetas().reduce((total, meta) => {
172
+ return total + meta.byteSize + byteLength(JSON.stringify(meta))
173
+ }, 0)
174
+ }
175
+
176
+ function oldestGroupKey ({ except } = {}) {
177
+ return allMetas()
178
+ .filter(meta => meta.groupKey !== except)
179
+ .sort((a, b) => a.updatedAt - b.updatedAt)[0]?.groupKey || ''
180
+ }
181
+
182
+ function evictOldestUntilFits (requiredBytes = 0, { except } = {}) {
183
+ if (!Number.isFinite(configuredMaxBytes)) return
184
+ while (totalStoredBytes() + requiredBytes > configuredMaxBytes) {
185
+ const groupKey = oldestGroupKey({ except })
186
+ if (!groupKey) break
187
+ removeGroup(groupKey)
188
+ }
189
+ }
190
+
191
+ function writeChunk (key, value, requiredBytes, except) {
192
+ evictOldestUntilFits(requiredBytes, { except })
193
+ try {
194
+ storage().setItem(key, value)
195
+ return
196
+ } catch (err) {
197
+ if (!isQuotaExceeded(err)) throw err
198
+ }
199
+
200
+ let groupKey = oldestGroupKey({ except })
201
+ while (groupKey) {
202
+ removeGroup(groupKey)
203
+ try {
204
+ storage().setItem(key, value)
205
+ return
206
+ } catch (err) {
207
+ if (!isQuotaExceeded(err)) throw err
208
+ }
209
+ groupKey = oldestGroupKey({ except })
210
+ }
211
+
212
+ storage().setItem(key, value)
213
+ }
214
+
215
+ function cleanupStale (nowMs = Date.now()) {
216
+ const cutoff = nowMs - configuredTtlMs
217
+ for (const meta of allMetas()) {
218
+ if (meta.updatedAt <= cutoff) removeGroup(meta.groupKey)
219
+ }
220
+ evictOldestUntilFits(0)
221
+ }
222
+
223
+ function put ({ channelPubkey, routerPubkey, index, total, content }) {
224
+ if (!channelPubkey || !routerPubkey) throw new Error('RECEIVED_CHUNK_GROUP_REQUIRED')
225
+ if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 0 || total < 1 || index >= total) {
226
+ throw new Error('INVALID_RECEIVED_CHUNK_INDEX')
227
+ }
228
+
229
+ cleanupStale()
230
+
231
+ const groupKey = groupKeyFor(channelPubkey, routerPubkey)
232
+ const now = Date.now()
233
+ const chunk = String(content || '')
234
+ const nextBytes = byteLength(chunk)
235
+ let meta = readMeta(groupKey)
236
+
237
+ if (meta && meta.total !== total) {
238
+ removeGroup(groupKey)
239
+ meta = null
240
+ }
241
+
242
+ if (!meta) {
243
+ meta = normalizeMeta({
244
+ channelPubkey,
245
+ routerPubkey,
246
+ total,
247
+ received: {},
248
+ receivedCount: 0,
249
+ nextIndex: 0,
250
+ rowIndex: 0,
251
+ carry: '',
252
+ payloadCiphertext: '',
253
+ receiverPubkeys: [],
254
+ byteSize: 0,
255
+ createdAt: now,
256
+ updatedAt: now
257
+ }, groupKey)
258
+ addGroupKey(groupKey)
259
+ writeMeta(meta)
260
+ }
261
+
262
+ if (!meta.received[String(index)]) {
263
+ if (meta.byteSize + nextBytes > configuredMaxBytes) {
264
+ removeGroup(groupKey)
265
+ throw new Error('RECEIVED_CHUNK_GROUP_TOO_LARGE')
266
+ }
267
+ writeChunk(chunkKey(groupKey, index), chunk, nextBytes, groupKey)
268
+ meta.received[String(index)] = true
269
+ meta.receivedCount++
270
+ meta.byteSize += nextBytes
271
+ }
272
+
273
+ meta.total = total
274
+ meta.updatedAt = now
275
+ writeMeta(meta)
276
+ evictOldestUntilFits(0, { except: groupKey })
277
+ return meta
278
+ }
279
+
280
+ function status (metaOrGroupKey) {
281
+ const meta = typeof metaOrGroupKey === 'string' ? readMeta(metaOrGroupKey) : metaOrGroupKey
282
+ if (!meta) return { received: 0, missing: [] }
283
+
284
+ const missing = []
285
+ let received = 0
286
+ for (let index = 0; index < meta.total; index++) {
287
+ if (index < meta.nextIndex || meta.received[String(index)]) received++
288
+ else missing.push(index)
289
+ }
290
+ return { received, missing }
291
+ }
292
+
293
+ function rememberReceiverPubkey (meta, pubkey) {
294
+ if (!pubkey || meta.receiverPubkeys.includes(pubkey)) return
295
+ meta.receiverPubkeys.push(pubkey)
296
+ }
297
+
298
+ function rememberPayloadCiphertext (meta, ciphertext) {
299
+ if (!meta.payloadCiphertext) meta.payloadCiphertext = ciphertext
300
+ }
301
+
302
+ async function drainAvailable (groupKey, { onLine } = {}) {
303
+ const meta = readMeta(groupKey)
304
+ if (!meta) return { complete: false, stopped: false, meta: null }
305
+
306
+ while (meta.nextIndex < meta.total) {
307
+ const index = meta.nextIndex
308
+ const raw = storage().getItem(chunkKey(groupKey, index))
309
+ if (raw == null) break
310
+
311
+ const text = `${meta.carry}${decoder.decode(base64ToBytes(raw))}`
312
+ let start = 0
313
+ let end = text.indexOf('\n', start)
314
+
315
+ while (end !== -1) {
316
+ const line = text.slice(start, end)
317
+ start = end + 1
318
+ if (line) {
319
+ const result = await onLine?.(line, meta.rowIndex, meta, { rememberPayloadCiphertext, rememberReceiverPubkey })
320
+ meta.rowIndex++
321
+ if (result?.stop) {
322
+ meta.updatedAt = Date.now()
323
+ writeMeta(meta)
324
+ return { complete: false, stopped: true, meta }
325
+ }
326
+ }
327
+ end = text.indexOf('\n', start)
328
+ }
329
+
330
+ meta.carry = text.slice(start)
331
+
332
+ meta.nextIndex++
333
+ meta.updatedAt = Date.now()
334
+ writeMeta(meta)
335
+ }
336
+
337
+ if (meta.nextIndex >= meta.total) {
338
+ if (meta.carry) {
339
+ const result = await onLine?.(meta.carry, meta.rowIndex, meta, { rememberPayloadCiphertext, rememberReceiverPubkey })
340
+ meta.rowIndex++
341
+ meta.carry = ''
342
+ if (result?.stop) {
343
+ meta.updatedAt = Date.now()
344
+ writeMeta(meta)
345
+ return { complete: false, stopped: true, meta }
346
+ }
347
+ }
348
+ meta.updatedAt = Date.now()
349
+ writeMeta(meta)
350
+ return { complete: true, stopped: false, meta }
351
+ }
352
+
353
+ return { complete: false, stopped: false, meta }
354
+ }
355
+
356
+ function readEnvelopeBundleContent (groupKey) {
357
+ const meta = readMeta(groupKey)
358
+ if (!meta) return ''
359
+ const parts = []
360
+ for (let index = 0; index < meta.total; index++) {
361
+ const chunk = storage().getItem(chunkKey(groupKey, index))
362
+ if (chunk == null) throw new Error('RECEIVED_CHUNK_MISSING')
363
+ parts.push(chunk)
364
+ }
365
+ return joinBase64Chunks(parts)
366
+ }
367
+
368
+ function readEnvelopeBundleText (groupKey) {
369
+ const content = readEnvelopeBundleContent(groupKey)
370
+ return decoder.decode(base64ToBytes(content))
371
+ }
372
+
373
+ function readChunkContents (groupKey) {
374
+ const meta = readMeta(groupKey)
375
+ if (!meta) return []
376
+ const parts = []
377
+ for (let index = 0; index < meta.total; index++) {
378
+ const chunk = storage().getItem(chunkKey(groupKey, index))
379
+ if (chunk == null) throw new Error('RECEIVED_CHUNK_MISSING')
380
+ parts.push(chunk)
381
+ }
382
+ return parts
383
+ }
384
+
385
+ cleanupStale()
386
+
387
+ return {
388
+ cleanupStale,
389
+ drainAvailable,
390
+ groupKeyFor,
391
+ put,
392
+ readChunkContents,
393
+ readEnvelopeBundleContent,
394
+ readEnvelopeBundleText,
395
+ removeGroup,
396
+ status
397
+ }
398
+ }