libp2r2p 0.11.27 → 0.11.28

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
@@ -1257,3 +1257,10 @@ The 0.11.27 session publishes independently of local event-store persistence aft
1257
1257
  its initial durable intent and exact-envelope checkpoint. `deliveryConfirmed`
1258
1258
  and the copied `localPersistence` summary distinguish relay acceptance from
1259
1259
  local storage. See `private-messenger/session/README.md` for retry and cleanup.
1260
+
1261
+ The 0.11.28 private messenger preserves classified publication failures for
1262
+ seeder recovery asks. Diagnostics include the logical ask ID, channel, seeder
1263
+ and range, plus report and relay classifications; arbitrary relay text is not
1264
+ logged. Native report causes remain available privately to recovery retry
1265
+ policy. Required publication coverage, first-ACK semantics and range recovery
1266
+ behavior are unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.27",
3
+ "version": "0.11.28",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -1,5 +1,96 @@
1
1
  import { getEventHash } from '../../event/index.js'
2
2
 
3
+ const safeTokenPattern = /^[A-Za-z0-9][A-Za-z0-9:_-]{0,159}$/
4
+ const safeToken = value => typeof value === 'string' && safeTokenPattern.test(value) ? value : undefined
5
+ const safeRelay = value => {
6
+ if (typeof value !== 'string') return undefined
7
+ try {
8
+ const url = new URL(value)
9
+ if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return undefined
10
+ return `${url.protocol}//${url.host}`
11
+ } catch {
12
+ return undefined
13
+ }
14
+ }
15
+
16
+ function reasonDiagnostic (reason) {
17
+ if (typeof reason === 'string') {
18
+ const prefix = /^([a-z][a-z0-9-]{0,31}):/i.exec(reason)?.[1]
19
+ return { code: safeToken(reason) ?? safeToken(prefix ? `${prefix}:` : undefined) ?? 'UNKNOWN' }
20
+ }
21
+ const prefix = typeof reason?.message === 'string' && /^([a-z][a-z0-9-]{0,31}):/i.exec(reason.message)?.[1]
22
+ const category = safeToken(reason?.category)
23
+ return {
24
+ code: safeToken(reason?.code) ?? safeToken(prefix ? `${prefix}:` : undefined) ?? category ?? 'UNKNOWN',
25
+ ...(category ? { category } : {}),
26
+ ...(safeToken(reason?.name) ? { name: safeToken(reason.name) } : {}),
27
+ ...(Number.isSafeInteger(reason?.retryAt) ? { retryAt: reason.retryAt } : {}),
28
+ ...(Number.isSafeInteger(reason?.retryAfterMs) ? { retryAfterMs: reason.retryAfterMs } : {})
29
+ }
30
+ }
31
+
32
+ function sanitizedReport (report, index) {
33
+ return {
34
+ index,
35
+ ...(Number.isSafeInteger(report?.total) ? { total: report.total } : {}),
36
+ ...(Number.isSafeInteger(report?.fulfilled) ? { fulfilled: report.fulfilled } : {}),
37
+ ...(report?.retryWhenAvailable === true ? { retryWhenAvailable: true } : {}),
38
+ ...(report?.retryWhenOnline === true ? { retryWhenOnline: true } : {}),
39
+ errors: (Array.isArray(report?.errors) ? report.errors : []).map(entry => {
40
+ const relay = safeRelay(entry?.relay)
41
+ const detail = reasonDiagnostic(entry?.reason ?? entry)
42
+ return {
43
+ ...(relay ? { relay } : {}),
44
+ ...(safeToken(entry?.code) ? { ...detail, code: entry.code } : detail)
45
+ }
46
+ })
47
+ }
48
+ }
49
+
50
+ const contextFields = ['operation', 'channelPubkey', 'seederPubkey', 'receiverPubkey', 'eventId']
51
+ const contextNumbers = ['eventKind', 'since', 'until']
52
+
53
+ export function sanitizePublicationError (error, context = {}) {
54
+ const source = error instanceof Error ? error : new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
55
+ const reports = Array.isArray(source.reports) ? source.reports : []
56
+ const retryReasons = reports.flatMap(report => (Array.isArray(report?.errors) ? report.errors : [])
57
+ .map(entry => entry?.reason).filter(Boolean))
58
+ if (source.code !== 'MESSAGE_NOT_PUBLISHED' && !reports.length) {
59
+ for (const key of contextFields) if (safeToken(context[key])) source[key] = context[key]
60
+ for (const key of contextNumbers) if (Number.isSafeInteger(context[key])) source[key] = context[key]
61
+ return source
62
+ }
63
+
64
+ const sanitized = new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
65
+ if (source.stack) sanitized.stack = source.stack
66
+ Object.assign(sanitized, {
67
+ code: safeToken(source.code) ?? 'MESSAGE_NOT_PUBLISHED',
68
+ ...(safeToken(source.reason) ? { reason: source.reason } : {}),
69
+ ...(safeToken(source.eventId) ? { eventId: source.eventId } : {}),
70
+ ...(Number.isSafeInteger(source.eventKind) ? { eventKind: source.eventKind } : {}),
71
+ ...(source.retryWhenAvailable === true ? { retryWhenAvailable: true } : {}),
72
+ ...(source.retryWhenOnline === true ? { retryWhenOnline: true } : {}),
73
+ reports: reports.map(sanitizedReport)
74
+ })
75
+ for (const key of contextFields) if (safeToken(context[key])) sanitized[key] = context[key]
76
+ for (const key of contextNumbers) if (Number.isSafeInteger(context[key])) sanitized[key] = context[key]
77
+ Object.defineProperty(sanitized, 'retryReasons', { value: retryReasons })
78
+ return sanitized
79
+ }
80
+
81
+ export function publicationFailureDiagnostic (error) {
82
+ const result = {}
83
+ for (const key of [...contextFields, ...contextNumbers]) {
84
+ const value = error?.[key]
85
+ if (safeToken(value) && contextFields.includes(key)) result[key] = value
86
+ else if (Number.isSafeInteger(value) && contextNumbers.includes(key)) result[key] = value
87
+ }
88
+ for (const key of ['code', 'reason', 'eventId']) if (safeToken(error?.[key])) result[key] = error[key]
89
+ for (const key of ['retryWhenAvailable', 'retryWhenOnline']) if (error?.[key] === true) result[key] = true
90
+ result.reports = (Array.isArray(error?.reports) ? error.reports : []).map(sanitizedReport)
91
+ return result
92
+ }
93
+
3
94
  // A report describes one outer event, not one relay. Each outer event needs
