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,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
|
-
|
|
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
|
-
|
|
2764
|
-
|
|
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 }
|