libp2r2p 0.10.15 → 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 CHANGED
@@ -371,13 +371,61 @@ 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
+
381
429
  `compactWhitespace(text, options?)` is also exported from `libp2r2p/nip27` as an
382
430
  opt-in display helper. It removes carriage returns, collapses spaces and
383
431
  tabs, removes spaces around line breaks, limits consecutive line breaks to
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,
@@ -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.16",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",