libp2r2p 0.10.3 → 0.10.5
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/README.md +16 -1
- package/nip46/services/client.js +3 -1
- package/nip46/services/transport.js +12 -9
- package/package.json +1 -1
- package/url/index.js +86 -16
package/README.md
CHANGED
|
@@ -264,9 +264,18 @@ import {
|
|
|
264
264
|
import { generateSecretKey, getPublicKey } from 'libp2r2p/key'
|
|
265
265
|
import { eventKinds, classifyKind } from 'libp2r2p/kind'
|
|
266
266
|
import * as nip44 from 'libp2r2p/nip44'
|
|
267
|
-
import {
|
|
267
|
+
import {
|
|
268
|
+
assertValidPublicBlossomServerUrl,
|
|
269
|
+
assertValidPublicRelayUrl,
|
|
270
|
+
normalizeBlossomServerUrl,
|
|
271
|
+
normalizeRelayUrl
|
|
272
|
+
} from 'libp2r2p/url'
|
|
268
273
|
```
|
|
269
274
|
|
|
275
|
+
Blossom server normalization accepts root `http:` origins for local
|
|
276
|
+
development. Public Blossom validation additionally requires `https:` and a
|
|
277
|
+
public host, and rejects credentials, paths, query strings and fragments.
|
|
278
|
+
|
|
270
279
|
`classifyEvent()` from `libp2r2p/event` combines the exact NIP-01 kind
|
|
271
280
|
ranges with tag-defined behavior. The first `d` tag may add `replaceable` or
|
|
272
281
|
`addressable`, while an `expiration` tag equal to `created_at` adds
|
|
@@ -278,6 +287,12 @@ NIP-44 v2 uses the interoperable `nip44-v2` salt by default. A custom UTF-8
|
|
|
278
287
|
salt of at most 32 bytes may be passed to `getConversationKey()`, but messages
|
|
279
288
|
derived with it are not interoperable with standard NIP-44 implementations.
|
|
280
289
|
|
|
290
|
+
NIP-46 clients and bunker signers use a 30-second operation timeout by
|
|
291
|
+
default. Set `timeout` in the `Nip46Client`/`BunkerSigner` constructor to
|
|
292
|
+
choose another default, override it for an individual `connect()` or RPC, or
|
|
293
|
+
pass `timeout: null` explicitly when an operation is intentionally allowed to
|
|
294
|
+
wait indefinitely.
|
|
295
|
+
|
|
281
296
|
Nostr Web Tokens are available from `libp2r2p/nwt`. Creation returns a signed
|
|
282
297
|
kind `27519` event, while transport encoding is kept separate:
|
|
283
298
|
|
package/nip46/services/client.js
CHANGED
|
@@ -129,7 +129,9 @@ export class Nip46Client {
|
|
|
129
129
|
get pointer () { return { ...this.#pointer, relays: [...this.#pointer.relays] } }
|
|
130
130
|
|
|
131
131
|
// Connects and immediately asks the remote signer for its preferred relays.
|
|
132
|
-
|
|
132
|
+
// An omitted timeout inherits the configurable constructor timeout; null
|
|
133
|
+
// explicitly disables the response deadline.
|
|
134
|
+
async connect ({ requestedPermissions = [], clientMetadata, timeout, signal } = {}) {
|
|
133
135
|
const permissions = Array.isArray(requestedPermissions)
|
|
134
136
|
? requestedPermissions.filter(permission => typeof permission === 'string' && permission).join(',')
|
|
135
137
|
: ''
|
|
@@ -59,7 +59,7 @@ export class Nip46Transport {
|
|
|
59
59
|
#secretKey
|
|
60
60
|
#pubkey
|
|
61
61
|
#relayPool
|
|
62
|
-
#
|
|
62
|
+
#operationTimeout
|
|
63
63
|
#timeoutAfterFirstEose
|
|
64
64
|
#onError
|
|
65
65
|
#contexts = new Set()
|
|
@@ -80,7 +80,7 @@ export class Nip46Transport {
|
|
|
80
80
|
this.#secretKey = secretKey
|
|
81
81
|
this.#pubkey = getPublicKey(secretKey)
|
|
82
82
|
this.#relayPool = relayPool
|
|
83
|
-
this.#
|
|
83
|
+
this.#operationTimeout = networkTimeout
|
|
84
84
|
this.#timeoutAfterFirstEose = timeoutAfterFirstEose
|
|
85
85
|
this.#onError = onError
|
|
86
86
|
}
|
|
@@ -111,7 +111,7 @@ export class Nip46Transport {
|
|
|
111
111
|
return context
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async awaitContextReady (context, { timeout = this.#
|
|
114
|
+
async awaitContextReady (context, { timeout = this.#operationTimeout, signal } = {}) {
|
|
115
115
|
const report = await waitForNip46(context.stream.ready, {
|
|
116
116
|
timeout,
|
|
117
117
|
signal,
|
|
@@ -159,11 +159,14 @@ export class Nip46Transport {
|
|
|
159
159
|
return true
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
async sendRequest (peerPubkey, method, params = [], {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
async sendRequest (peerPubkey, method, params = [], options = {}) {
|
|
163
|
+
const {
|
|
164
|
+
// Undefined inherits the constructor-wide operation timeout. Passing
|
|
165
|
+
// null explicitly is the opt-out for RPCs that may wait indefinitely.
|
|
166
|
+
timeout = this.#operationTimeout,
|
|
167
|
+
signal,
|
|
168
|
+
extension
|
|
169
|
+
} = options
|
|
167
170
|
if (this.#closed) throw new Error('NIP46_CLOSED')
|
|
168
171
|
if (typeof method !== 'string' || !method) throw new ValidationError('NIP46_METHOD_REQUIRED')
|
|
169
172
|
if (!Array.isArray(params) || !params.every(param => typeof param === 'string')) {
|
|
@@ -229,7 +232,7 @@ export class Nip46Transport {
|
|
|
229
232
|
if (!relays.length) throw new Error('NIP46_NO_READY_RELAYS')
|
|
230
233
|
const event = createNip46Event({ secretKey: this.#secretKey, recipientPubkey: peerPubkey, payload })
|
|
231
234
|
const published = await this.#relayPool.sendEvent(event, relays, {
|
|
232
|
-
timeout: this.#
|
|
235
|
+
timeout: this.#operationTimeout,
|
|
233
236
|
timeoutUntilFirstFulfillment: null
|
|
234
237
|
})
|
|
235
238
|
if (!published.success) {
|
package/package.json
CHANGED
package/url/index.js
CHANGED
|
@@ -53,6 +53,30 @@ function removeEmptyQuerySegments (url) {
|
|
|
53
53
|
url.searchParams.sort()
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function publicHostError (url, {
|
|
57
|
+
invalidHostCode,
|
|
58
|
+
nonPublicHostCode,
|
|
59
|
+
nonPublicIpCode
|
|
60
|
+
}) {
|
|
61
|
+
const hostname = url.hostname.toLowerCase().replace(/\.+$/, '')
|
|
62
|
+
if (
|
|
63
|
+
hostname === 'localhost' ||
|
|
64
|
+
hostname.endsWith('.localhost') ||
|
|
65
|
+
hostname.endsWith('.local') ||
|
|
66
|
+
hostname.endsWith('.onion')
|
|
67
|
+
) return nonPublicHostCode
|
|
68
|
+
|
|
69
|
+
const unwrappedHostname = hostname.replace(/^\[|\]$/g, '')
|
|
70
|
+
if (isIpv4Address(unwrappedHostname)) {
|
|
71
|
+
if (!isPublicIpv4Address(unwrappedHostname)) return nonPublicIpCode
|
|
72
|
+
} else if (unwrappedHostname.includes(':')) {
|
|
73
|
+
if (!isPublicIpv6Address(unwrappedHostname)) return nonPublicIpCode
|
|
74
|
+
} else if (!hostname.includes('.')) {
|
|
75
|
+
return invalidHostCode
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
export function normalizeRelayUrl (value) {
|
|
57
81
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
58
82
|
throw new ValidationError('INVALID_RELAY_URL', { message: 'URL_SHOULD_BE_A_STRING' })
|
|
@@ -76,6 +100,36 @@ export function normalizeRelayUrl (value) {
|
|
|
76
100
|
return url.toString().replace(/^(wss?:\/\/[^/?#]+)\/(?=[?#]|$)/, '$1')
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
// Canonicalizes the origin used by the root-level Blossom HTTP endpoints.
|
|
104
|
+
export function normalizeBlossomServerUrl (value) {
|
|
105
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
106
|
+
throw new ValidationError('INVALID_BLOSSOM_SERVER_URL', { message: 'URL_SHOULD_BE_A_STRING' })
|
|
107
|
+
}
|
|
108
|
+
const input = value.trim()
|
|
109
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
|
|
110
|
+
throw new ValidationError('INVALID_BLOSSOM_SERVER_URL', { message: 'ABSOLUTE_URL_REQUIRED' })
|
|
111
|
+
}
|
|
112
|
+
let url
|
|
113
|
+
try {
|
|
114
|
+
url = new URL(input)
|
|
115
|
+
} catch (cause) {
|
|
116
|
+
throw new ValidationError('INVALID_BLOSSOM_SERVER_URL', { cause })
|
|
117
|
+
}
|
|
118
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
119
|
+
throw new ValidationError('INVALID_BLOSSOM_SERVER_PROTOCOL')
|
|
120
|
+
}
|
|
121
|
+
if (url.username || url.password) {
|
|
122
|
+
throw new ValidationError('BLOSSOM_SERVER_URL_CREDENTIALS_NOT_ALLOWED')
|
|
123
|
+
}
|
|
124
|
+
if (!/^\/*$/.test(url.pathname)) {
|
|
125
|
+
throw new ValidationError('BLOSSOM_SERVER_URL_PATH_NOT_ALLOWED')
|
|
126
|
+
}
|
|
127
|
+
if (url.search) throw new ValidationError('BLOSSOM_SERVER_URL_QUERY_NOT_ALLOWED')
|
|
128
|
+
if (url.hash) throw new ValidationError('BLOSSOM_SERVER_URL_FRAGMENT_NOT_ALLOWED')
|
|
129
|
+
url.hostname = url.hostname.replace(/\.+$/, '')
|
|
130
|
+
return url.origin
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
function publicRelayUrlError (value) {
|
|
80
134
|
if (typeof value !== 'string' || value.trim().length === 0) return 'INVALID_RELAY_URL'
|
|
81
135
|
let input = value.trim()
|
|
@@ -97,22 +151,12 @@ function publicRelayUrlError (value) {
|
|
|
97
151
|
if (url.protocol !== 'wss:') return 'INSECURE_RELAY_URL'
|
|
98
152
|
if (url.username || url.password) return 'RELAY_URL_CREDENTIALS_NOT_ALLOWED'
|
|
99
153
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
) return 'NON_PUBLIC_RELAY_HOST'
|
|
107
|
-
|
|
108
|
-
const unwrappedHostname = hostname.replace(/^\[|\]$/g, '')
|
|
109
|
-
if (isIpv4Address(unwrappedHostname)) {
|
|
110
|
-
if (!isPublicIpv4Address(unwrappedHostname)) return 'NON_PUBLIC_RELAY_IP'
|
|
111
|
-
} else if (unwrappedHostname.includes(':')) {
|
|
112
|
-
if (!isPublicIpv6Address(unwrappedHostname)) return 'NON_PUBLIC_RELAY_IP'
|
|
113
|
-
} else if (!hostname.includes('.')) {
|
|
114
|
-
return 'INVALID_RELAY_HOST'
|
|
115
|
-
}
|
|
154
|
+
const hostError = publicHostError(url, {
|
|
155
|
+
invalidHostCode: 'INVALID_RELAY_HOST',
|
|
156
|
+
nonPublicHostCode: 'NON_PUBLIC_RELAY_HOST',
|
|
157
|
+
nonPublicIpCode: 'NON_PUBLIC_RELAY_IP'
|
|
158
|
+
})
|
|
159
|
+
if (hostError) return hostError
|
|
116
160
|
|
|
117
161
|
const lowerValue = normalized.toLowerCase()
|
|
118
162
|
if (lowerValue.includes('npub1') || lowerValue.includes('nprofile1')) return 'RELAY_URL_NOSTR_ENTITY_NOT_ALLOWED'
|
|
@@ -129,3 +173,29 @@ export function assertValidPublicRelayUrl (value) {
|
|
|
129
173
|
if (code) throw new ValidationError(code)
|
|
130
174
|
return value
|
|
131
175
|
}
|
|
176
|
+
|
|
177
|
+
function publicBlossomServerUrlError (value) {
|
|
178
|
+
let normalized
|
|
179
|
+
try {
|
|
180
|
+
normalized = normalizeBlossomServerUrl(value)
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return error instanceof ValidationError ? error.code : 'INVALID_BLOSSOM_SERVER_URL'
|
|
183
|
+
}
|
|
184
|
+
const url = new URL(normalized)
|
|
185
|
+
if (url.protocol !== 'https:') return 'INSECURE_BLOSSOM_SERVER_URL'
|
|
186
|
+
return publicHostError(url, {
|
|
187
|
+
invalidHostCode: 'INVALID_BLOSSOM_SERVER_HOST',
|
|
188
|
+
nonPublicHostCode: 'NON_PUBLIC_BLOSSOM_SERVER_HOST',
|
|
189
|
+
nonPublicIpCode: 'NON_PUBLIC_BLOSSOM_SERVER_IP'
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isValidPublicBlossomServerUrl (value) {
|
|
194
|
+
return publicBlossomServerUrlError(value) === null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function assertValidPublicBlossomServerUrl (value) {
|
|
198
|
+
const code = publicBlossomServerUrlError(value)
|
|
199
|
+
if (code) throw new ValidationError(code)
|
|
200
|
+
return value
|
|
201
|
+
}
|