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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/package.json +1 -1
  3. 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 { assertValidPublicRelayUrl, normalizeRelayUrl } from 'libp2r2p/url'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
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 hostname = url.hostname.toLowerCase()
101
- if (
102
- hostname === 'localhost' ||
103
- hostname.endsWith('.localhost') ||
104
- hostname.endsWith('.local') ||
105
- hostname.endsWith('.onion')
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
+ }