libp2r2p 0.10.14 → 0.10.16
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 +72 -1
- package/nip27/helpers/compact-whitespace.js +33 -0
- package/nip27/index.js +36 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,13 +371,84 @@ 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
380
|
|
|
381
|
+
App references are a library extension to NIP-27. `extractMedia()` recognizes
|
|
382
|
+
encoded `+…` app entities and named references such as `+hallway@fiatjaf.com`,
|
|
383
|
+
`++myapp@bob@example.com` and `+++myapp@npub1…`, with optional `nostr:` before
|
|
384
|
+
the entire reference. Named authors accept the NIP-05 spellings above, `npub`,
|
|
385
|
+
`nprofile` and hex pubkeys; app names may be URL-encoded. The one-to-three `+`
|
|
386
|
+
prefix selects `main`, `next` or `draft` respectively.
|
|
387
|
+
|
|
388
|
+
Bare app names use the `defaultAppAuthor` option, which defaults to
|
|
389
|
+
`'44billion.net'`: `+apps` identifies the same app as `+apps@44billion.net`.
|
|
390
|
+
The option accepts a NIP-05 reference (including compact spellings), `npub`,
|
|
391
|
+
`nprofile` or hex pubkey and is validated locally on every call. An invalid
|
|
392
|
+
value throws `ValidationError` with code `INVALID_DEFAULT_APP_AUTHOR`.
|
|
393
|
+
Explicit authors and encoded app entities are never overridden. Malformed
|
|
394
|
+
explicit authors remain text instead of falling back to the default.
|
|
395
|
+
|
|
396
|
+
```js
|
|
397
|
+
extractMedia('+apps', { defaultAppAuthor: 'bob@example.com' })
|
|
398
|
+
// app.user: { type: 'nip05', local: 'bob', domain: 'example.com', raw: 'bob.example.com' }
|
|
399
|
+
// app.original remains '+apps'.
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
These references produce `{ key: 'app', app: { original, ...decoded } }`, where
|
|
403
|
+
`decoded` is the result of `decodeAppUrl()` from `libp2r2p/url`, with a missing
|
|
404
|
+
named author filled from `defaultAppAuthor` as described above. Named apps
|
|
405
|
+
include `type: 'named'`, `prefix`, `channel`, `appName` (the manifest's `d` tag)
|
|
406
|
+
and the decoded `user`; entities include `type: 'entity'` and `entity`, usable
|
|
407
|
+
with `appDecode()` from `libp2r2p/nip19`. Extraction performs no network lookup
|
|
408
|
+
and does not verify that the app exists. For example:
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
extractMedia('Open nostr:++myapp@bob@example.com')
|
|
412
|
+
// [
|
|
413
|
+
// { key: 'text', text: { value: 'Open ' } },
|
|
414
|
+
// { key: 'app', app: {
|
|
415
|
+
// original: 'nostr:++myapp@bob@example.com', type: 'named',
|
|
416
|
+
// prefix: '++', channel: 'next', appName: 'myapp',
|
|
417
|
+
// user: { type: 'nip05', local: 'bob', domain: 'example.com', raw: 'bob.example.com' }
|
|
418
|
+
// } }
|
|
419
|
+
// ]
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
App references must be standalone inline tokens, not concatenated to event
|
|
423
|
+
pointers or embedded in URL paths. Invalid app candidates remain text.
|
|
424
|
+
Bare names can use letters, numbers, dots, underscores, hyphens and tildes;
|
|
425
|
+
URL-encode spaces, `@` and other punctuation in them.
|
|
426
|
+
`+naddr1…` is recognized as an app only for site-manifest kinds; unprefixed
|
|
427
|
+
`naddr1…` and `nostr:naddr1…` keep their existing `event` item shape.
|
|
428
|
+
|
|
429
|
+
`compactWhitespace(text, options?)` is also exported from `libp2r2p/nip27` as an
|
|
430
|
+
opt-in display helper. It removes carriage returns, collapses spaces and
|
|
431
|
+
tabs, removes spaces around line breaks, limits consecutive line breaks to
|
|
432
|
+
two, keeps the first eight line breaks (replacing subsequent runs with a
|
|
433
|
+
space), and trims the result. Empty strings are accepted; non-string inputs
|
|
434
|
+
throw `ValidationError` with code `INVALID_WHITESPACE_TEXT`. It is not
|
|
435
|
+
applied automatically by `extractMedia()`.
|
|
436
|
+
|
|
437
|
+
The option `maxLineBreaks` (default `8`) sets the total limit and accepts a
|
|
438
|
+
non-negative safe integer. Setting it to `0` replaces all line-break runs
|
|
439
|
+
with spaces. `consecutiveLineBreakThreshold` (default `3`) sets the minimum
|
|
440
|
+
run length that collapses to **two** line breaks; shorter runs are preserved.
|
|
441
|
+
It accepts safe integers of at least `3`. Either option accepts `Infinity`
|
|
442
|
+
to disable its rule. Runs are collapsed before applying the total limit,
|
|
443
|
+
and trimming happens last. Invalid options throw `ValidationError` with
|
|
444
|
+
code `INVALID_WHITESPACE_OPTIONS`.
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
import { compactWhitespace } from 'libp2r2p/nip27'
|
|
448
|
+
|
|
449
|
+
compactWhitespace(text, { maxLineBreaks: 12, consecutiveLineBreakThreshold: 5 })
|
|
450
|
+
```
|
|
451
|
+
|
|
381
452
|
User references (`npub`, `nprofile`, hex pubkeys and every NIP-05 spelling)
|
|
382
453
|
are handled by `libp2r2p/nip27`: `decodeUserReference()` returns the decoded
|
|
383
454
|
form with its canonical compact spelling, `encodeUserReference()` returns
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ValidationError } from '../../error/index.js'
|
|
2
|
+
|
|
3
|
+
// Compacts display text, defaulting to collapsing runs of three or more line
|
|
4
|
+
// breaks to two and keeping eight overall. extractMedia preserves input.
|
|
5
|
+
export function compactWhitespace (text, options = {}) {
|
|
6
|
+
if (typeof text !== 'string') {
|
|
7
|
+
throw new ValidationError('INVALID_WHITESPACE_TEXT', { message: 'TEXT_SHOULD_BE_A_STRING' })
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
11
|
+
throw new ValidationError('INVALID_WHITESPACE_OPTIONS', { message: 'OPTIONS_SHOULD_BE_AN_OBJECT' })
|
|
12
|
+
}
|
|
13
|
+
const { maxLineBreaks = 8, consecutiveLineBreakThreshold = 3 } = options
|
|
14
|
+
if (maxLineBreaks !== Infinity && (!Number.isSafeInteger(maxLineBreaks) || maxLineBreaks < 0)) {
|
|
15
|
+
throw new ValidationError('INVALID_WHITESPACE_OPTIONS', { message: 'MAX_LINE_BREAKS_SHOULD_BE_A_NON_NEGATIVE_SAFE_INTEGER_OR_INFINITY' })
|
|
16
|
+
}
|
|
17
|
+
if (consecutiveLineBreakThreshold !== Infinity && (!Number.isSafeInteger(consecutiveLineBreakThreshold) || consecutiveLineBreakThreshold < 3)) {
|
|
18
|
+
throw new ValidationError('INVALID_WHITESPACE_OPTIONS', { message: 'CONSECUTIVE_LINE_BREAK_THRESHOLD_SHOULD_BE_A_SAFE_INTEGER_AT_LEAST_THREE_OR_INFINITY' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let remainingLineBreaks = maxLineBreaks
|
|
22
|
+
return text
|
|
23
|
+
.replace(/\r/g, '')
|
|
24
|
+
.replace(/[\t ]+/g, ' ')
|
|
25
|
+
.replace(/ ?\n ?/g, '\n')
|
|
26
|
+
.replace(/\n+/g, lineBreaks => {
|
|
27
|
+
const consecutive = lineBreaks.length >= consecutiveLineBreakThreshold ? 2 : lineBreaks.length
|
|
28
|
+
const kept = Math.min(remainingLineBreaks, consecutive)
|
|
29
|
+
remainingLineBreaks -= kept
|
|
30
|
+
return lineBreaks.slice(0, kept) + (kept < consecutive ? ' ' : '')
|
|
31
|
+
})
|
|
32
|
+
.trim()
|
|
33
|
+
}
|
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,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from './helpers/user-reference.js'
|
|
15
16
|
|
|
16
17
|
export { decodeUserReference, encodeUserReference, tryDecodeUserReference }
|
|
18
|
+
export { compactWhitespace } from './helpers/compact-whitespace.js'
|
|
17
19
|
|
|
18
20
|
const BECH32_BODY = '[ac-hj-np-z02-9]'
|
|
19
21
|
const BOUNDARY_PREFIX = /(?<=^|[\s"«„「¡¿:{([])/.source
|
|
@@ -36,6 +38,15 @@ const NIP05_AT_CUSTOM = `@(?<nip05AtCustom>${NIP05_LOCAL}\\.${NIP05_DOMAIN})`
|
|
|
36
38
|
const NIP05_BARE_ROOT = '(?<nip05BareRoot>[a-z0-9-]+\\.[a-z]{2,63})'
|
|
37
39
|
const NIP05_BARE_CUSTOM = `(?<nip05BareCustom>${NIP05_LOCAL}\\.${NIP05_DOMAIN})`
|
|
38
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
|
+
|
|
39
50
|
function entitySource (name) {
|
|
40
51
|
const bodyLength = name === 'npub' ? '58' : (name === 'nrelay' ? '10,5000' : '58,5000')
|
|
41
52
|
return `(?:@|nostr:)?(?<${name}>${name}1${BECH32_BODY}{${bodyLength}})`
|
|
@@ -65,6 +76,7 @@ function getReferencesRegex (bareNip05) {
|
|
|
65
76
|
]
|
|
66
77
|
const alternatives = [
|
|
67
78
|
URL_SOURCE,
|
|
79
|
+
APP_SOURCE,
|
|
68
80
|
NIP05_STANDARD,
|
|
69
81
|
...nip05Compact,
|
|
70
82
|
...ENTITY_SOURCES,
|
|
@@ -276,7 +288,17 @@ function decodeFragmentValue (value) {
|
|
|
276
288
|
}
|
|
277
289
|
}
|
|
278
290
|
|
|
279
|
-
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
|
+
|
|
280
302
|
if (groups.url) {
|
|
281
303
|
const url = `${groups.protocol ? '' : 'https://'}${groups.url}`
|
|
282
304
|
let mediaMetadata = {}
|
|
@@ -376,10 +398,20 @@ function getReferenceItem (original, groups, { getMimeType }) {
|
|
|
376
398
|
// Bare compact NIP-05 spellings (`bob.example.com`) are only recognized when
|
|
377
399
|
// `{ bareNip05: true }` is passed, since they are otherwise indistinguishable
|
|
378
400
|
// from plain hostnames; prefixed forms (`@bob.example.com`) always work.
|
|
379
|
-
|
|
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' } = {}) {
|
|
380
405
|
if (typeof content !== 'string') {
|
|
381
406
|
throw new ValidationError('INVALID_MEDIA_CONTENT', { message: 'CONTENT_SHOULD_BE_A_STRING' })
|
|
382
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
|
+
}
|
|
383
415
|
const regex = getReferencesRegex(bareNip05)
|
|
384
416
|
const items = []
|
|
385
417
|
let end = 0
|
|
@@ -391,7 +423,7 @@ export function extractMedia (content, { bareNip05 = false, getMimeType } = {})
|
|
|
391
423
|
}
|
|
392
424
|
const original = match[0]
|
|
393
425
|
end = start + original.length
|
|
394
|
-
const item = getReferenceItem(original, match.groups, { getMimeType })
|
|
426
|
+
const item = getReferenceItem(original, match.groups, { getMimeType, defaultAppUser })
|
|
395
427
|
if (item) items.push(item)
|
|
396
428
|
}
|
|
397
429
|
|