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,252 @@
1
+ import { getPublicKey } from 'nostr-tools'
2
+ import { relayPool as defaultRelayPool } from '../../relay/index.js'
3
+ import {
4
+ DEFAULT_TIMEOUT,
5
+ DEFAULT_TIMEOUT_AFTER_FIRST_EOSE,
6
+ NIP46_KIND,
7
+ RELAY_SWITCH_WAIT_TIMEOUT
8
+ } from '../constants/index.js'
9
+ import {
10
+ decodeNip46Frame,
11
+ isNip46EventFor,
12
+ requestError,
13
+ validRequestFrame
14
+ } from '../helpers/frame.js'
15
+ import { normalizeBunkerPointer, parseNostrConnectURI } from '../helpers/url.js'
16
+ import { Nip46Transport, sameRelays, waitForNip46 } from './transport.js'
17
+
18
+ function cleanClientMetadata (value) {
19
+ if (!value || typeof value !== 'object') return null
20
+ const metadata = {}
21
+ for (const key of ['name', 'url', 'image']) {
22
+ if (typeof value[key] === 'string' && value[key]) metadata[key] = value[key]
23
+ }
24
+ return Object.keys(metadata).length ? metadata : null
25
+ }
26
+
27
+ // A reusable NIP-46 client for standard or application-defined commands.
28
+ export class Nip46Client {
29
+ #secretKey
30
+ #transport
31
+ #pointer
32
+ #onAuthUrl
33
+ #onRequest
34
+
35
+ constructor (clientSecretKey, pointer, {
36
+ relayPool = defaultRelayPool,
37
+ onAuthUrl,
38
+ onRequest,
39
+ onError,
40
+ timeout = DEFAULT_TIMEOUT,
41
+ timeoutAfterFirstEose = DEFAULT_TIMEOUT_AFTER_FIRST_EOSE
42
+ } = {}) {
43
+ const normalized = normalizeBunkerPointer(pointer)
44
+ if (!normalized) throw new Error('INVALID_BUNKER_POINTER')
45
+ this.#secretKey = clientSecretKey
46
+ this.#pointer = normalized
47
+ this.#onAuthUrl = onAuthUrl
48
+ this.#onRequest = onRequest
49
+ this.#transport = new Nip46Transport(clientSecretKey, {
50
+ relayPool,
51
+ networkTimeout: timeout,
52
+ timeoutAfterFirstEose,
53
+ onError
54
+ })
55
+ this.#transport.activateContext(this.#openResponseContext(normalized), { retirePrevious: false })
56
+ }
57
+
58
+ // Creates a client for a parsed direct `bunker://` pointer.
59
+ static fromBunker (clientSecretKey, pointer, options = {}) {
60
+ return new this(clientSecretKey, pointer, options)
61
+ }
62
+
63
+ // Waits for a signer response to a client-created `nostrconnect://` URI.
64
+ static async fromURI (clientSecretKey, uri, options = {}) {
65
+ const parsed = parseNostrConnectURI(uri)
66
+ const clientPubkey = getPublicKey(clientSecretKey)
67
+ if (!parsed || clientPubkey !== parsed.clientPubkey) throw new Error('INVALID_NOSTRCONNECT_URI')
68
+
69
+ const relayPool = options.relayPool || defaultRelayPool
70
+ const controller = new AbortController()
71
+ const stream = relayPool.getLiveEventsGenerator({
72
+ kinds: [NIP46_KIND],
73
+ '#p': [clientPubkey],
74
+ limit: 0
75
+ }, parsed.relays, {
76
+ signal: controller.signal,
77
+ timeoutAfterFirstEose: options.timeoutAfterFirstEose ?? DEFAULT_TIMEOUT_AFTER_FIRST_EOSE
78
+ })
79
+ const found = Promise.withResolvers()
80
+ const consume = (async () => {
81
+ try {
82
+ for await (const event of stream) {
83
+ if (!isNip46EventFor(event, clientPubkey)) continue
84
+ const response = decodeNip46Frame(event, clientSecretKey)
85
+ if (response?.result === parsed.secret) {
86
+ found.resolve({
87
+ remoteSignerPubkey: event.pubkey,
88
+ relays: parsed.relays,
89
+ secret: parsed.secret
90
+ })
91
+ }
92
+ }
93
+ } catch (error) {
94
+ if (error?.message !== 'Aborted') found.reject(error)
95
+ }
96
+ })()
97
+
98
+ try {
99
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT
100
+ const report = await waitForNip46(stream.ready, {
101
+ timeout,
102
+ signal: options.signal,
103
+ label: 'NIP46_LISTENER_TIMEOUT'
104
+ })
105
+ if (!stream.readyRelays.length) {
106
+ throw requestError(report.errors?.[0]?.reason || 'NIP46_NO_READY_RELAYS')
107
+ }
108
+ const pointer = await waitForNip46(found.promise, {
109
+ timeout,
110
+ signal: options.signal,
111
+ label: 'NIP46_CONNECTION_TIMEOUT'
112
+ })
113
+ controller.abort()
114
+ await consume
115
+
116
+ const client = new this(clientSecretKey, pointer, options)
117
+ await client.#awaitReady({ timeout, signal: options.signal })
118
+ await client.#switchRelaysAfterConnect()
119
+ return client
120
+ } catch (error) {
121
+ controller.abort()
122
+ await consume.catch(() => {})
123
+ throw error
124
+ }
125
+ }
126
+
127
+ get clientPubkey () { return this.#transport.pubkey }
128
+ get pointer () { return { ...this.#pointer, relays: [...this.#pointer.relays] } }
129
+
130
+ // Connects and immediately asks the remote signer for its preferred relays.
131
+ async connect ({ requestedPermissions = [], clientMetadata, timeout = null, signal } = {}) {
132
+ const permissions = Array.isArray(requestedPermissions)
133
+ ? requestedPermissions.filter(permission => typeof permission === 'string' && permission).join(',')
134
+ : ''
135
+ const metadata = cleanClientMetadata(clientMetadata)
136
+ const params = [this.#pointer.remoteSignerPubkey, this.#pointer.secret || '']
137
+ if (permissions || metadata) params.push(permissions)
138
+ if (metadata) params.push(JSON.stringify(metadata))
139
+ await this.sendRequest('connect', params, { timeout, signal })
140
+ return this.#switchRelaysAfterConnect()
141
+ }
142
+
143
+ // Sends a positional-string request after the response listener is ready.
144
+ sendRequest (method, params = [], options = {}) {
145
+ return this.#transport.sendRequest(this.#pointer.remoteSignerPubkey, method, params, options)
146
+ }
147
+
148
+ async ping (options) {
149
+ const response = await this.sendRequest('ping', [], options)
150
+ if (response !== 'pong') throw new Error(`NIP46_PING_FAILED:${response}`)
151
+ }
152
+
153
+ async logout (options) {
154
+ const response = await this.sendRequest('logout', [], options)
155
+ if (response !== 'ack') throw new Error(`NIP46_LOGOUT_FAILED:${response}`)
156
+ await this.close()
157
+ }
158
+
159
+ // Moves to the relay list returned by the remote signer, if it changed.
160
+ async switchRelays (options) {
161
+ const response = await this.sendRequest('switch_relays', [], options)
162
+ if (response === null) return false
163
+ let relays
164
+ try {
165
+ relays = JSON.parse(response)
166
+ } catch {
167
+ return false
168
+ }
169
+ if (relays === null) return false
170
+
171
+ const nextPointer = normalizeBunkerPointer({ ...this.#pointer, relays })
172
+ if (!nextPointer || sameRelays(nextPointer.relays, this.#pointer.relays)) return false
173
+
174
+ const nextContext = this.#openResponseContext(nextPointer)
175
+ try {
176
+ await this.#transport.awaitContextReady(nextContext)
177
+ } catch {
178
+ await this.#transport.closeContext(nextContext)
179
+ return false
180
+ }
181
+
182
+ this.#pointer = nextPointer
183
+ this.#transport.activateContext(nextContext)
184
+ return true
185
+ }
186
+
187
+ close () {
188
+ return this.#transport.close()
189
+ }
190
+
191
+ async #switchRelaysAfterConnect () {
192
+ try {
193
+ return await this.switchRelays({ timeout: RELAY_SWITCH_WAIT_TIMEOUT })
194
+ } catch {
195
+ return false
196
+ }
197
+ }
198
+
199
+ #openResponseContext (pointer) {
200
+ let context
201
+ context = this.#transport.openContext({
202
+ filter: {
203
+ kinds: [NIP46_KIND],
204
+ authors: [pointer.remoteSignerPubkey],
205
+ '#p': [this.#transport.pubkey],
206
+ limit: 0
207
+ },
208
+ relays: pointer.relays,
209
+ onEvent: event => this.#receiveEvent(event, context)
210
+ })
211
+ return context
212
+ }
213
+
214
+ #awaitReady (options) {
215
+ return this.#transport.awaitContextReady(this.#transport.activeContext, options)
216
+ }
217
+
218
+ #receiveEvent (event, context) {
219
+ if (event?.pubkey !== this.#pointer.remoteSignerPubkey ||
220
+ !isNip46EventFor(event, this.#transport.pubkey) ||
221
+ !this.#transport.isNewEvent(event)) return
222
+ const frame = decodeNip46Frame(event, this.#secretKey)
223
+ if (!frame) return
224
+
225
+ if (Object.hasOwn(frame, 'method')) {
226
+ return this.#handleRequest(event.pubkey, frame, context)
227
+ }
228
+ this.#transport.receiveResponse(event.pubkey, frame, { onAuthUrl: this.#onAuthUrl })
229
+ }
230
+
231
+ async #handleRequest (peerPubkey, request, context) {
232
+ if (!validRequestFrame(request)) {
233
+ if (typeof request?.id === 'string') {
234
+ await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_INVALID_REQUEST', { context })
235
+ }
236
+ return
237
+ }
238
+
239
+ try {
240
+ if (!this.#onRequest) throw new Error('NIP46_METHOD_NOT_SUPPORTED')
241
+ const result = await this.#onRequest({
242
+ method: request.method,
243
+ params: [...request.params],
244
+ peerPubkey
245
+ })
246
+ if (typeof result !== 'string') throw new Error('NIP46_RESULT_REQUIRED')
247
+ await this.#transport.reply(peerPubkey, request.id, result, null, { context })
248
+ } catch (error) {
249
+ await this.#transport.reply(peerPubkey, request.id, null, error?.message || 'NIP46_REQUEST_REJECTED', { context })
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,225 @@
1
+ import { relayPool as defaultRelayPool } from '../../relay/index.js'
2
+ import { DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_AFTER_FIRST_EOSE, NIP46_KIND } from '../constants/index.js'
3
+ import {
4
+ decodeNip46Frame,
5
+ isNip46EventFor,
6
+ validRequestFrame
7
+ } from '../helpers/frame.js'
8
+ import { Nip46Transport, sameRelays } from './transport.js'
9
+
10
+ function cleanRelays (relays) {
11
+ return [...new Set((Array.isArray(relays) ? relays : [])
12
+ .filter(relay => typeof relay === 'string' && relay))]
13
+ }
14
+
15
+ function parseClientMetadata (value) {
16
+ if (!value) return null
17
+ try {
18
+ const metadata = JSON.parse(value)
19
+ return metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? metadata : null
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ // A one-secret, single-client NIP-46 server session.
26
+ export class Nip46ServerSession {
27
+ #secretKey
28
+ #secret
29
+ #secretConsumed = false
30
+ #transport
31
+ #relays
32
+ #clientPubkey = null
33
+ #connectingPubkey = null
34
+ #pendingContext = null
35
+ #onConnect
36
+ #onRequest
37
+ #onLogout
38
+
39
+ constructor (serverSecretKey, {
40
+ relays,
41
+ secret = '',
42
+ relayPool = defaultRelayPool,
43
+ onConnect,
44
+ onRequest,
45
+ onLogout,
46
+ onError,
47
+ timeout = DEFAULT_TIMEOUT,
48
+ timeoutAfterFirstEose = DEFAULT_TIMEOUT_AFTER_FIRST_EOSE
49
+ } = {}) {
50
+ this.#relays = cleanRelays(relays)
51
+ if (!this.#relays.length) throw new Error('NIP46_RELAYS_REQUIRED')
52
+ if (typeof secret !== 'string') throw new Error('NIP46_SECRET_REQUIRED')
53
+ this.#secretKey = serverSecretKey
54
+ this.#secret = secret
55
+ this.#onConnect = onConnect
56
+ this.#onRequest = onRequest
57
+ this.#onLogout = onLogout
58
+ this.#transport = new Nip46Transport(serverSecretKey, {
59
+ relayPool,
60
+ networkTimeout: timeout,
61
+ timeoutAfterFirstEose,
62
+ onError
63
+ })
64
+ }
65
+
66
+ get serverPubkey () { return this.#transport.pubkey }
67
+ get clientPubkey () { return this.#clientPubkey }
68
+ get relays () { return Object.freeze([...this.#relays]) }
69
+ get readyRelays () { return this.#transport.readyRelays }
70
+
71
+ // Opens the live request listener and waits until at least one relay is ready.
72
+ start (options) {
73
+ if (!this.#transport.activeContext) {
74
+ this.#transport.activateContext(this.#openContext(this.#relays), { retirePrevious: false })
75
+ }
76
+ return this.#transport.awaitContextReady(this.#transport.activeContext, options)
77
+ }
78
+
79
+ // Sends an application-defined request to the connected client.
80
+ sendRequest (method, params = [], options = {}) {
81
+ if (!this.#clientPubkey) return Promise.reject(new Error('NIP46_NOT_CONNECTED'))
82
+ return this.#transport.sendRequest(this.#clientPubkey, method, params, options)
83
+ }
84
+
85
+ // Opens replacements now and switches only after the client requests them.
86
+ async updateRelays (relays) {
87
+ const nextRelays = cleanRelays(relays)
88
+ if (!nextRelays.length) throw new Error('NIP46_RELAYS_REQUIRED')
89
+ if (sameRelays(nextRelays, this.#relays)) return false
90
+ if (!this.#transport.activeContext) {
91
+ this.#relays = nextRelays
92
+ return true
93
+ }
94
+
95
+ const nextContext = this.#openContext(nextRelays)
96
+ try {
97
+ await this.#transport.awaitContextReady(nextContext)
98
+ } catch (error) {
99
+ await this.#transport.closeContext(nextContext)
100
+ throw error
101
+ }
102
+ if (this.#pendingContext) await this.#transport.closeContext(this.#pendingContext)
103
+ this.#relays = nextRelays
104
+ this.#pendingContext = nextContext
105
+ return true
106
+ }
107
+
108
+ close () {
109
+ this.#pendingContext = null
110
+ return this.#transport.close()
111
+ }
112
+
113
+ #openContext (relays) {
114
+ let context
115
+ context = this.#transport.openContext({
116
+ filter: {
117
+ kinds: [NIP46_KIND],
118
+ '#p': [this.#transport.pubkey],
119
+ limit: 0
120
+ },
121
+ relays,
122
+ onEvent: event => this.#receiveEvent(event, context)
123
+ })
124
+ return context
125
+ }
126
+
127
+ #receiveEvent (event, context) {
128
+ if (!isNip46EventFor(event, this.#transport.pubkey) || !this.#transport.isNewEvent(event)) return
129
+ const frame = decodeNip46Frame(event, this.#secretKey)
130
+ if (!frame) return
131
+
132
+ if (!Object.hasOwn(frame, 'method')) {
133
+ if (event.pubkey === this.#clientPubkey) this.#transport.receiveResponse(event.pubkey, frame)
134
+ return
135
+ }
136
+ return this.#handleRequest(event.pubkey, frame, context)
137
+ }
138
+
139
+ async #handleRequest (peerPubkey, request, context) {
140
+ if (!validRequestFrame(request)) {
141
+ if (typeof request?.id === 'string') {
142
+ await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_INVALID_REQUEST', { context })
143
+ }
144
+ return
145
+ }
146
+
147
+ if (request.method === 'connect') {
148
+ await this.#handleConnect(peerPubkey, request, context)
149
+ return
150
+ }
151
+ if (!this.#clientPubkey || peerPubkey !== this.#clientPubkey) {
152
+ await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_NOT_CONNECTED', { context })
153
+ return
154
+ }
155
+
156
+ try {
157
+ if (request.method === 'ping') {
158
+ await this.#transport.reply(peerPubkey, request.id, 'pong', null, { context })
159
+ return
160
+ }
161
+ if (request.method === 'switch_relays') {
162
+ await this.#transport.reply(peerPubkey, request.id, JSON.stringify(this.#relays), null, { context })
163
+ if (this.#pendingContext) {
164
+ const nextContext = this.#pendingContext
165
+ this.#pendingContext = null
166
+ const previous = this.#transport.activateContext(nextContext, { retirePrevious: false })
167
+ if (previous && previous !== nextContext) this.#transport.retireContext(previous)
168
+ }
169
+ return
170
+ }
171
+ if (request.method === 'logout') {
172
+ await this.#transport.reply(peerPubkey, request.id, 'ack', null, { context })
173
+ await this.#onLogout?.({ clientPubkey: peerPubkey })
174
+ await this.close()
175
+ return
176
+ }
177
+ if (!this.#onRequest) throw new Error('NIP46_METHOD_NOT_SUPPORTED')
178
+ const result = await this.#onRequest({
179
+ method: request.method,
180
+ params: [...request.params],
181
+ clientPubkey: peerPubkey
182
+ })
183
+ if (typeof result !== 'string') throw new Error('NIP46_RESULT_REQUIRED')
184
+ await this.#transport.reply(peerPubkey, request.id, result, null, { context })
185
+ } catch (error) {
186
+ await this.#transport.reply(peerPubkey, request.id, null, error?.message || 'NIP46_REQUEST_REJECTED', { context })
187
+ }
188
+ }
189
+
190
+ async #handleConnect (peerPubkey, request, context) {
191
+ if (this.#clientPubkey || this.#connectingPubkey || this.#secretConsumed) {
192
+ await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_ALREADY_CONNECTED', { context })
193
+ return
194
+ }
195
+ if (request.params[0] !== this.#transport.pubkey || request.params[1] !== this.#secret) {
196
+ await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_INVALID_SECRET', { context })
197
+ return
198
+ }
199
+
200
+ const requestedPermissions = request.params[2]
201
+ ? request.params[2].split(',').filter(Boolean)
202
+ : []
203
+ const clientMetadata = parseClientMetadata(request.params[3])
204
+ this.#connectingPubkey = peerPubkey
205
+ try {
206
+ await this.#onConnect?.({ peerPubkey, requestedPermissions, clientMetadata })
207
+ } catch (error) {
208
+ this.#connectingPubkey = null
209
+ await this.#transport.reply(peerPubkey, request.id, null, error?.message || 'NIP46_CONNECT_REJECTED', { context })
210
+ return
211
+ }
212
+
213
+ this.#clientPubkey = peerPubkey
214
+ this.#secretConsumed = true
215
+ try {
216
+ await this.#transport.reply(peerPubkey, request.id, 'ack', null, { context })
217
+ } catch (error) {
218
+ this.#clientPubkey = null
219
+ this.#secretConsumed = false
220
+ throw error
221
+ } finally {
222
+ this.#connectingPubkey = null
223
+ }
224
+ }
225
+ }