4
95
  // accepted publication covering its routed recipients. Keep the successful fast
5
96
  // path immediate; a partially covered multi-recipient report remains a failure.
@@ -19,6 +19,11 @@ export function recoveryRetryDelay (errors, delay) {
19
19
  if (['timeout', 'cutoff', 'closed'].includes(entry.status)) retry = true
20
20
  visit(entry.error)
21
21
  }
22
+ for (const report of error.reports || []) {
23
+ if (report.retryWhenAvailable || report.retryWhenOnline) retry = true
24
+ for (const entry of report.errors || []) visit(entry.reason ?? entry.error)
25
+ }
26
+ for (const reason of error.retryReasons || []) visit(reason)
22
27
  visit(error.cause)
23
28
  }
24
29
  errors.forEach(visit)
@@ -36,6 +41,10 @@ export function isPermanentRecoveryError (error) {
36
41
  /^(auth-required:|restricted:|blocked:|invalid:|pow:)/.test(value.message || '')) return true
37
42
  for (const child of value.errors || []) if (visit(child)) return true
38
43
  for (const entry of value.relays || []) if (visit(entry.error)) return true
44
+ for (const report of value.reports || []) {
45
+ for (const entry of report.errors || []) if (visit(entry.reason ?? entry.error)) return true
46
+ }
47
+ for (const reason of value.retryReasons || []) if (visit(reason)) return true
39
48
  return visit(value.cause)
40
49
  }
41
50
  return visit(error)
@@ -52,6 +52,7 @@ import { backgroundSigner } from './helpers/background-signer.js'
52
52
  import { createSendRelayRouting, normalizeFallbackRelays, normalizeFallbackDelay } from './helpers/send-routing.js'
53
53
  import { incompleteFetchError } from '../private-channel/helpers/fetch-error.js'
54
54
  import { recoveryRetryDelay, isPermanentRecoveryError } from './helpers/recovery-retry.js'
55
+ import { assertMessagePublished, publicationFailureDiagnostic, sanitizePublicationError } from './helpers/publication.js'
55
56
  import { normalizeRelayUrl } from '../url/index.js'
56
57
  import { deliveryInfo } from '../private-channel/helpers/rumor.js'
57
58
  import { migrateMessageQueue } from './services/migrate-message-queue.js'
@@ -2755,13 +2756,17 @@ export class PrivateMessenger {
2755
2756
  payload: { since, until }
2756
2757
  })
2757
2758
  asks.push(ask)
2758
- const reports = ask?.delivery?.reports
2759
- if (!Array.isArray(reports) || !reports.length || reports.some(report => report?.success !== true)) {
2760
- throw new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
2761
- }
2759
+ await assertMessagePublished(ask, ask.question)
2762
2760
  } catch (err) {
2763
- failures.push({ seeder, error: err })
2764
- console.warn('private-messenger seeder recovery ask failed', seeder, err?.message ?? err)
2761
+ const error = sanitizePublicationError(err, {
2762
+ operation: 'seeder-recovery-ask',
2763
+ channelPubkey,
2764
+ seederPubkey: seeder,
2765
+ since,
2766
+ until
2767
+ })
2768
+ failures.push({ seeder, error })
2769
+ console.warn('private-messenger seeder recovery ask failed', JSON.stringify(publicationFailureDiagnostic(error)))
2765
2770
  }
2766
2771
  }
2767
2772
  return { asks, failures }