libp2r2p 0.10.9 → 0.10.10
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 +7 -3
- package/nip05/helpers/nip05-identifier.js +23 -8
- package/nip05/index.js +5 -2
- package/nip27/helpers/user-reference.js +30 -9
- package/nip27/index.js +96 -38
- package/package.json +1 -1
- package/relay/services/events.js +4 -1
- package/url/app-url.js +54 -19
package/README.md
CHANGED
|
@@ -342,13 +342,17 @@ are handled by `libp2r2p/nip27`: `decodeUserReference()` returns the decoded
|
|
|
342
342
|
form with its canonical compact spelling, `encodeUserReference()` returns
|
|
343
343
|
that canonical spelling, and `resolveUserReference()` resolves it to a
|
|
344
344
|
pubkey. `libp2r2p/nip05` keeps only `queryProfile()`, the NIP-05 lookup, which
|
|
345
|
-
accepts the compact custom forms directly.
|
|
345
|
+
accepts the compact custom forms directly. The decoders (`decodeReference`,
|
|
346
|
+
`decodeMediaMetadata`, `decodeUserReference`, `decodeAppUrl`) throw
|
|
347
|
+
`ValidationError` with a stable code; each has a `tryDecode…` counterpart
|
|
348
|
+
that returns `null` when the value cannot be decoded.
|
|
346
349
|
|
|
347
350
|
Public validity checks consistently use a non-throwing `is…` predicate plus an
|
|
348
351
|
`assert…` counterpart when callers need the exact reason. Strict codecs,
|
|
349
352
|
decoders, token validation, and malformed public arguments also throw
|
|
350
|
-
`ValidationError` from `libp2r2p/error
|
|
351
|
-
|
|
353
|
+
`ValidationError` from `libp2r2p/error`; probing code can use the
|
|
354
|
+
non-throwing `tryDecode…` variants instead of catching. Network, timeout,
|
|
355
|
+
abort, quota, and closed-state failures remain ordinary operational errors.
|
|
352
356
|
|
|
353
357
|
NIP-04 remains available at
|
|
354
358
|
`libp2r2p/nip04` only for compatibility with older Nostr applications.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ValidationError } from '../../error/index.js'
|
|
2
|
+
|
|
1
3
|
const NIP05_LOCAL = /^[a-z0-9._-]+$/
|
|
2
4
|
const NIP05_DOMAIN = /^[a-z0-9.-]+$/
|
|
3
5
|
|
|
@@ -26,23 +28,36 @@ export function nip05FromLocalDomain (local, domain) {
|
|
|
26
28
|
// - `local@domain` (standard NIP-05)
|
|
27
29
|
// - `domain` with exactly one dot -> root `_@domain`
|
|
28
30
|
// - `local.domain...` with more than one dot -> local part + domain (custom extension)
|
|
31
|
+
// Throws `ValidationError('INVALID_NIP05_IDENTIFIER')` for malformed input.
|
|
29
32
|
export function decodeNip05Identifier (value) {
|
|
30
|
-
if (typeof value !== 'string')
|
|
33
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
34
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'IDENTIFIER_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
35
|
+
}
|
|
31
36
|
const text = value.trim().toLowerCase()
|
|
32
|
-
if (!text) return null
|
|
33
37
|
|
|
34
38
|
const at = text.lastIndexOf('@')
|
|
35
39
|
if (at !== -1) {
|
|
36
|
-
if (at === 0 || at === text.length - 1 || text.includes('@', at + 1))
|
|
37
|
-
|
|
40
|
+
if (at === 0 || at === text.length - 1 || text.includes('@', at + 1)) {
|
|
41
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'INVALID_NIP05_FORMAT' })
|
|
42
|
+
}
|
|
43
|
+
const result = nip05FromLocalDomain(text.slice(0, at), text.slice(at + 1))
|
|
44
|
+
if (!result) {
|
|
45
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'INVALID_NIP05_LOCAL_OR_DOMAIN' })
|
|
46
|
+
}
|
|
47
|
+
return result
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
if (!text.includes('.'))
|
|
50
|
+
if (!text.includes('.')) {
|
|
51
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'INVALID_NIP05_FORMAT' })
|
|
52
|
+
}
|
|
41
53
|
const firstDot = text.indexOf('.')
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
const result = text.slice(firstDot + 1).includes('.')
|
|
55
|
+
? nip05FromLocalDomain(text.slice(0, firstDot), text.slice(firstDot + 1))
|
|
56
|
+
: nip05FromLocalDomain('_', text)
|
|
57
|
+
if (!result) {
|
|
58
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'INVALID_NIP05_LOCAL_OR_DOMAIN' })
|
|
44
59
|
}
|
|
45
|
-
return
|
|
60
|
+
return result
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
// Returns the most compact unambiguous NIP-05 spelling:
|
package/nip05/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from '../error/index.js'
|
|
1
2
|
import { normalizeRelayUrl } from '../url/index.js'
|
|
2
3
|
import { decodeNip05Identifier } from './helpers/nip05-identifier.js'
|
|
3
4
|
|
|
@@ -8,9 +9,11 @@ export async function queryProfile (identifier, {
|
|
|
8
9
|
signal,
|
|
9
10
|
timeoutMs = 5000
|
|
10
11
|
} = {}) {
|
|
11
|
-
if (typeof identifier !== 'string' ||
|
|
12
|
+
if (typeof identifier !== 'string' || !identifier.trim()) {
|
|
13
|
+
throw new ValidationError('INVALID_NIP05_IDENTIFIER', { message: 'IDENTIFIER_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
14
|
+
}
|
|
15
|
+
if (typeof fetchImpl !== 'function') return null
|
|
12
16
|
const nip05 = decodeNip05Identifier(identifier)
|
|
13
|
-
if (!nip05) return null
|
|
14
17
|
const name = nip05.local
|
|
15
18
|
const domain = nip05.domain
|
|
16
19
|
|
|
@@ -26,11 +26,17 @@ function stripReferencePrefix (value) {
|
|
|
26
26
|
// Decodes a user reference without performing any network lookup.
|
|
27
27
|
// Returns `{ type: 'pubkey', pubkey, relays, raw }` for npub/nprofile/hex or
|
|
28
28
|
// `{ type: 'nip05', local, domain, raw }` for NIP-05 (standard or extended),
|
|
29
|
-
// where `raw` is always the most compact canonical spelling.
|
|
29
|
+
// where `raw` is always the most compact canonical spelling. Throws
|
|
30
|
+
// `ValidationError('INVALID_USER_REFERENCE')` when the value cannot be
|
|
31
|
+
// decoded; use `tryDecodeUserReference` when a null result is preferred.
|
|
30
32
|
export function decodeUserReference (value) {
|
|
31
|
-
if (typeof value !== 'string')
|
|
33
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
34
|
+
throw new ValidationError('INVALID_USER_REFERENCE', { message: 'USER_REFERENCE_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
35
|
+
}
|
|
32
36
|
const text = stripReferencePrefix(value)
|
|
33
|
-
if (!text)
|
|
37
|
+
if (!text) {
|
|
38
|
+
throw new ValidationError('INVALID_USER_REFERENCE', { message: 'EMPTY_USER_REFERENCE' })
|
|
39
|
+
}
|
|
34
40
|
|
|
35
41
|
if (HEX_PUBKEY.test(text)) {
|
|
36
42
|
const raw = text.toLowerCase()
|
|
@@ -41,8 +47,8 @@ export function decodeUserReference (value) {
|
|
|
41
47
|
try {
|
|
42
48
|
const raw = text.toLowerCase()
|
|
43
49
|
return { type: 'pubkey', pubkey: npubDecode(raw), relays: [], raw }
|
|
44
|
-
} catch {
|
|
45
|
-
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
throw new ValidationError('INVALID_USER_REFERENCE', { message: 'INVALID_NPUB', cause })
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -51,17 +57,32 @@ export function decodeUserReference (value) {
|
|
|
51
57
|
const raw = text.toLowerCase()
|
|
52
58
|
const { pubkey, relays } = nprofileDecode(raw)
|
|
53
59
|
return { type: 'pubkey', pubkey, relays, raw }
|
|
54
|
-
} catch {
|
|
55
|
-
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new ValidationError('INVALID_USER_REFERENCE', { message: 'INVALID_NPROFILE', cause })
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
let nip05
|
|
66
|
+
try {
|
|
67
|
+
nip05 = decodeNip05Identifier(text)
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
throw new ValidationError('INVALID_USER_REFERENCE', { message: 'INVALID_NIP05', cause })
|
|
70
|
+
}
|
|
61
71
|
const raw = compactNip05Raw(nip05.local, nip05.domain)
|
|
62
72
|
return { type: 'nip05', ...nip05, raw }
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
// Non-throwing variant of `decodeUserReference`: returns the decoded
|
|
76
|
+
// reference or `null` when the value is not a valid user reference.
|
|
77
|
+
export function tryDecodeUserReference (value) {
|
|
78
|
+
try {
|
|
79
|
+
return decodeUserReference(value)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof ValidationError) return null
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
// Returns the canonical compact spelling for a user reference, either as a
|
|
66
87
|
// string or as a decoded reference object.
|
|
67
88
|
export function encodeUserReference (value) {
|
package/nip27/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from '../error/index.js'
|
|
1
2
|
import {
|
|
2
3
|
naddrDecode,
|
|
3
4
|
neventDecode,
|
|
@@ -8,10 +9,11 @@ import { queryProfile } from '../nip05/index.js'
|
|
|
8
9
|
import { normalizeRelayUrl } from '../url/index.js'
|
|
9
10
|
import {
|
|
10
11
|
decodeUserReference,
|
|
11
|
-
encodeUserReference
|
|
12
|
+
encodeUserReference,
|
|
13
|
+
tryDecodeUserReference
|
|
12
14
|
} from './helpers/user-reference.js'
|
|
13
15
|
|
|
14
|
-
export { decodeUserReference, encodeUserReference }
|
|
16
|
+
export { decodeUserReference, encodeUserReference, tryDecodeUserReference }
|
|
15
17
|
|
|
16
18
|
const BECH32_BODY = '[ac-hj-np-z02-9]'
|
|
17
19
|
const BOUNDARY_PREFIX = /(?<=^|[\s"«„「¡¿:{([])/.source
|
|
@@ -107,18 +109,30 @@ function normalizeRelays (relays) {
|
|
|
107
109
|
.filter(Boolean)
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
function looksLikeUserReference (text) {
|
|
113
|
+
return /^[0-9a-f]{64}$/i.test(text) ||
|
|
114
|
+
/^(?:npub1|nprofile1)/i.test(text) ||
|
|
115
|
+
text.includes('@') ||
|
|
116
|
+
/^[a-z0-9._-]+(?:\.[a-z0-9-]+)+$/i.test(text)
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
// Parses a single NIP-27-style reference (with optional `@`/`nostr:`
|
|
111
120
|
// prefixes): NIP-05 (including the custom compact forms), npub/nprofile/hex
|
|
112
|
-
// accounts, note/nevent/naddr events and nrelay relays.
|
|
121
|
+
// accounts, note/nevent/naddr events and nrelay relays. Throws
|
|
122
|
+
// `ValidationError` when the value cannot be decoded; use
|
|
123
|
+
// `tryDecodeReference` when a null result is preferred.
|
|
113
124
|
export function decodeReference (value) {
|
|
114
|
-
if (typeof value !== 'string')
|
|
125
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
126
|
+
throw new ValidationError('INVALID_REFERENCE', { message: 'REFERENCE_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
127
|
+
}
|
|
115
128
|
const original = value.trim()
|
|
116
|
-
if (!original) return null
|
|
117
129
|
const text = stripReferencePrefix(original)
|
|
118
|
-
if (!text)
|
|
130
|
+
if (!text) {
|
|
131
|
+
throw new ValidationError('INVALID_REFERENCE', { message: 'EMPTY_REFERENCE' })
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
if (looksLikeUserReference(text)) {
|
|
135
|
+
const account = decodeUserReference(text)
|
|
122
136
|
return account.type === 'pubkey'
|
|
123
137
|
? {
|
|
124
138
|
type: 'pubkey',
|
|
@@ -137,34 +151,29 @@ export function decodeReference (value) {
|
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
if (text.startsWith('note1')) {
|
|
140
|
-
|
|
141
|
-
return { type: 'note', original, value: text, id: noteDecode(text) }
|
|
142
|
-
} catch {
|
|
143
|
-
return null
|
|
144
|
-
}
|
|
154
|
+
return { type: 'note', original, value: text, id: noteDecode(text) }
|
|
145
155
|
}
|
|
146
156
|
if (text.startsWith('nevent1')) {
|
|
147
|
-
|
|
148
|
-
return { type: 'nevent', original, value: text, ...neventDecode(text) }
|
|
149
|
-
} catch {
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
157
|
+
return { type: 'nevent', original, value: text, ...neventDecode(text) }
|
|
152
158
|
}
|
|
153
159
|
if (text.startsWith('naddr1')) {
|
|
154
|
-
|
|
155
|
-
return { type: 'naddr', original, value: text, ...naddrDecode(text) }
|
|
156
|
-
} catch {
|
|
157
|
-
return null
|
|
158
|
-
}
|
|
160
|
+
return { type: 'naddr', original, value: text, ...naddrDecode(text) }
|
|
159
161
|
}
|
|
160
162
|
if (text.startsWith('nrelay1')) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
return { type: 'nrelay', original, value: text, relay: nrelayDecode(text) }
|
|
164
|
+
}
|
|
165
|
+
throw new ValidationError('INVALID_REFERENCE', { message: 'UNRECOGNIZED_REFERENCE' })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Non-throwing variant of `decodeReference`: returns the decoded reference
|
|
169
|
+
// or `null` when the value is not a valid reference.
|
|
170
|
+
export function tryDecodeReference (value) {
|
|
171
|
+
try {
|
|
172
|
+
return decodeReference(value)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof ValidationError) return null
|
|
175
|
+
throw error
|
|
166
176
|
}
|
|
167
|
-
return null
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
const NIP94_TAGS = {
|
|
@@ -185,11 +194,33 @@ const NIP94_TAGS = {
|
|
|
185
194
|
caption: ['caption']
|
|
186
195
|
}
|
|
187
196
|
|
|
197
|
+
function validateTagConfigs (extraTags) {
|
|
198
|
+
if (!extraTags || typeof extraTags !== 'object' || Array.isArray(extraTags)) {
|
|
199
|
+
throw new ValidationError('INVALID_MEDIA_METADATA_TAGS', { message: 'EXTRA_TAGS_SHOULD_BE_AN_OBJECT' })
|
|
200
|
+
}
|
|
201
|
+
for (const [key, config] of Object.entries(extraTags)) {
|
|
202
|
+
const valid = Array.isArray(config) && config.length > 0 && config.every(entry =>
|
|
203
|
+
typeof entry === 'string' ||
|
|
204
|
+
(entry && typeof entry === 'object' && typeof entry.key === 'string' &&
|
|
205
|
+
(entry.type === undefined || entry.type === 'string' || entry.type === 'array'))
|
|
206
|
+
)
|
|
207
|
+
if (!valid) {
|
|
208
|
+
throw new ValidationError('INVALID_MEDIA_METADATA_TAGS', { message: `INVALID_TAG_CONFIG:${key}` })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
188
213
|
// Decodes the file/media metadata carried in a URL fragment
|
|
189
214
|
// (`#m=image/png&dim=640x480&alt=...`). Kept generic on purpose: the old
|
|
190
|
-
// draft number (54) was taken by an unrelated NIP.
|
|
215
|
+
// draft number (54) was taken by an unrelated NIP. Throws
|
|
216
|
+
// `ValidationError` for invalid URLs, tag configs or malformed `dim` values;
|
|
217
|
+
// use `tryDecodeMediaMetadata` when a null result is preferred. A URL
|
|
218
|
+
// without a metadata fragment still decodes to `{}`.
|
|
191
219
|
export function decodeMediaMetadata (url, { extraTags } = {}) {
|
|
192
|
-
if (typeof url !== 'string' || !url)
|
|
220
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
221
|
+
throw new ValidationError('INVALID_MEDIA_METADATA_URL', { message: 'URL_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
222
|
+
}
|
|
223
|
+
if (extraTags !== undefined) validateTagConfigs(extraTags)
|
|
193
224
|
|
|
194
225
|
const tags = extraTags ? { ...NIP94_TAGS, ...extraTags } : NIP94_TAGS
|
|
195
226
|
const tagIndexes = {}
|
|
@@ -217,12 +248,26 @@ export function decodeMediaMetadata (url, { extraTags } = {}) {
|
|
|
217
248
|
const { width, height } = obj.dim.match(
|
|
218
249
|
/(?<width>[1-9]{1}[0-9]{0,10})(?:\s*[xX]\s*)(?<height>[1-9]{1}[0-9]{0,10})/
|
|
219
250
|
)?.groups ?? {}
|
|
220
|
-
if (width
|
|
221
|
-
|
|
251
|
+
if (width === undefined || height === undefined) {
|
|
252
|
+
throw new ValidationError('INVALID_MEDIA_METADATA_DIM', { message: 'DIM_SHOULD_BE_WIDTHxHEIGHT' })
|
|
253
|
+
}
|
|
254
|
+
obj.width = width
|
|
255
|
+
obj.height = height
|
|
222
256
|
}
|
|
223
257
|
return obj
|
|
224
258
|
}
|
|
225
259
|
|
|
260
|
+
// Non-throwing variant of `decodeMediaMetadata`: returns the decoded
|
|
261
|
+
// metadata or `null` when the URL/tags/dim cannot be decoded.
|
|
262
|
+
export function tryDecodeMediaMetadata (url, options) {
|
|
263
|
+
try {
|
|
264
|
+
return decodeMediaMetadata(url, options)
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof ValidationError) return null
|
|
267
|
+
throw error
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
226
271
|
function decodeFragmentValue (value) {
|
|
227
272
|
try {
|
|
228
273
|
return decodeURIComponent(value.replace(/\+/g, '%20'))
|
|
@@ -234,7 +279,13 @@ function decodeFragmentValue (value) {
|
|
|
234
279
|
function getReferenceItem (original, groups, { getMimeType }) {
|
|
235
280
|
if (groups.url) {
|
|
236
281
|
const url = `${groups.protocol ? '' : 'https://'}${groups.url}`
|
|
237
|
-
|
|
282
|
+
let mediaMetadata = {}
|
|
283
|
+
try {
|
|
284
|
+
mediaMetadata = decodeMediaMetadata(url)
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (!(error instanceof ValidationError)) throw error
|
|
287
|
+
}
|
|
288
|
+
const urlItem = { value: url, ...(groups.ext && { ext: groups.ext }), ...mediaMetadata }
|
|
238
289
|
if (!urlItem.m && typeof getMimeType === 'function') {
|
|
239
290
|
const mime = getMimeType({ url, ext: groups.ext })
|
|
240
291
|
if (mime) urlItem.m = mime
|
|
@@ -249,7 +300,7 @@ function getReferenceItem (original, groups, { getMimeType }) {
|
|
|
249
300
|
groups.nip05BareRoot ||
|
|
250
301
|
groups.nip05BareCustom
|
|
251
302
|
) {
|
|
252
|
-
const account =
|
|
303
|
+
const account = tryDecodeUserReference(original)
|
|
253
304
|
if (!account) return null
|
|
254
305
|
return {
|
|
255
306
|
key: 'nip05',
|
|
@@ -266,7 +317,13 @@ function getReferenceItem (original, groups, { getMimeType }) {
|
|
|
266
317
|
return { key: 'hashtag', hashtag: { value: groups.hashtag } }
|
|
267
318
|
}
|
|
268
319
|
|
|
269
|
-
|
|
320
|
+
let ref
|
|
321
|
+
try {
|
|
322
|
+
ref = decodeReference(original)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (!(error instanceof ValidationError)) throw error
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
270
327
|
if (!ref) return null
|
|
271
328
|
switch (ref.type) {
|
|
272
329
|
case 'pubkey': {
|
|
@@ -320,7 +377,9 @@ function getReferenceItem (original, groups, { getMimeType }) {
|
|
|
320
377
|
// `{ bareNip05: true }` is passed, since they are otherwise indistinguishable
|
|
321
378
|
// from plain hostnames; prefixed forms (`@bob.example.com`) always work.
|
|
322
379
|
export function extractMedia (content, { bareNip05 = false, getMimeType } = {}) {
|
|
323
|
-
if (typeof content !== 'string')
|
|
380
|
+
if (typeof content !== 'string') {
|
|
381
|
+
throw new ValidationError('INVALID_MEDIA_CONTENT', { message: 'CONTENT_SHOULD_BE_A_STRING' })
|
|
382
|
+
}
|
|
324
383
|
const regex = getReferencesRegex(bareNip05)
|
|
325
384
|
const items = []
|
|
326
385
|
let end = 0
|
|
@@ -349,7 +408,6 @@ export function extractMedia (content, { bareNip05 = false, getMimeType } = {})
|
|
|
349
408
|
// spellings); npub/nprofile/hex are resolved locally.
|
|
350
409
|
export async function resolveUserReference (value, options = {}) {
|
|
351
410
|
const account = decodeUserReference(value)
|
|
352
|
-
if (!account) return null
|
|
353
411
|
if (account.type === 'pubkey') {
|
|
354
412
|
return { pubkey: account.pubkey, relays: account.relays, label: account.raw }
|
|
355
413
|
}
|
package/package.json
CHANGED
package/relay/services/events.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from '../../error/index.js'
|
|
1
2
|
import { freeRelays } from '../constants/index.js'
|
|
2
3
|
import { pickRelaysForPubkeys } from '../helpers/routing.js'
|
|
3
4
|
import { relayPool } from './relay-pool.js'
|
|
@@ -99,7 +100,9 @@ export async function getLatestEventsByPubkey (pubkeys, {
|
|
|
99
100
|
} = {}) {
|
|
100
101
|
const authors = [...new Set(pubkeys || [])].filter(Boolean)
|
|
101
102
|
if (!authors.length) return { events: [], byPubkey: {}, relaysByPubkey: {} }
|
|
102
|
-
if (!Array.isArray(kinds) || kinds.length === 0)
|
|
103
|
+
if (!Array.isArray(kinds) || kinds.length === 0) {
|
|
104
|
+
throw new ValidationError('MISSING_EVENT_KINDS', { message: 'Missing kinds' })
|
|
105
|
+
}
|
|
103
106
|
const type = relayType === 'read' ? 'read' : 'write'
|
|
104
107
|
|
|
105
108
|
const relaysByAuthor = { ...(relaysByPubkey || {}) }
|
package/url/app-url.js
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
} from '../nip05/helpers/nip05-identifier.js'
|
|
6
6
|
import {
|
|
7
7
|
decodeUserReference,
|
|
8
|
-
encodeUserReference
|
|
8
|
+
encodeUserReference,
|
|
9
|
+
tryDecodeUserReference
|
|
9
10
|
} from '../nip27/helpers/user-reference.js'
|
|
10
11
|
import {
|
|
11
12
|
NAPP_ENTITY_REGEX,
|
|
@@ -55,8 +56,10 @@ function isValidAppName (appName) {
|
|
|
55
56
|
// `naddr` already carries the event kind, so it does not need the `+`/`++`/
|
|
56
57
|
// `+++` channel prefix (a leading prefix is still tolerated). Only site
|
|
57
58
|
// manifests are app URLs; the result is canonicalized to the `appEncode`
|
|
58
|
-
// entity so downstream code keeps working unchanged.
|
|
59
|
-
|
|
59
|
+
// entity so downstream code keeps working unchanged. Throws
|
|
60
|
+
// `ValidationError('INVALID_APP_URL_NADDR')` for malformed naddr or
|
|
61
|
+
// naddr that is not a site manifest.
|
|
62
|
+
function decodeNaddrSegment (value) {
|
|
60
63
|
if (typeof value !== 'string' || !value) return null
|
|
61
64
|
const body = value.replace(/^\+{1,3}/, '')
|
|
62
65
|
if (!body.startsWith(NADDR_PREFIX)) return null
|
|
@@ -64,10 +67,12 @@ function tryDecodeNaddr (value) {
|
|
|
64
67
|
let decoded
|
|
65
68
|
try {
|
|
66
69
|
decoded = naddrDecode(body)
|
|
67
|
-
} catch {
|
|
68
|
-
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
throw new ValidationError('INVALID_APP_URL_NADDR', { message: 'INVALID_NADDR', cause })
|
|
72
|
+
}
|
|
73
|
+
if (!SITE_MANIFEST_KINDS.has(decoded.kind)) {
|
|
74
|
+
throw new ValidationError('INVALID_APP_URL_NADDR', { message: 'NOT_SITE_MANIFEST' })
|
|
69
75
|
}
|
|
70
|
-
if (!SITE_MANIFEST_KINDS.has(decoded.kind)) return null
|
|
71
76
|
|
|
72
77
|
try {
|
|
73
78
|
return {
|
|
@@ -79,8 +84,8 @@ function tryDecodeNaddr (value) {
|
|
|
79
84
|
relays: decoded.relays
|
|
80
85
|
})
|
|
81
86
|
}
|
|
82
|
-
} catch {
|
|
83
|
-
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
throw new ValidationError('INVALID_APP_URL_NADDR', { message: 'INVALID_APP_ENTITY', cause })
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -90,27 +95,34 @@ function tryDecodeNaddr (value) {
|
|
|
90
95
|
// - `{ type: 'entity', entity }` for NIP-19 app entities;
|
|
91
96
|
// - `{ type: 'named', prefix, channel, appName, user }` for named URLs
|
|
92
97
|
// (`user` is null when no user part is present or it is invalid);
|
|
93
|
-
//
|
|
98
|
+
// Throws `ValidationError` when the segment is not a valid app URL; use
|
|
99
|
+
// `tryDecodeAppUrl` when a null result is preferred.
|
|
94
100
|
export function decodeAppUrl (segment) {
|
|
95
|
-
if (typeof segment !== 'string' || !segment)
|
|
96
|
-
|
|
101
|
+
if (typeof segment !== 'string' || !segment) {
|
|
102
|
+
throw new ValidationError('INVALID_APP_URL', { message: 'URL_SEGMENT_SHOULD_BE_A_NON_EMPTY_STRING' })
|
|
103
|
+
}
|
|
104
|
+
const decodedNaddr = decodeNaddrSegment(segment)
|
|
97
105
|
if (decodedNaddr) return decodedNaddr
|
|
98
106
|
|
|
99
107
|
const prefixMatch = segment.match(/^\+{1,3}/)
|
|
100
|
-
if (!prefixMatch)
|
|
108
|
+
if (!prefixMatch) {
|
|
109
|
+
throw new ValidationError('INVALID_APP_URL', { message: 'MISSING_APP_URL_PREFIX' })
|
|
110
|
+
}
|
|
101
111
|
const prefix = prefixMatch[0]
|
|
102
112
|
|
|
103
113
|
if (NAPP_ENTITY_REGEX.test(segment)) {
|
|
104
114
|
try {
|
|
105
115
|
appDecode(segment)
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
throw new ValidationError('INVALID_APP_URL_ENTITY', { message: 'INVALID_APP_ENTITY', cause })
|
|
108
118
|
}
|
|
109
119
|
return { type: 'entity', entity: segment }
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
const remainder = segment.slice(prefix.length)
|
|
113
|
-
if (!remainder)
|
|
123
|
+
if (!remainder) {
|
|
124
|
+
throw new ValidationError('INVALID_APP_URL', { message: 'MISSING_APP_NAME' })
|
|
125
|
+
}
|
|
114
126
|
const parts = remainder.split('@')
|
|
115
127
|
let appName
|
|
116
128
|
let user = null
|
|
@@ -120,7 +132,7 @@ export function decodeAppUrl (segment) {
|
|
|
120
132
|
} else if (parts.length === 2) {
|
|
121
133
|
const tail = safeDecode(parts[1])
|
|
122
134
|
appName = safeDecode(parts[0])
|
|
123
|
-
user = tail === null ? null :
|
|
135
|
+
user = tail === null ? null : tryDecodeUserReference(tail)
|
|
124
136
|
} else {
|
|
125
137
|
const local = safeDecode(parts[parts.length - 2])
|
|
126
138
|
const domain = safeDecode(parts[parts.length - 1])
|
|
@@ -133,8 +145,12 @@ export function decodeAppUrl (segment) {
|
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
if (appName === null || !isValidAppName(appName))
|
|
137
|
-
|
|
148
|
+
if (appName === null || !isValidAppName(appName)) {
|
|
149
|
+
throw new ValidationError('INVALID_APP_URL_NAME', { message: 'Invalid app URL name' })
|
|
150
|
+
}
|
|
151
|
+
if (!user && appName.length >= APP_URL_MIN_ENTITY_BODY_LENGTH) {
|
|
152
|
+
throw new ValidationError('INVALID_APP_URL_ENTITY', { message: 'ENTITY_LIKE_URL_WITHOUT_USER' })
|
|
153
|
+
}
|
|
138
154
|
|
|
139
155
|
return {
|
|
140
156
|
type: 'named',
|
|
@@ -145,6 +161,17 @@ export function decodeAppUrl (segment) {
|
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
163
|
|
|
164
|
+
// Non-throwing variant of `decodeAppUrl`: returns the decoded app URL or
|
|
165
|
+
// `null` when the segment is not a valid app URL.
|
|
166
|
+
export function tryDecodeAppUrl (segment) {
|
|
167
|
+
try {
|
|
168
|
+
return decodeAppUrl(segment)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof ValidationError) return null
|
|
171
|
+
throw error
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
148
175
|
// Encodes a named app URL segment. `user` is a decoded user reference:
|
|
149
176
|
// NIP-05 (`bob@example.com`, `_@example.com`, `example.com` or the custom
|
|
150
177
|
// `bob.xyz.example.com` form), npub, nprofile or hex pubkey.
|
|
@@ -156,7 +183,15 @@ export function encodeAppUrl ({ appName, channel = 'main', user }) {
|
|
|
156
183
|
if (!prefix) {
|
|
157
184
|
throw new ValidationError('INVALID_APP_URL_CHANNEL', { message: 'Invalid app URL channel' })
|
|
158
185
|
}
|
|
159
|
-
|
|
186
|
+
let userRef
|
|
187
|
+
try {
|
|
188
|
+
userRef = decodeUserReference(user)
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
if (cause instanceof ValidationError) {
|
|
191
|
+
throw new ValidationError('INVALID_APP_URL_USER', { message: cause.message ?? 'Invalid app URL user', cause })
|
|
192
|
+
}
|
|
193
|
+
throw cause
|
|
194
|
+
}
|
|
160
195
|
if (!userRef) {
|
|
161
196
|
throw new ValidationError('INVALID_APP_URL_USER', { message: 'Invalid app URL user' })
|
|
162
197
|
}
|