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,224 @@
1
+ import { freeRelays, seedRelays } from '../constants/index.js'
2
+ import { relayPool } from './relay-pool.js'
3
+
4
+ const QUERY_CACHE_MS = 40 * 60 * 1000
5
+ const RELAY_CACHE_MAX_ITEMS = 500
6
+ const HEX_PUBKEY = /^[0-9a-f]{64}$/i
7
+ const relaysByPubkey = Object.create(null)
8
+ const relayCacheTimersByPubkey = Object.create(null)
9
+ const relayCacheAddedAtByPubkey = Object.create(null)
10
+ const relayCacheEventCreatedAtByPubkey = Object.create(null)
11
+
12
+ const getEvents = (...args) => relayPool.getEvents(...args)
13
+ const getEventsFeedGenerator = (...args) => relayPool.getEventsFeedGenerator(...args)
14
+
15
+ function hasCachedKey (cache, key) {
16
+ return Object.prototype.hasOwnProperty.call(cache, key)
17
+ }
18
+
19
+ function maybeUnref (timer) {
20
+ timer?.unref?.()
21
+ return timer
22
+ }
23
+
24
+ function cloneRelays (relays) {
25
+ return {
26
+ read: [...(relays?.read || [])],
27
+ write: [...(relays?.write || [])]
28
+ }
29
+ }
30
+
31
+ // NIP-65 relay-list tags without a marker apply to both read and write use.
32
+ function parseRelayListEvent (event) {
33
+ const out = { read: [], write: [] }
34
+ if (!event || event.kind !== 10002) return out
35
+ for (const tag of event.tags) {
36
+ if (tag[0] !== 'r' || typeof tag[1] !== 'string') continue
37
+ if (tag[2] === 'read') out.read.push(tag[1])
38
+ else if (tag[2] === 'write') out.write.push(tag[1])
39
+ else { out.read.push(tag[1]); out.write.push(tag[1]) }
40
+ }
41
+ out.read = [...new Set(out.read)]
42
+ out.write = [...new Set(out.write)]
43
+ return out
44
+ }
45
+
46
+ function uniquePubkeys (pubkeys, { requireHex = false } = {}) {
47
+ const values = [...new Set(pubkeys || [])].filter(Boolean)
48
+ return requireHex ? values.filter(pubkey => HEX_PUBKEY.test(pubkey)) : values
49
+ }
50
+
51
+ function relayListCreatedAt (event) {
52
+ return Number.isFinite(event?.created_at) ? event.created_at : 0
53
+ }
54
+
55
+ function relaySetsEqual (a, b) {
56
+ const left = new Set(a || [])
57
+ const right = new Set(b || [])
58
+ if (left.size !== right.size) return false
59
+ for (const value of left) if (!right.has(value)) return false
60
+ return true
61
+ }
62
+
63
+ function relaySetChanges (previous, next) {
64
+ const read = !relaySetsEqual(previous?.read, next?.read)
65
+ const write = !relaySetsEqual(previous?.write, next?.write)
66
+ return {
67
+ read,
68
+ write,
69
+ both: read || write
70
+ }
71
+ }
72
+
73
+ function relayTypeChanged (changes, relayType) {
74
+ if (relayType === 'read') return changes.read
75
+ if (relayType === 'write') return changes.write
76
+ return changes.both
77
+ }
78
+
79
+ function deleteCachedRelay (pubkey) {
80
+ clearTimeout(relayCacheTimersByPubkey[pubkey])
81
+ delete relaysByPubkey[pubkey]
82
+ delete relayCacheTimersByPubkey[pubkey]
83
+ delete relayCacheAddedAtByPubkey[pubkey]
84
+ delete relayCacheEventCreatedAtByPubkey[pubkey]
85
+ }
86
+
87
+ function setCachedRelays (pubkey, relays, createdAt, cacheMs) {
88
+ relaysByPubkey[pubkey] = cloneRelays(relays)
89
+ relayCacheAddedAtByPubkey[pubkey] = Date.now()
90
+ relayCacheEventCreatedAtByPubkey[pubkey] = createdAt
91
+ clearTimeout(relayCacheTimersByPubkey[pubkey])
92
+ if (cacheMs > 0) {
93
+ relayCacheTimersByPubkey[pubkey] = maybeUnref(setTimeout(() => {
94
+ deleteCachedRelay(pubkey)
95
+ }, cacheMs))
96
+ } else {
97
+ delete relayCacheTimersByPubkey[pubkey]
98
+ }
99
+ }
100
+
101
+ function pruneRelayCache () {
102
+ const keys = Object.keys(relaysByPubkey)
103
+ if (keys.length <= RELAY_CACHE_MAX_ITEMS) return
104
+
105
+ keys
106
+ .sort((a, b) => (relayCacheAddedAtByPubkey[a] || 0) - (relayCacheAddedAtByPubkey[b] || 0))
107
+ .slice(0, keys.length - RELAY_CACHE_MAX_ITEMS)
108
+ .forEach(deleteCachedRelay)
109
+ }
110
+
111
+ export function clearRelayQueryCache () {
112
+ for (const timer of Object.values(relayCacheTimersByPubkey)) clearTimeout(timer)
113
+ for (const key of Object.keys(relaysByPubkey)) delete relaysByPubkey[key]
114
+ for (const key of Object.keys(relayCacheTimersByPubkey)) delete relayCacheTimersByPubkey[key]
115
+ for (const key of Object.keys(relayCacheAddedAtByPubkey)) delete relayCacheAddedAtByPubkey[key]
116
+ for (const key of Object.keys(relayCacheEventCreatedAtByPubkey)) delete relayCacheEventCreatedAtByPubkey[key]
117
+ }
118
+
119
+ export function cacheRelayListEvent (event, { cacheMs = QUERY_CACHE_MS } = {}) {
120
+ if (!event || event.kind !== 10002 || !event.pubkey) return null
121
+ const createdAt = relayListCreatedAt(event)
122
+ const previousCreatedAt = relayCacheEventCreatedAtByPubkey[event.pubkey]
123
+ if (previousCreatedAt != null && createdAt <= previousCreatedAt) return null
124
+
125
+ const previousRelays = hasCachedKey(relaysByPubkey, event.pubkey)
126
+ ? cloneRelays(relaysByPubkey[event.pubkey])
127
+ : null
128
+ const relays = parseRelayListEvent(event)
129
+ const changes = relaySetChanges(previousRelays, relays)
130
+ setCachedRelays(event.pubkey, relays, createdAt, cacheMs)
131
+ pruneRelayCache()
132
+
133
+ return {
134
+ pubkey: event.pubkey,
135
+ event,
136
+ relays: cloneRelays(relays),
137
+ previousRelays,
138
+ changes
139
+ }
140
+ }
141
+
142
+ export function subscribeRelayListUpdates (pubkeys, {
143
+ relayType = 'both',
144
+ onChange,
145
+ relays = seedRelays,
146
+ cacheMs = QUERY_CACHE_MS,
147
+ _eventsFeedGenerator = getEventsFeedGenerator
148
+ } = {}) {
149
+ const authors = uniquePubkeys(pubkeys, { requireHex: _eventsFeedGenerator === getEventsFeedGenerator })
150
+ if (!authors.length) return () => {}
151
+
152
+ let closed = false
153
+ const controller = new AbortController()
154
+
155
+ async function consumeRelayListUpdates () {
156
+ try {
157
+ for await (const event of _eventsFeedGenerator({
158
+ kinds: [10002],
159
+ authors
160
+ }, relays, {
161
+ signal: controller.signal,
162
+ timeout: 5000,
163
+ timeoutAfterFirstEose: null
164
+ })) {
165
+ if (closed || !authors.includes(event.pubkey)) continue
166
+ const update = cacheRelayListEvent(event, { cacheMs })
167
+ if (!update || !relayTypeChanged(update.changes, relayType)) continue
168
+ onChange?.({
169
+ ...update,
170
+ relayType
171
+ })
172
+ }
173
+ } catch (error) {
174
+ if (!closed && error?.message !== 'Aborted') console.error('relay-list watch failed:', error)
175
+ }
176
+ }
177
+
178
+ consumeRelayListUpdates()
179
+
180
+ return () => {
181
+ closed = true
182
+ controller.abort()
183
+ }
184
+ }
185
+
186
+ export async function getRelaysByPubkey (pubkeys, { _getEvents = getEvents, cacheMs = QUERY_CACHE_MS } = {}) {
187
+ const pubkeyList = uniquePubkeys(pubkeys, { requireHex: _getEvents === getEvents })
188
+ if (!pubkeyList.length) return {}
189
+
190
+ const out = {}
191
+ const missingPubkeys = []
192
+ for (const pubkey of pubkeyList) {
193
+ if (hasCachedKey(relaysByPubkey, pubkey)) out[pubkey] = cloneRelays(relaysByPubkey[pubkey])
194
+ else missingPubkeys.push(pubkey)
195
+ }
196
+ if (!missingPubkeys.length) return out
197
+
198
+ const { result: events } = await _getEvents({
199
+ kinds: [10002],
200
+ authors: missingPubkeys,
201
+ limit: missingPubkeys.length
202
+ }, seedRelays, {
203
+ timeout: 5000,
204
+ timeoutAfterFirstEose: null
205
+ })
206
+
207
+ const latestByPubkey = {}
208
+ for (const event of events) {
209
+ if (!missingPubkeys.includes(event.pubkey)) continue
210
+ if (!latestByPubkey[event.pubkey] || event.created_at > latestByPubkey[event.pubkey].created_at) {
211
+ latestByPubkey[event.pubkey] = event
212
+ }
213
+ }
214
+
215
+ for (const pubkey of missingPubkeys) {
216
+ const relays = latestByPubkey[pubkey]
217
+ ? parseRelayListEvent(latestByPubkey[pubkey])
218
+ : { read: freeRelays.slice(0, 2), write: freeRelays.slice(0, 2) }
219
+ setCachedRelays(pubkey, relays, relayListCreatedAt(latestByPubkey[pubkey]), cacheMs)
220
+ out[pubkey] = relays
221
+ }
222
+ pruneRelayCache()
223
+ return out
224
+ }
@@ -0,0 +1,156 @@
1
+ import { Relay } from 'nostr-tools/relay'
2
+ import { maybeUnref } from '../helpers/timer.js'
3
+
4
+ function relayError (reason, fallback) {
5
+ return reason instanceof Error ? reason : new Error(reason || fallback)
6
+ }
7
+
8
+ // Relay.auth() caches one authentication promise per connection. NIP-42 permits
9
+ // several pubkeys on one connection, so this adapter tracks AUTH replies by
10
+ // signed auth-event id instead of using Relay.auth().
11
+ export class RelayConnection extends Relay {
12
+ #challenge = null
13
+ #pendingAuths = new Map()
14
+ #countSerial = 0
15
+ #pendingCounts = new Map()
16
+
17
+ constructor (url) {
18
+ super(url)
19
+ this.onclose = () => {
20
+ this.#challenge = null
21
+ this.#rejectPendingAuths(new Error('AUTH_CONNECTION_CLOSED'))
22
+ this.#rejectPendingCounts(new Error('COUNT_CONNECTION_CLOSED'))
23
+ }
24
+ }
25
+
26
+ #settleAuth (eventId, { success, reason }) {
27
+ const pending = this.#pendingAuths.get(eventId)
28
+ if (!pending) return
29
+
30
+ this.#pendingAuths.delete(eventId)
31
+ clearTimeout(pending.timer)
32
+ if (success) pending.resolve(reason)
33
+ else pending.reject(relayError(reason, 'AUTH_REJECTED'))
34
+ }
35
+
36
+ #rejectPendingAuths (reason) {
37
+ for (const eventId of [...this.#pendingAuths.keys()]) {
38
+ this.#settleAuth(eventId, { success: false, reason })
39
+ }
40
+ }
41
+
42
+ #settleCount (id, { payload, reason }) {
43
+ const pending = this.#pendingCounts.get(id)
44
+ if (!pending) return
45
+
46
+ this.#pendingCounts.delete(id)
47
+ pending.signal?.removeEventListener('abort', pending.onAbort)
48
+ if (reason !== undefined) pending.reject(relayError(reason, 'COUNT_REJECTED'))
49
+ else pending.resolve(payload)
50
+ }
51
+
52
+ #rejectPendingCounts (reason) {
53
+ for (const id of [...this.#pendingCounts.keys()]) {
54
+ this.#settleCount(id, { reason })
55
+ }
56
+ }
57
+
58
+ // Sends a caller-signed AUTH event and waits for the matching NIP-42 OK.
59
+ // Each auth event is independent, so different pubkeys can share this socket.
60
+ async authenticate (getAuthEvent) {
61
+ if (!this.#challenge) throw new Error('AUTH_CHALLENGE_MISSING')
62
+
63
+ const event = await getAuthEvent({
64
+ relay: this.url,
65
+ challenge: this.#challenge
66
+ })
67
+ if (typeof event?.id !== 'string' || !event.id) throw new Error('AUTH_EVENT_ID_REQUIRED')
68
+ const existing = this.#pendingAuths.get(event.id)
69
+ if (existing) return existing.promise
70
+
71
+ let resolveAuth
72
+ let rejectAuth
73
+ const promise = new Promise((resolve, reject) => {
74
+ resolveAuth = resolve
75
+ rejectAuth = reject
76
+ })
77
+ const timer = maybeUnref(setTimeout(() => {
78
+ this.#settleAuth(event.id, { success: false, reason: new Error('AUTH_TIMEOUT') })
79
+ }, this.publishTimeout))
80
+ this.#pendingAuths.set(event.id, {
81
+ resolve: resolveAuth,
82
+ reject: rejectAuth,
83
+ timer,
84
+ promise
85
+ })
86
+
87
+ Promise.resolve(this.send(JSON.stringify(['AUTH', event]))).catch(reason => {
88
+ this.#settleAuth(event.id, { success: false, reason })
89
+ })
90
+ return promise
91
+ }
92
+
93
+ // nostr-tools@2.23.5 exposes only Relay.count(), which discards the NIP-45
94
+ // HLL payload. Keep these requests separate so callers can aggregate it.
95
+ countWithHll (filters, { signal } = {}) {
96
+ if (signal?.aborted) return Promise.reject(new Error('COUNT_ABORTED'))
97
+
98
+ const id = `p2r2p-count:${++this.#countSerial}`
99
+ let resolveCount
100
+ let rejectCount
101
+ const promise = new Promise((resolve, reject) => {
102
+ resolveCount = resolve
103
+ rejectCount = reject
104
+ })
105
+ const onAbort = () => {
106
+ this.#settleCount(id, { reason: new Error('COUNT_ABORTED') })
107
+ }
108
+ this.#pendingCounts.set(id, {
109
+ resolve: resolveCount,
110
+ reject: rejectCount,
111
+ signal,
112
+ onAbort
113
+ })
114
+ signal?.addEventListener('abort', onAbort, { once: true })
115
+
116
+ let message
117
+ try {
118
+ message = JSON.stringify(['COUNT', id, ...filters])
119
+ } catch (error) {
120
+ this.#settleCount(id, { reason: error })
121
+ return promise
122
+ }
123
+ Promise.resolve(this.send(message)).catch(reason => {
124
+ this.#settleCount(id, { reason })
125
+ })
126
+ return promise
127
+ }
128
+
129
+ // Relay assigns this method to its WebSocket handler while connecting. Capture
130
+ // AUTH and raw COUNT replies before Relay's parser discards COUNT HLL payloads.
131
+ _onmessage (message) {
132
+ try {
133
+ const data = JSON.parse(message.data)
134
+ if (data[0] === 'AUTH' && typeof data[1] === 'string') {
135
+ this.#challenge = data[1]
136
+ } else if (data[0] === 'OK' && typeof data[1] === 'string') {
137
+ this.#settleAuth(data[1], { success: data[2] === true, reason: data[3] })
138
+ } else if (data[0] === 'COUNT' && typeof data[1] === 'string') {
139
+ this.#settleCount(data[1], { payload: data[2] })
140
+ } else if (data[0] === 'CLOSED' && typeof data[1] === 'string') {
141
+ this.#settleCount(data[1], { reason: data[2] || 'COUNT_CLOSED' })
142
+ }
143
+ } catch {
144
+ // Relay's own parser below reports malformed relay messages.
145
+ }
146
+
147
+ return super._onmessage(message)
148
+ }
149
+
150
+ close () {
151
+ this.#challenge = null
152
+ this.#rejectPendingAuths(new Error('AUTH_CONNECTION_CLOSED'))
153
+ this.#rejectPendingCounts(new Error('COUNT_CONNECTION_CLOSED'))
154
+ return super.close()
155
+ }
156
+ }