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 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
- /(?:(?:\/[-._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
+ /(?:(?:\/[-._+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
- export function extractMedia (content, { bareNip05 = false, getMimeType } = {}) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.10.15",
3
+ "version": "0.10.17",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",