libp2r2p 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +72 -45
  2. package/base16/index.js +6 -3
  3. package/base36/index.js +12 -8
  4. package/base62/index.js +15 -11
  5. package/base64/index.js +18 -2
  6. package/base93/index.js +19 -7
  7. package/content-key/event/index.js +40 -12
  8. package/double-dh/index.js +21 -6
  9. package/ecdh/index.js +12 -1
  10. package/error/index.js +32 -0
  11. package/event/helpers/serialize.js +31 -0
  12. package/event/index.js +116 -0
  13. package/idb/index.js +5 -3
  14. package/idb-queue/index.js +21 -20
  15. package/index.js +10 -1
  16. package/key/index.js +31 -18
  17. package/kind/index.js +244 -0
  18. package/nip04/index.js +47 -0
  19. package/nip05/index.js +61 -0
  20. package/nip19/index.js +176 -43
  21. package/nip44/helpers.js +95 -0
  22. package/nip44/index.js +14 -0
  23. package/nip44-v3/index.js +35 -16
  24. package/nip46/helpers/frame.js +6 -6
  25. package/nip46/helpers/url.js +8 -6
  26. package/nip46/services/bunker-signer.js +7 -6
  27. package/nip46/services/client.js +8 -7
  28. package/nip46/services/server-session.js +8 -7
  29. package/nip46/services/transport.js +9 -8
  30. package/nip96/index.js +285 -0
  31. package/nip98/index.js +56 -0
  32. package/nwt/index.js +241 -0
  33. package/package.json +22 -5
  34. package/private-channel/helpers/chunks.js +7 -6
  35. package/private-channel/helpers/event.js +5 -4
  36. package/private-channel/index.js +63 -61
  37. package/private-channel/services/received-chunks.js +4 -3
  38. package/private-message/index.js +32 -31
  39. package/private-messenger/index.js +23 -22
  40. package/private-messenger/recovery/index.js +15 -14
  41. package/private-messenger/services/channel-state.js +2 -1
  42. package/relay/helpers/hll.js +3 -1
  43. package/relay/services/query.js +3 -3
  44. package/relay/services/relay-connection.js +222 -121
  45. package/relay/services/relay-pool.js +13 -10
  46. package/temporary-storage/index.js +3 -1
  47. package/url/index.js +131 -0
  48. package/web-storage-queue/index.js +8 -6
  49. package/i18n/index.js +0 -235
package/nip44-v3/index.js CHANGED
@@ -5,6 +5,7 @@ import { concatBytes, utf8ToBytes } from '@noble/hashes/utils.js'
5
5
  import { chacha20 } from '@noble/ciphers/chacha.js'
6
6
  import { bytesToBase64, base64ToBytes } from '../base64/index.js'
7
7
  import { sharedXOnlySecret } from '../ecdh/index.js'
8
+ import { ValidationError } from '../error/index.js'
8
9
 
9
10
  // NIP-44 v3 — local implementation (spec.nostr.land/nip44v3)
10
11
  // Copied from the bunker testbench and verified against the vendored
@@ -34,7 +35,7 @@ function readU32be (b, off) {
34
35
  return new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(off, false)
35
36
  }
36
37
 
