libp2r2p 0.11.19 → 0.11.21

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
@@ -692,6 +692,13 @@ Blossom server normalization accepts root `http:` origins for local
692
692
  development. Public Blossom validation additionally requires `https:` and a
693
693
  public host, and rejects credentials, paths, query strings and fragments.
694
694
 
695
+ Starting with 0.11.21, import `CONTENT_KEY_ANNOUNCEMENT` (18716) from
696
+ `libp2r2p/kind`. It is also included in `eventKinds` and used by content-key
697
+ builders, proof validation and relay discovery. The announcement publishes a
698
+ public content key for private communication and can be shared across apps.
699
+ The former `CONTENT_KEY_KIND` export from `libp2r2p/content-key/event` has been
700
+ removed; consumers must update that import.
701
+
695
702
  `classifyEvent()` from `libp2r2p/event` combines the exact NIP-01 kind
696
703
  ranges with tag-defined behavior. The first `d` tag may add `replaceable` or
697
704
  `addressable`, while an `expiration` tag equal to `created_at` adds
@@ -955,22 +962,26 @@ also admits relay policy refusals (`blocked:`, `restricted:`, `auth-required:`,
955
962
  Local authentication denials, invalid events and unknown errors qualify for
956
963
  neither policy. Predicates classify individual native errors; callers decide
957
964
  how to handle mixed per-relay outcomes and application-specific aggregates.
958
- `parseRelayRetryAdvice(reason, extra, { now = Date.now() })`, exported by
965
+ `parseRelayRetryAdvice(reason, params, { now = Date.now() })`, exported by
959
966
  `libp2r2p/relay`, returns `{ retryAt, retryAfterMs? }` or `null`. It only interprets
960
- leading `rate-limited:` refusals. Optional `retry_at` is a Unix timestamp in
961
- seconds (fractions allowed), converted to milliseconds. Finite positive absolute
962
- advice takes precedence over `retry_after`, including when already expired;
963
- expired advice does not restart a relative wait. Future waits are capped at five
964
- minutes from processing. Missing/invalid absolute advice falls back to finite
965
- positive relative seconds, capped at 300. A valid relative value is retained as
966
- `retryAfterMs` even when the absolute deadline wins. Schedulers wait until
967
+ leading `rate-limited:` refusals. The optional string `params` is relative
968
+ milliseconds (e.g. "2500") or an absolute Unix timestamp in milliseconds
969
+ prefixed with `@` (e.g. "@1791738154332"). Values must be positive safe integers
970
+ in canonical decimal form, without signs, whitespace, fractions or leading zeros.
971
+ Relative values produce both `retryAfterMs` and `retryAt`; absolute values produce
972
+ only `retryAt` and retain an expired deadline without starting a new wait.
973
+ Future waits are capped at five minutes. Missing or invalid params are ignored
974
+ and do not instruct clients to retry immediately. Schedulers wait until
967
975
  max(local backoff deadline, retryAt), never sum the two or renew the deadline on
968
976
  online notifications.
969
977
 
970
- The optional Nostr extension works with older relays that only send
971
- `retry_after` and older consumers that ignore `retry_at`. Neither field supplies
972
- provenance or retry eligibility: `local`, `origin`, `retryable`, category and code
973
- claims in extras are ignored. Native messages, causes and codes remain intact.
978
+ Rate limits apply only to the client message type that triggered the refusal:
979
+ `EVENT`, `AUTH`, `REQ` and `COUNT` have independent connection cooldowns. Responses
980
+ are correlated with pending operations; unknown IDs never create cooldowns.
981
+ CLOSE cleanup remains immediate. The launcher can replace valid relative params
982
+ with `@` timestamps before queueing bridge delivery because its consumers share
983
+ its clock. No legacy timing objects are interpreted. Params never supply provenance
984
+ or retry eligibility; native messages, causes and codes remain intact.
974
985
 
975
986
  ## Binary encodings
976
987
 
@@ -1091,10 +1102,11 @@ user-facing send error.
1091
1102
 
1092
1103
  ### Relay backpressure and bounded synchronization (0.11.7)
1093
1104
 
1094
- Optional `retry_at` Unix seconds or numeric `retry_after` seconds on relay `CLOSED` and rejected `OK`
1095
- responses are preserved as `retryAfterMs` and `retryAt` on native relay errors.
1096
- Only `rate-limited:` responses apply this advice; invalid values are ignored
1097
- and delays are capped at five minutes. New operations on that connection wait
1105
+ Optional decimal `params` milliseconds or `@` Unix milliseconds on relay `CLOSED`
1106
+ and rejected `OK` responses are preserved as normalized `retryAt` (and
1107
+ `retryAfterMs` for relative params) on native relay errors. Only `rate-limited:`
1108
+ responses apply this advice; invalid values are ignored
1109
+ and delays are capped at five minutes. New operations of the refused type wait
1098
1110
  within their existing deadlines; active subscriptions remain live and `CLOSE`
1099
1111
  never waits. Cancelled or timed-out operations cannot be sent later. A rejection
1100
1112
  is still reported to the caller; this does not silently republish failed events.
@@ -1,7 +1,6 @@
1
1
  import { ValidationError } from '../../error/index.js'
2
2
  import { getEventHash, isValidEvent } from '../../event/index.js'
3
-
4
- export const CONTENT_KEY_KIND = 18716
3
+ import { CONTENT_KEY_ANNOUNCEMENT } from '../../kind/index.js'
5
4
 
6
5
  const HEX_PUBKEY = /^[0-9a-f]{64}$/
7
6
  const HEX_SIG = /^[0-9a-f]{128}$/i
@@ -15,7 +14,7 @@ export async function makeContentKeyEventForPubkey ({ userSigner, contentPubkey,
15
14
  if (!HEX_PUBKEY.test(contentPubkey || '')) throw new ValidationError('CONTENT_PUBKEY_REQUIRED')
16
15
 
17
16
  return userSigner.signEvent({
18
- kind: CONTENT_KEY_KIND,
17
+ kind: CONTENT_KEY_ANNOUNCEMENT,
19
18
  created_at: createdAt,
20
19
  tags: [['cp', contentPubkey]],
21
20
  content: ''
@@ -32,7 +31,7 @@ export async function makeContentKeyEvent ({ userSigner, contentKeySigner, creat
32
31
  }
33
32
 
34
33
  export function parseContentKeyEvent (event) {
35
- if (!event || event.kind !== CONTENT_KEY_KIND || event.content !== '') return null
34
+ if (!event || event.kind !== CONTENT_KEY_ANNOUNCEMENT || event.content !== '') return null
36
35
  if (!HEX_PUBKEY.test(event.pubkey) || !Number.isSafeInteger(event.created_at)) return null
37
36
  if (!Array.isArray(event.tags) || event.tags.length !== 1) return null
38
37
  if (!isValidEvent(event)) return null
@@ -67,7 +66,7 @@ function contentKeyProofError ({ ownerPubkey, contentPubkey, proof }) {
67
66
  if (!parsed) return 'INVALID_CONTENT_KEY_PROOF'
68
67
 
69
68
  const event = {
70
- kind: CONTENT_KEY_KIND,
69
+ kind: CONTENT_KEY_ANNOUNCEMENT,
71
70
  pubkey: ownerPubkey,
72
71
  created_at: parsed.created_at,
73
72
  tags: [['cp', contentPubkey]],
@@ -1,4 +1,5 @@
1
- import { CONTENT_KEY_KIND, parseContentKeyEvent } from '../event/index.js'
1
+ import { parseContentKeyEvent } from '../event/index.js'
2
+ import { CONTENT_KEY_ANNOUNCEMENT } from '../../kind/index.js'
2
3
  import { pickRelaysForPubkeys, getRelaysByPubkey, relayPool } from '../../relay/index.js'
3
4
 
4
5
  const QUERY_CACHE_MS = 40 * 60 * 1000
@@ -81,7 +82,7 @@ async function queryContentKeyProofs (pubkeyList, { _getEvents, _getRelaysByPubk
81
82
  .map(async ([relay, authors]) => {
82
83
  try {
83
84
  const { result } = await _getEvents({
84
- kinds: [CONTENT_KEY_KIND],
85
+ kinds: [CONTENT_KEY_ANNOUNCEMENT],
85
86
  authors,
86
87
  limit: authors.length
87
88
  }, [relay], {
package/kind/index.js CHANGED
@@ -58,6 +58,7 @@ export const FILE_SERVER_PREFERENCE = 10096
58
58
  export const GOOD_WIKI_AUTHORS = 10101
59
59
  export const GOOD_WIKI_RELAYS = 10102
60
60
  export const NWC_WALLET_INFO = 13194
61
+ export const CONTENT_KEY_ANNOUNCEMENT = 18716
61
62
  export const LIGHTNING_PUB_RPC = 21000
62
63
  export const AUTH = 22242
63
64
  export const NWC_WALLET_REQUEST = 23194
@@ -198,6 +199,7 @@ export const eventKinds = /* @__PURE__ */ Object.freeze({
198
199
  GOOD_WIKI_AUTHORS,
199
200
  GOOD_WIKI_RELAYS,
200
201
  NWC_WALLET_INFO,
202
+ CONTENT_KEY_ANNOUNCEMENT,
201
203
  LIGHTNING_PUB_RPC,
202
204
  AUTH,
203
205
  NWC_WALLET_REQUEST,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.19",
3
+ "version": "0.11.21",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -33,8 +33,8 @@ export function relayCloseError (event, category, cause) {
33
33
 
34
34
  // Keep native diagnostics and attach only bounded timing advice. An absolute
35
35
  // deadline survives delayed delivery through a launcher-owned relay bridge.
36
- export function relayRejectionError (reason, extra, fallback) {
36
+ export function relayRejectionError (reason, params, fallback) {
37
37
  const error = categorizeRelayError(reason, 'relay', fallback)
38
- Object.assign(error, parseRelayRetryAdvice(error.message, extra))
38
+ Object.assign(error, parseRelayRetryAdvice(error.message, params))
39
39
  return error
40
40
  }
@@ -1,18 +1,13 @@
1
- // Nostr transport extensions, in seconds. Timing advice is bounded and never
1
+ // Nostr params use relative milliseconds or @Unix milliseconds. Advice never
2
2
  // supplies an error category, provenance or a routing/retry policy.
3
- export function parseRelayRetryAdvice (reason, extra, { now = Date.now() } = {}) {
3
+ export function parseRelayRetryAdvice (reason, params, { now = Date.now() } = {}) {
4
4
  if (typeof reason !== 'string' || !reason.startsWith('rate-limited:') ||
5
- !extra || typeof extra !== 'object' || Array.isArray(extra) || !Number.isFinite(now)) return null
5
+ typeof params !== 'string' || params.match(/^@?[1-9]\d*$/)?.[0] !== params || !Number.isFinite(now)) return null
6
6
 
7
- const after = extra.retry_after
8
- const retryAfterMs = typeof after === 'number' && Number.isFinite(after) && after > 0
9
- ? Math.min(after, 300) * 1000
10
- : undefined
11
- const at = extra.retry_at
12
- const absolute = typeof at === 'number' && Number.isFinite(at) && at > 0 && Number.isFinite(at * 1000)
13
- ? Math.min(at * 1000, now + 300000)
14
- : undefined
15
- const retryAt = absolute ?? (retryAfterMs === undefined ? undefined : now + retryAfterMs)
16
- if (retryAt === undefined) return null
17
- return { ...(retryAfterMs === undefined ? {} : { retryAfterMs }), retryAt }
7
+ const absolute = params.startsWith('@')
8
+ const value = Number(absolute ? params.slice(1) : params)
9
+ if (!Number.isSafeInteger(value)) return null
10
+ if (absolute) return { retryAt: Math.min(value, now + 300000) }
11
+ const retryAfterMs = Math.min(value, 300000)
12
+ return { retryAfterMs, retryAt: now + retryAfterMs }
18
13
  }
@@ -50,7 +50,7 @@ export class RelayConnection {
50
50
  #publishes = new Map()
51
51
  #authentications = new Map()
52
52
  #counts = new Map()
53
- #retryAt = 0
53
+ #retryAtByType = new Map()
54
54
 
55
55
  constructor (url, { WebSocket: WebSocketImpl = globalThis.WebSocket } = {}) {
56
56
  this.url = url
@@ -124,12 +124,12 @@ export class RelayConnection {
124
124
 
125
125
  // New work respects relay cooldowns; cleanup uses send() directly. These
126
126
  // waits stay within the caller's existing deadline and are cancellable.
127
- #dispatchWork (send, fail) {
127
+ #dispatchWork (type, send, fail) {
128
128
  let timer = null
129
129
  let cancelled = false
130
130
  const dispatch = () => {
131
131
  if (cancelled) return
132
- const remaining = this.#retryAt - Date.now()
132
+ const remaining = (this.#retryAtByType.get(type) ?? 0) - Date.now()
133
133
  if (remaining > 0) {
134
134
  timer = maybeUnref(setTimeout(dispatch, remaining))
135
135
  return
@@ -140,9 +140,9 @@ export class RelayConnection {
140
140
  return () => { cancelled = true; clearTimeout(timer) }
141
141
  }
142
142
 
143
- #rejection (reason, extra, fallback) {
144
- const error = relayRejectionError(reason, extra, fallback)
145
- this.#retryAt = Math.max(this.#retryAt, error.retryAt || 0)
143
+ #rejection (type, reason, params, fallback) {
144
+ const error = relayRejectionError(reason, params, fallback)
145
+ if (error.retryAt !== undefined) this.#retryAtByType.set(type, Math.max(this.#retryAtByType.get(type) ?? 0, error.retryAt))
146
146
  return error
147
147
  }
148
148
 
@@ -163,7 +163,7 @@ export class RelayConnection {
163
163
  }
164
164
  const subscription = { filters, handlers, close, cancelSend: null }
165
165
  this.#subscriptions.set(id, subscription)
166
- subscription.cancelSend = this.#dispatchWork(() => {
166
+ subscription.cancelSend = this.#dispatchWork('REQ', () => {
167
167
  this.send(JSON.stringify(['REQ', id, ...filters]))
168
168
  sent = true
169
169
  }, error => {
@@ -209,7 +209,7 @@ export class RelayConnection {
209
209
  pending.waiters.add(waiter)
210
210
  signal?.addEventListener('abort', onAbort, { once: true })
211
211
  if (created) {
212
- pending.cancelSend = this.#dispatchWork(() => {
212
+ pending.cancelSend = this.#dispatchWork(type, () => {
213
213
  this.send(JSON.stringify([type, event]))
214
214
  }, error => this.#settleEvent(map, event.id, error))
215
215
  }
@@ -224,7 +224,7 @@ export class RelayConnection {
224
224
  const pending = { ...deferred, signal, onAbort, cancelSend: null }
225
225
  this.#counts.set(id, pending)
226
226
  signal?.addEventListener('abort', onAbort, { once: true })
227
- pending.cancelSend = this.#dispatchWork(() => this.send(JSON.stringify(['COUNT', id, ...filters])), error => {
227
+ pending.cancelSend = this.#dispatchWork('COUNT', () => this.send(JSON.stringify(['COUNT', id, ...filters])), error => {
228
228
  this.#settleCount(id, null, error)
229
229
  })
230
230
  return deferred.promise
@@ -277,7 +277,8 @@ export class RelayConnection {
277
277
  if (data[0] === 'CLOSED') {
278
278
  const id = data[1]
279
279
  const subscription = this.#subscriptions.get(id)
280
- const reason = this.#rejection(data[2], data[3], subscription ? 'SUBSCRIPTION_CLOSED' : 'COUNT_CLOSED')
280
+ if (!subscription && !this.#counts.has(id)) return
281
+ const reason = this.#rejection(subscription ? 'REQ' : 'COUNT', data[2], data[3], subscription ? 'SUBSCRIPTION_CLOSED' : 'COUNT_CLOSED')
281
282
  if (subscription) {
282
283
  this.#subscriptions.delete(id)
283
284
  subscription.cancelSend?.()
@@ -286,9 +287,11 @@ export class RelayConnection {
286
287
  return
287
288
  }
288
289
  if (data[0] === 'OK') {
289
- const reason = data[2] === true ? null : this.#rejection(data[3], data[4], 'EVENT_REJECTED')
290
- this.#settleEvent(this.#publishes, data[1], reason, data[3])
291
- this.#settleEvent(this.#authentications, data[1], reason, data[3])
290
+ for (const [type, pending] of [['EVENT', this.#publishes], ['AUTH', this.#authentications]]) {
291
+ if (!pending.has(data[1])) continue
292
+ const reason = data[2] === true ? null : this.#rejection(type, data[3], data[4], 'EVENT_REJECTED')
293
+ this.#settleEvent(pending, data[1], reason, data[3])
294
+ }
292
295
  return
293
296
  }
294
297
  if (data[0] === 'AUTH' && typeof data[1] === 'string') {