libp2r2p 0.10.15 → 0.10.17
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 +52 -1
- package/nip27/index.js +36 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,12 +371,63 @@ event. Their `assert…` counterparts return the original event or throw a
|
|
|
371
371
|
`ValidationError` with a stable code.
|
|
372
372
|
|
|
373
373
|
NIP-27 text references live in `libp2r2p/nip27`. `extractMedia()` splits
|
|
374
|
-
content into text, URL, profile, event, relay, NIP-05 and hashtag items in
|
|
374
|
+
content into text, URL, profile, event, relay, NIP-05, app and hashtag items in
|
|
375
375
|
occurrence order, accepting the optional `@` and `nostr:` mention prefixes
|
|
376
376
|
plus NIP-05 in its standard, root and custom compact spellings.
|
|
377
377
|
`decodeReference()` parses a single reference, and `decodeMediaMetadata()`
|
|
378
378
|
reads the file/media metadata carried in a URL fragment
|
|
379
379
|
(`#m=image/png&dim=640x480&...`).
|
|
380
|
+
URLs can contain literal `+` in their paths, including launcher links such as
|
|
381
|
+
`https://44billion.net/+apps?by=fiatjaf.com`. They produce a single URL item
|
|
382
|
+
with the full address, rather than an app item for the embedded reference.
|
|
383
|
+
|
|
384
|
+
App references are a library extension to NIP-27. `extractMedia()` recognizes
|
|
385
|
+
encoded `+…` app entities and named references such as `+hallway@fiatjaf.com`,
|
|
386
|
+
`++myapp@bob@example.com` and `+++myapp@npub1…`, with optional `nostr:` before
|
|
387
|
+
the entire reference. Named authors accept the NIP-05 spellings above, `npub`,
|
|
388
|
+
`nprofile` and hex pubkeys; app names may be URL-encoded. The one-to-three `+`
|
|
389
|
+
prefix selects `main`, `next` or `draft` respectively.
|
|
390
|
+
|
|
391
|
+
Bare app names use the `defaultAppAuthor` option, which defaults to
|
|
392
|
+
`'44billion.net'`: `+apps` identifies the same app as `+apps@44billion.net`.
|
|
393
|
+
The option accepts a NIP-05 reference (including compact spellings), `npub`,
|
|
394
|
+
`nprofile` or hex pubkey and is validated locally on every call. An invalid
|
|
395
|
+
value throws `ValidationError` with code `INVALID_DEFAULT_APP_AUTHOR`.
|
|
396
|
+
Explicit authors and encoded app entities are never overridden. Malformed
|
|
397
|
+
explicit authors remain text instead of falling back to the default.
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
extractMedia('+apps', { defaultAppAuthor: 'bob@example.com' })
|
|
401
|
+
// app.user: { type: 'nip05', local: 'bob', domain: 'example.com', raw: 'bob.example.com' }
|
|
402
|
+
// app.original remains '+apps'.
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
These references produce `{ key: 'app', app: { original, ...decoded } }`, where
|
|
406
|
+
`decoded` is the result of `decodeAppUrl()` from `libp2r2p/url`, with a missing
|
|
407
|
+
named author filled from `defaultAppAuthor` as described above. Named apps
|
|
408
|
+
include `type: 'named'`, `prefix`, `channel`, `appName` (the manifest's `d` tag)
|
|
409
|
+
and the decoded `user`; entities include `type: 'entity'` and `entity`, usable
|
|
410
|
+
with `appDecode()` from `libp2r2p/nip19`. Extraction performs no network lookup
|
|
411
|
+
and does not verify that the app exists. For example:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
extractMedia('Open nostr:++myapp@bob@example.com')
|
|
415
|
+
// [
|
|
416
|
+
// { key: 'text', text: { value: 'Open ' } },
|
|
417
|
+
// { key: 'app', app: {
|
|
418
|
+
// original: 'nostr:++myapp@bob@example.com', type: 'named',
|
|
419
|
+
// prefix: '++', channel: 'next', appName: 'myapp',
|
|
420
|
+
// user: { type: 'nip05', local: 'bob', domain: 'example.com', raw: 'bob.example.com' }
|
|
421
|
+
// } }
|
|
422
|
+
// ]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
App references must be standalone inline tokens, not concatenated to event
|
|
426
|
+
pointers or embedded in URL paths. Invalid app candidates remain text.
|
|
427
|
+
Bare names can use letters, numbers, dots, underscores, hyphens and tildes;
|
|
428
|
+
URL-encode spaces, `@` and other punctuation in them.
|
|
429
|
+
`+naddr1…` is recognized as an app only for site-manifest kinds; unprefixed
|
|
430
|
+
`naddr1…` and `nostr:naddr1…` keep their existing `event` item shape.
|
|
380
431
|
|
|
381
432
|
`compactWhitespace(text, options?)` is also exported from `libp2r2p/nip27` as an
|
|
382
433
|
opt-in display helper. It removes carriage returns, collapses spaces and
|
package/nip27/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ValidationError } from '../error/index.js'
|
|
2
2
|
import {
|
|
3
|
+
NAPP_ENTITY_REGEX,
|
|
3
4
|
naddrDecode,
|
|
4
5
|
neventDecode,
|
|
5
6
|
noteDecode,
|
|
6
7
|
nrelayDecode
|
|
7
8
|
} from '../nip19/index.js'
|
|
8
9
|
import { queryProfile } from '../nip05/index.js'
|
|
9
|
-
import { normalizeRelayUrl } from '../url/index.js'
|
|
10
|
+
import { normalizeRelayUrl, tryDecodeAppUrl } from '../url/index.js'
|
|
10
11
|
import {
|
|
11
12
|
decodeUserReference,
|
|
12
13
|
encodeUserReference,
|
|
@@ -23,7 +24,7 @@ const BOUNDARY_SUFFIX = /(?=\.?$|[.,]?\s|\.(?=\.)|\.?["»”」'!?;\])}])/.sourc
|
|
|
23
24
|
const URL_SOURCE =
|
|
24
25
|
'(?<url>' +
|
|
25
26
|
/(?<protocol>https:\/\/)?(?:[-_A-Za-z0-9]{1,30}\.){1,4}[a-z]{2,63}/.source +
|
|
26
|
-
/(?:(?:\/[-.
|
|
27
|
+
/(?:(?:\/[-._+A-Za-z0-9%@#]{1,300}){0,11}(?<ext>\.[a-z-0-9]{3,4})|(?:\/[-._+A-Za-z0-9%@#]{1,300}){0,11})\/?/.source +
|
|
27
28
|
/(?:\??&?(?:[-_+.A-Za-z0-9%*]{1,30}=[-_+.A-Za-z0-9%*]{1,300}&?){1,40}|\?)?/.source +
|
|
28
29
|
/(?:(?:#|(?<=#))[-_+.A-Za-z0-9%=&*:~,]{1,4000})?/.source +
|
|
29
30
|
/(?:\/|(?<![.,]))/.source +
|
|
@@ -37,6 +38,15 @@ const NIP05_AT_CUSTOM = `@(?<nip05AtCustom>${NIP05_LOCAL}\\.${NIP05_DOMAIN})`
|
|
|
37
38
|
const NIP05_BARE_ROOT = '(?<nip05BareRoot>[a-z0-9-]+\\.[a-z]{2,63})'
|
|
38
39
|
const NIP05_BARE_CUSTOM = `(?<nip05BareCustom>${NIP05_LOCAL}\\.${NIP05_DOMAIN})`
|
|
39
40
|
|
|
41
|
+
// App references are a library extension. Names may contain punctuation, but
|
|
42
|
+
// author spellings and encoded entities delimit the token. decodeAppUrl owns
|
|
43
|
+
// validation. Literal + inside a name must be encoded, so a named candidate
|
|
44
|
+
// cannot consume a neighboring app reference. Prose punctuation stays outside.
|
|
45
|
+
const APP_SOURCE = '(?:nostr:)?(?<app>' +
|
|
46
|
+
NAPP_ENTITY_REGEX.source.slice(1, -1) + '|' +
|
|
47
|
+
/\+{1,3}[^\s/<>+]+?@[a-zA-Z0-9._%@-]*[a-zA-Z0-9]/.source + '|' +
|
|
48
|
+
/\+{1,3}[\p{L}\p{N}._%~-]*[\p{L}\p{N}_%~-]/.source + ')'
|
|
49
|
+
|
|
40
50
|
function entitySource (name) {
|
|
41
51
|
const bodyLength = name === 'npub' ? '58' : (name === 'nrelay' ? '10,5000' : '58,5000')
|
|
42
52
|
return `(?:@|nostr:)?(?<${name}>${name}1${BECH32_BODY}{${bodyLength}})`
|
|
@@ -66,6 +76,7 @@ function getReferencesRegex (bareNip05) {
|
|
|
66
76
|
]
|
|
67
77
|
const alternatives = [
|
|
68
78
|
URL_SOURCE,
|
|
79
|
+
APP_SOURCE,
|
|
69
80
|
NIP05_STANDARD,
|
|
70
81
|
...nip05Compact,
|
|
71
82
|
...ENTITY_SOURCES,
|
|
@@ -277,7 +288,17 @@ function decodeFragmentValue (value) {
|
|
|
277
288
|
}
|
|
278
289
|
}
|
|
279
290
|
|
|
280
|
-
function getReferenceItem (original, groups, { getMimeType }) {
|
|
291
|
+
function getReferenceItem (original, groups, { getMimeType, defaultAppUser }) {
|
|
292
|
+
if (groups.app) {
|
|
293
|
+
const app = tryDecodeAppUrl(groups.app)
|
|
294
|
+
// An omitted author uses the configured default. An explicit but invalid
|
|
295
|
+
// author must never silently resolve to a different app.
|
|
296
|
+
if (app?.type === 'named' && !app.user && !groups.app.includes('@')) app.user = defaultAppUser
|
|
297
|
+
return app && (app.type === 'entity' || app.user)
|
|
298
|
+
? { key: 'app', app: { original, ...app } }
|
|
299
|
+
: { key: 'text', text: { value: original } }
|
|
300
|
+
}
|
|
301
|
+
|
|
281
302
|
if (groups.url) {
|
|
282
303
|
const url = `${groups.protocol ? '' : 'https://'}${groups.url}`
|
|
283
304
|
let mediaMetadata = {}
|
|
@@ -377,10 +398,20 @@ function getReferenceItem (original, groups, { getMimeType }) {
|
|
|
377
398
|
// Bare compact NIP-05 spellings (`bob.example.com`) are only recognized when
|
|
378
399
|
// `{ bareNip05: true }` is passed, since they are otherwise indistinguishable
|
|
379
400
|
// from plain hostnames; prefixed forms (`@bob.example.com`) always work.
|
|
380
|
-
|
|
401
|
+
// App references (+encodedEntity, +app@author or +app, with optional nostr:) return
|
|
402
|
+
// { key: 'app', app: { original, ...decodeAppUrlResult } } without resolution.
|
|
403
|
+
// Missing app authors use defaultAppAuthor, validated as a user reference.
|
|
404
|
+
export function extractMedia (content, { bareNip05 = false, getMimeType, defaultAppAuthor = '44billion.net' } = {}) {
|
|
381
405
|
if (typeof content !== 'string') {
|
|
382
406
|
throw new ValidationError('INVALID_MEDIA_CONTENT', { message: 'CONTENT_SHOULD_BE_A_STRING' })
|
|
383
407
|
}
|
|
408
|
+
let defaultAppUser
|
|
409
|
+
try {
|
|
410
|
+
defaultAppUser = decodeUserReference(defaultAppAuthor)
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
if (!(cause instanceof ValidationError)) throw cause
|
|
413
|
+
throw new ValidationError('INVALID_DEFAULT_APP_AUTHOR', { cause })
|
|
414
|
+
}
|
|
384
415
|
const regex = getReferencesRegex(bareNip05)
|
|
385
416
|
const items = []
|
|
386
417
|
let end = 0
|
|
@@ -392,7 +423,7 @@ export function extractMedia (content, { bareNip05 = false, getMimeType } = {})
|
|
|
392
423
|
}
|
|
393
424
|
const original = match[0]
|
|
394
425
|
end = start + original.length
|
|
395
|
-
const item = getReferenceItem(original, match.groups, { getMimeType })
|
|
426
|
+
const item = getReferenceItem(original, match.groups, { getMimeType, defaultAppUser })
|
|
396
427
|
if (item) items.push(item)
|
|
397
428
|
}
|
|
398
429
|
|