37
- function equalBytes (a, b) {
38
+ function areBytesEqual (a, b) {
38
39
  if (a.length !== b.length) return false
39
40
  let diff = 0
40
41
  for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]
@@ -47,7 +48,15 @@ function randomBytes32 () {
47
48
  return bytes
48
49
  }
49
50
 
51
+ function assertBytes (value, code, length) {
52
+ if (!(value instanceof Uint8Array) || (length !== undefined && value.length !== length)) {
53
+ throw new ValidationError(code)
54
+ }
55
+ return value
56
+ }
57
+
50
58
  export function deriveKeys (seckey, pubkey, nonce) {
59
+ assertBytes(nonce, 'INVALID_NONCE', 32)
51
60
  const shared = sharedXOnlySecret(seckey, pubkey)
52
61
  const salt = concatBytes(utf8ToBytes('nip44-v3\x00'), nonce)
53
62
  const prk = hkdfExtract(sha256, shared, salt)
@@ -71,6 +80,8 @@ export function payloadByteLength (plaintextByteLength, scopeByteLength = 0) {
71
80
  }
72
81
 
73
82
  export function deriveKeysFromConversationKey (conversationKey, nonce) {
83
+ assertBytes(conversationKey, 'INVALID_CONVERSATION_KEY', 32)
84
+ assertBytes(nonce, 'INVALID_NONCE', 32)
74
85
  const salt = concatBytes(utf8ToBytes('nip44-v3\x00'), nonce)
75
86
  const prk = hkdfExtract(sha256, conversationKey, salt)
76
87
  return {
@@ -87,6 +98,11 @@ export function encryptBytes (seckey, pubkey, kind, scope, plaintext, nonce) {
87
98
 
88
99
  export function encryptWithConversationKeyBytes (conversationKey, kind, scope, plaintext, nonce) {
89
100
  nonce ??= randomBytes32()
101
+ assertBytes(conversationKey, 'INVALID_CONVERSATION_KEY', 32)
102
+ kind = normalizeKind(kind)
103
+ assertBytes(scope, 'INVALID_SCOPE')
104
+ assertBytes(plaintext, 'INVALID_PLAINTEXT')
105
+ assertBytes(nonce, 'INVALID_NONCE', 32)
90
106
  const { encryption_key: encryptionKey, mac_key: macKey } = deriveKeysFromConversationKey(conversationKey, nonce)
91
107
  const prefixed = concatBytes(u32be(plaintext.length), plaintext)
92
108
  const padded = new Uint8Array(targetSize(prefixed.length))
@@ -102,34 +118,37 @@ export function decryptBytes (seckey, pubkey, expectedKind, expectedScope, ciphe
102
118
  }
103
119
 
104
120
  export function decryptWithConversationKeyBytes (conversationKey, expectedKind, expectedScope, ciphertext) {
105
- if (!ciphertext || ciphertext.length === 0) throw new Error('empty ciphertext')
106
- if (ciphertext[0] === '#') throw new Error('unsupported future version')
121
+ assertBytes(conversationKey, 'INVALID_CONVERSATION_KEY', 32)
122
+ expectedKind = normalizeKind(expectedKind)
123
+ assertBytes(expectedScope, 'INVALID_SCOPE')
124
+ if (typeof ciphertext !== 'string' || ciphertext.length === 0) throw new ValidationError('EMPTY_CIPHERTEXT', { message: 'empty ciphertext' })
125
+ if (ciphertext[0] === '#') throw new ValidationError('UNSUPPORTED_NIP44_VERSION', { message: 'unsupported future version' })
107
126
  let decoded
108
- try { decoded = base64ToBytes(ciphertext) } catch { throw new Error('invalid base64') }
109
- if (decoded.length < 77) throw new Error('ciphertext too short')
110
- if (decoded[0] !== VERSION) throw new Error(`unsupported version ${decoded[0]}`)
127
+ try { decoded = base64ToBytes(ciphertext) } catch (cause) { throw new ValidationError('INVALID_BASE64', { message: 'invalid base64', cause }) }
128
+ if (decoded.length < 77) throw new ValidationError('NIP44_CIPHERTEXT_TOO_SHORT', { message: 'ciphertext too short' })
129
+ if (decoded[0] !== VERSION) throw new ValidationError('UNSUPPORTED_NIP44_VERSION', { message: `unsupported version ${decoded[0]}` })
111
130
  const nonce = decoded.subarray(1, 33)
112
131
  const mac = decoded.subarray(33, 65)
113
132
  const kind = readU32be(decoded, 65)
114
133
  const scopeLength = readU32be(decoded, 69)
115
- if (scopeLength > decoded.length - 73) throw new Error('invalid scope length')
134
+ if (scopeLength > decoded.length - 73) throw new ValidationError('INVALID_NIP44_SCOPE_LENGTH', { message: 'invalid scope length' })
116
135
  const scope = decoded.subarray(73, 73 + scopeLength)
117
- try { fatalTextDecoder.decode(scope) } catch { throw new Error('scope is not valid UTF-8') }
136
+ try { fatalTextDecoder.decode(scope) } catch (cause) { throw new ValidationError('INVALID_NIP44_SCOPE_UTF8', { message: 'scope is not valid UTF-8', cause }) }
118
137
  const ct = decoded.subarray(73 + scopeLength)
119
- if (ct.length < 4) throw new Error('ciphertext too short')
120
- if (kind !== expectedKind) throw new Error(`kind mismatch: got ${kind}, expected ${expectedKind}`)
121
- if (!equalBytes(scope, expectedScope)) throw new Error('scope mismatch')
138
+ if (ct.length < 4) throw new ValidationError('NIP44_CIPHERTEXT_TOO_SHORT', { message: 'ciphertext too short' })
139
+ if (kind !== expectedKind) throw new ValidationError('NIP44_KIND_MISMATCH', { message: `kind mismatch: got ${kind}, expected ${expectedKind}` })
140
+ if (!areBytesEqual(scope, expectedScope)) throw new ValidationError('NIP44_SCOPE_MISMATCH', { message: 'scope mismatch' })
122
141
  const { encryption_key: encryptionKey, mac_key: macKey } = deriveKeysFromConversationKey(conversationKey, nonce)
123
142
  const authData = concatBytes(nonce, u32be(kind), u32be(scope.length), scope, ct)
124
- if (!equalBytes(mac, hmac(sha256, macKey, authData))) throw new Error('invalid MAC')
143
+ if (!areBytesEqual(mac, hmac(sha256, macKey, authData))) throw new ValidationError('INVALID_MAC', { message: 'invalid MAC' })
125
144
  const padded = chacha(encryptionKey, ct)
126
145
  const plaintextLength = readU32be(padded, 0)
127
- if (plaintextLength + 4 > padded.length) throw new Error('invalid plaintext length')
128
- if (plaintextLength > 2 ** 31 - 1) throw new Error('plaintext too long')
146
+ if (plaintextLength + 4 > padded.length) throw new ValidationError('INVALID_PLAINTEXT_LENGTH', { message: 'invalid plaintext length' })
147
+ if (plaintextLength > 2 ** 31 - 1) throw new ValidationError('PLAINTEXT_TOO_LONG', { message: 'plaintext too long' })
129
148
  // Only verify the padding is all-zeroes. Per spec, implementations MUST NOT do any
130
149
  // other check on the padding length — non-standard zero-padding must decrypt.
131
150
  const padding = padded.subarray(4 + plaintextLength)
132
- if (!equalBytes(padding, new Uint8Array(padding.length))) throw new Error('invalid padding')
151
+ if (!areBytesEqual(padding, new Uint8Array(padding.length))) throw new ValidationError('INVALID_PADDING', { message: 'invalid padding' })
133
152
  return padded.subarray(4, 4 + plaintextLength)
134
153
  }
135
154
 
@@ -139,7 +158,7 @@ function deriveSharedConversationKey (seckey, pubkey) {
139
158
 
140
159
  export function normalizeKind (kind) {
141
160
  const n = typeof kind === 'string' && kind.trim() !== '' ? Number(kind) : kind
142
- if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) throw new Error('INVALID_KIND')
161
+ if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) throw new ValidationError('INVALID_KIND')
143
162
  return n
144
163
  }
145
164
 
@@ -1,10 +1,10 @@
1
- import { finalizeEvent, verifyEvent } from 'nostr-tools'
2
- import { decrypt, encrypt, getConversationKey } from 'nostr-tools/nip44'
1
+ import { finalizeEvent, isValidEvent } from '../../event/index.js'
2
+ import { decrypt, encrypt, getConversationKey } from '../../nip44/index.js'
3
3
  import { NIP46_KIND } from '../constants/index.js'
4
4
 
5
5
  const PUBKEY = /^[0-9a-f]{64}$/
6
6
 
7
- export function validPubkey (value) {
7
+ export function isValidPubkey (value) {
8
8
  return typeof value === 'string' && PUBKEY.test(value)
9
9
  }
10
10
 
@@ -14,9 +14,9 @@ export function hasPTag (event, pubkey) {
14
14
 
15
15
  export function isNip46EventFor (event, pubkey) {
16
16
  return event?.kind === NIP46_KIND &&
17
- validPubkey(event.pubkey) &&
17
+ isValidPubkey(event.pubkey) &&
18
18
  hasPTag(event, pubkey) &&
19
- verifyEvent(event)
19
+ isValidEvent(event)
20
20
  }
21
21
 
22
22
  export function decodeNip46Frame (event, secretKey) {
@@ -38,7 +38,7 @@ export function createNip46Event ({ secretKey, recipientPubkey, payload }) {
38
38
  }, secretKey)
39
39
  }
40
40
 
41
- export function validRequestFrame (frame) {
41
+ export function isValidRequestFrame (frame) {
42
42
  return typeof frame?.id === 'string' && frame.id &&
43
43
  typeof frame.method === 'string' && frame.method &&
44
44
  Array.isArray(frame.params) && frame.params.every(param => typeof param === 'string')
@@ -1,3 +1,5 @@
1
+ import { ValidationError } from '../../error/index.js'
2
+
1
3
  const PUBKEY = /^[0-9a-f]{64}$/
2
4
 
3
5
  function uniqueStrings (values) {
@@ -5,7 +7,7 @@ function uniqueStrings (values) {
5
7
  return [...new Set(values.filter(value => typeof value === 'string' && value))]
6
8
  }
7
9
 
8
- function validPubkey (value) {
10
+ function isValidPubkey (value) {
9
11
  return typeof value === 'string' && PUBKEY.test(value)
10
12
  }
11
13
 
@@ -13,7 +15,7 @@ export function normalizeBunkerPointer (pointer) {
13
15
  if (!pointer || typeof pointer !== 'object') return null
14
16
  const remoteSignerPubkey = String(pointer.remoteSignerPubkey || '').toLowerCase()
15
17
  const relays = uniqueStrings(pointer.relays)
16
- if (!validPubkey(remoteSignerPubkey) || !relays.length) return null
18
+ if (!isValidPubkey(remoteSignerPubkey) || !relays.length) return null
17
19
  return {
18
20
  remoteSignerPubkey,
19
21
  relays,
@@ -39,7 +41,7 @@ export function parseBunkerUrl (input) {
39
41
  // Serializes a direct NIP-46 bunker pointer without any network lookup.
40
42
  export function toBunkerUrl (pointer) {
41
43
  const normalized = normalizeBunkerPointer(pointer)
42
- if (!normalized) throw new Error('INVALID_BUNKER_POINTER')
44
+ if (!normalized) throw new ValidationError('INVALID_BUNKER_POINTER')
43
45
  const url = new URL(`bunker://${normalized.remoteSignerPubkey}`)
44
46
  for (const relay of normalized.relays) url.searchParams.append('relay', relay)
45
47
  if (normalized.secret) url.searchParams.set('secret', normalized.secret)
@@ -58,8 +60,8 @@ export function createNostrConnectURI ({
58
60
  } = {}) {
59
61
  const normalizedPubkey = String(clientPubkey || '').toLowerCase()
60
62
  const normalizedRelays = uniqueStrings(relays)
61
- if (!validPubkey(normalizedPubkey) || !normalizedRelays.length || typeof secret !== 'string' || !secret) {
62
- throw new Error('INVALID_NOSTRCONNECT_URI')
63
+ if (!isValidPubkey(normalizedPubkey) || !normalizedRelays.length || typeof secret !== 'string' || !secret) {
64
+ throw new ValidationError('INVALID_NOSTRCONNECT_URI')
63
65
  }
64
66
 
65
67
  const uri = new URL(`nostrconnect://${normalizedPubkey}`)
@@ -78,7 +80,7 @@ export function parseNostrConnectURI (input) {
78
80
  const clientPubkey = url.hostname.toLowerCase()
79
81
  const relays = uniqueStrings(url.searchParams.getAll('relay'))
80
82
  const secret = url.searchParams.get('secret') || ''
81
- if (url.protocol !== 'nostrconnect:' || !validPubkey(clientPubkey) || !relays.length || !secret) return null
83
+ if (url.protocol !== 'nostrconnect:' || !isValidPubkey(clientPubkey) || !relays.length || !secret) return null
82
84
  return {
83
85
  clientPubkey,
84
86
  relays,
@@ -1,5 +1,6 @@
1
- import { verifyEvent } from 'nostr-tools'
2
- import { validPubkey } from '../helpers/frame.js'
1
+ import { ValidationError } from '../../error/index.js'
2
+ import { isValidEvent } from '../../event/index.js'
3
+ import { isValidPubkey } from '../helpers/frame.js'
3
4
  import { Nip46Client } from './client.js'
4
5
 
5
6
  // A NIP-46 remote signer with the standard Nostr signing commands.
@@ -14,7 +15,7 @@ export class BunkerSigner extends Nip46Client {
14
15
  async getPublicKey (options) {
15
16
  if (!options?.extension && this.#cachedPubkey) return this.#cachedPubkey
16
17
  const pubkey = await this.sendRequest('get_public_key', [], options)
17
- if (!validPubkey(pubkey)) throw new Error('NIP46_INVALID_PUBLIC_KEY')
18
+ if (!isValidPubkey(pubkey)) throw new ValidationError('NIP46_INVALID_PUBLIC_KEY')
18
19
  if (!options?.extension) this.#cachedPubkey = pubkey
19
20
  return pubkey
20
21
  }
@@ -24,10 +25,10 @@ export class BunkerSigner extends Nip46Client {
24
25
  let signed
25
26
  try {
26
27
  signed = JSON.parse(response)
27
- } catch {
28
- throw new Error('NIP46_INVALID_SIGNED_EVENT')
28
+ } catch (cause) {
29
+ throw new ValidationError('NIP46_INVALID_SIGNED_EVENT', { cause })
29
30
  }
30
- if (!verifyEvent(signed)) throw new Error('NIP46_INVALID_SIGNED_EVENT')
31
+ if (!isValidEvent(signed)) throw new ValidationError('NIP46_INVALID_SIGNED_EVENT')
31
32
  return signed
32
33
  }
33
34
 
@@ -1,4 +1,5 @@
1
- import { getPublicKey } from 'nostr-tools'
1
+ import { ValidationError } from '../../error/index.js'
2
+ import { getPublicKey } from '../../key/index.js'
2
3
  import { relayPool as defaultRelayPool } from '../../relay/index.js'
3
4
  import {
4
5
  DEFAULT_TIMEOUT,
@@ -10,10 +11,10 @@ import {
10
11
  decodeNip46Frame,
11
12
  isNip46EventFor,
12
13
  requestError,
13
- validRequestFrame
14
+ isValidRequestFrame
14
15
  } from '../helpers/frame.js'
15
16
  import { normalizeBunkerPointer, parseNostrConnectURI } from '../helpers/url.js'
16
- import { Nip46Transport, sameRelays, waitForNip46 } from './transport.js'
17
+ import { areRelaySetsEqual, Nip46Transport, waitForNip46 } from './transport.js'
17
18
 
18
19
  function cleanClientMetadata (value) {
19
20
  if (!value || typeof value !== 'object') return null
@@ -41,7 +42,7 @@ export class Nip46Client {
41
42
  timeoutAfterFirstEose = DEFAULT_TIMEOUT_AFTER_FIRST_EOSE
42
43
  } = {}) {
43
44
  const normalized = normalizeBunkerPointer(pointer)
44
- if (!normalized) throw new Error('INVALID_BUNKER_POINTER')
45
+ if (!normalized) throw new ValidationError('INVALID_BUNKER_POINTER')
45
46
  this.#secretKey = clientSecretKey
46
47
  this.#pointer = normalized
47
48
  this.#onAuthUrl = onAuthUrl
@@ -64,7 +65,7 @@ export class Nip46Client {
64
65
  static async fromURI (clientSecretKey, uri, options = {}) {
65
66
  const parsed = parseNostrConnectURI(uri)
66
67
  const clientPubkey = getPublicKey(clientSecretKey)
67
- if (!parsed || clientPubkey !== parsed.clientPubkey) throw new Error('INVALID_NOSTRCONNECT_URI')
68
+ if (!parsed || clientPubkey !== parsed.clientPubkey) throw new ValidationError('INVALID_NOSTRCONNECT_URI')
68
69
 
69
70
  const relayPool = options.relayPool || defaultRelayPool
70
71
  const controller = new AbortController()
@@ -169,7 +170,7 @@ export class Nip46Client {
169
170
  if (relays === null) return false
170
171
 
171
172
  const nextPointer = normalizeBunkerPointer({ ...this.#pointer, relays })
172
- if (!nextPointer || sameRelays(nextPointer.relays, this.#pointer.relays)) return false
173
+ if (!nextPointer || areRelaySetsEqual(nextPointer.relays, this.#pointer.relays)) return false
173
174
 
174
175
  const nextContext = this.#openResponseContext(nextPointer)
175
176
  try {
@@ -229,7 +230,7 @@ export class Nip46Client {
229
230
  }
230
231
 
231
232
  async #handleRequest (peerPubkey, request, context) {
232
- if (!validRequestFrame(request)) {
233
+ if (!isValidRequestFrame(request)) {
233
234
  if (typeof request?.id === 'string') {
234
235
  await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_INVALID_REQUEST', { context })
235
236
  }
@@ -1,11 +1,12 @@
1
+ import { ValidationError } from '../../error/index.js'
1
2
  import { relayPool as defaultRelayPool } from '../../relay/index.js'
2
3
  import { DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_AFTER_FIRST_EOSE, NIP46_KIND } from '../constants/index.js'
3
4
  import {
4
5
  decodeNip46Frame,
5
6
  isNip46EventFor,
6
- validRequestFrame
7
+ isValidRequestFrame
7
8
  } from '../helpers/frame.js'
8
- import { Nip46Transport, sameRelays } from './transport.js'
9
+ import { areRelaySetsEqual, Nip46Transport } from './transport.js'
9
10
 
10
11
  function cleanRelays (relays) {
11
12
  return [...new Set((Array.isArray(relays) ? relays : [])
@@ -48,8 +49,8 @@ export class Nip46ServerSession {
48
49
  timeoutAfterFirstEose = DEFAULT_TIMEOUT_AFTER_FIRST_EOSE
49
50
  } = {}) {
50
51
  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')
52
+ if (!this.#relays.length) throw new ValidationError('NIP46_RELAYS_REQUIRED')
53
+ if (typeof secret !== 'string') throw new ValidationError('NIP46_SECRET_REQUIRED')
53
54
  this.#secretKey = serverSecretKey
54
55
  this.#secret = secret
55
56
  this.#onConnect = onConnect
@@ -85,8 +86,8 @@ export class Nip46ServerSession {
85
86
  // Opens replacements now and switches only after the client requests them.
86
87
  async updateRelays (relays) {
87
88
  const nextRelays = cleanRelays(relays)
88
- if (!nextRelays.length) throw new Error('NIP46_RELAYS_REQUIRED')
89
- if (sameRelays(nextRelays, this.#relays)) return false
89
+ if (!nextRelays.length) throw new ValidationError('NIP46_RELAYS_REQUIRED')
90
+ if (areRelaySetsEqual(nextRelays, this.#relays)) return false
90
91
  if (!this.#transport.activeContext) {
91
92
  this.#relays = nextRelays
92
93
  return true
@@ -137,7 +138,7 @@ export class Nip46ServerSession {
137
138
  }
138
139
 
139
140
  async #handleRequest (peerPubkey, request, context) {
140
- if (!validRequestFrame(request)) {
141
+ if (!isValidRequestFrame(request)) {
141
142
  if (typeof request?.id === 'string') {
142
143
  await this.#transport.reply(peerPubkey, request.id, null, 'NIP46_INVALID_REQUEST', { context })
143
144
  }
@@ -1,4 +1,5 @@
1
- import { getPublicKey } from 'nostr-tools'
1
+ import { ValidationError } from '../../error/index.js'
2
+ import { getPublicKey } from '../../key/index.js'
2
3
  import { relayPool as defaultRelayPool } from '../../relay/index.js'
3
4
  import { DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_AFTER_FIRST_EOSE } from '../constants/index.js'
4
5
  import { createNip46Event, requestError } from '../helpers/frame.js'
@@ -14,9 +15,9 @@ function requestId () {
14
15
 
15
16
  function requestExtension (value) {
16
17
  if (value === undefined || value === null) return null
17
- if (typeof value !== 'object' || Array.isArray(value)) throw new Error('NIP46_REQUEST_EXTENSION_REQUIRED')
18
+ if (typeof value !== 'object' || Array.isArray(value)) throw new ValidationError('NIP46_REQUEST_EXTENSION_REQUIRED')
18
19
  for (const key of ['id', 'method', 'params']) {
19
- if (Object.hasOwn(value, key)) throw new Error(`NIP46_REQUEST_EXTENSION_CANNOT_SET_${key.toUpperCase()}`)
20
+ if (Object.hasOwn(value, key)) throw new ValidationError(`NIP46_REQUEST_EXTENSION_CANNOT_SET_${key.toUpperCase()}`)
20
21
  }
21
22
  return value
22
23
  }
@@ -46,7 +47,7 @@ export function waitForNip46 (promise, { timeout = null, signal, label = 'NIP46_
46
47
  })
47
48
  }
48
49
 
49
- export function sameRelays (left, right) {
50
+ export function areRelaySetsEqual (left, right) {
50
51
  if (left.length !== right.length) return false
51
52
  const values = new Set(left)
52
53
  return right.every(value => values.has(value))
@@ -74,8 +75,8 @@ export class Nip46Transport {
74
75
  timeoutAfterFirstEose = DEFAULT_TIMEOUT_AFTER_FIRST_EOSE,
75
76
  onError
76
77
  } = {}) {
77
- if (!(secretKey instanceof Uint8Array)) throw new Error('NIP46_SECRET_KEY_REQUIRED')
78
- if (!relayPool?.getLiveEventsGenerator || !relayPool?.sendEvent) throw new Error('RELAY_POOL_REQUIRED')
78
+ if (!(secretKey instanceof Uint8Array)) throw new ValidationError('NIP46_SECRET_KEY_REQUIRED')
79
+ if (!relayPool?.getLiveEventsGenerator || !relayPool?.sendEvent) throw new ValidationError('RELAY_POOL_REQUIRED')
79
80
  this.#secretKey = secretKey
80
81
  this.#pubkey = getPublicKey(secretKey)
81
82
  this.#relayPool = relayPool
@@ -164,9 +165,9 @@ export class Nip46Transport {
164
165
  extension
165
166
  } = {}) {
166
167
  if (this.#closed) throw new Error('NIP46_CLOSED')
167
- if (typeof method !== 'string' || !method) throw new Error('NIP46_METHOD_REQUIRED')
168
+ if (typeof method !== 'string' || !method) throw new ValidationError('NIP46_METHOD_REQUIRED')
168
169
  if (!Array.isArray(params) || !params.every(param => typeof param === 'string')) {
169
- throw new Error('NIP46_PARAMS_REQUIRED')
170
+ throw new ValidationError('NIP46_PARAMS_REQUIRED')
170
171
  }
171
172
  if (signal?.aborted) throw new Error('Aborted')
172
173
  const context = this.#activeContext