libp2r2p 0.10.4 → 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 +10 -1
- 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
|
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
|
+
}
|