libp2r2p 0.11.30 → 0.11.31
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
|
@@ -1278,3 +1278,9 @@ circuit: explicit bans/refusals open immediately, repeated transport failures
|
|
|
1278
1278
|
open after three attempts, and a fully quarantined route reports
|
|
1279
1279
|
`RELAY_CIRCUIT_OPEN` with `retryAt` before another envelope is prepared. Reads and
|
|
1280
1280
|
receive routing are unaffected. See [session relay fallback](private-messenger/session/README.md#relay-fallback).
|
|
1281
|
+
|
|
1282
|
+
The 0.11.31 pending-decryption retry groups records by
|
|
1283
|
+
`(receiverPubkey, ownContentPubkey)` for one pass. A FIFO probe may skip the
|
|
1284
|
+
remaining records only when the local signer confirms `CONTENT_KEY_NOT_FOUND`
|
|
1285
|
+
with `hasOwnContentKey: false`. MAC, proof, parsing and operational failures are
|
|
1286
|
+
never generalized to the group, and every deferred record remains persisted.
|
package/package.json
CHANGED
|
@@ -57,7 +57,7 @@ import { assertMessagePublished, publicationFailureDiagnostic, sanitizePublicati
|
|
|
57
57
|
import { normalizeRelayUrl } from '../url/index.js'
|
|
58
58
|
import { deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
59
59
|
import { migrateMessageQueue } from './services/migrate-message-queue.js'
|
|
60
|
-
import { createPendingDecryption } from './services/pending-decryption.js'
|
|
60
|
+
import { createPendingDecryption, pendingContentKeyGroup, pendingMissingContentKeyGroup } from './services/pending-decryption.js'
|
|
61
61
|
import { createDecryptionScheduler } from './services/decryption-scheduler.js'
|
|
62
62
|
import { decryptionError, isDeferredDecryptionError } from '../private-channel/helpers/decryption-error.js'
|
|
63
63
|
import { bytesToBase64 } from '../base64/index.js'
|
|
@@ -3259,12 +3259,7 @@ export class PrivateMessenger {
|
|
|
3259
3259
|
job.started = true
|
|
3260
3260
|
const force = job.force
|
|
3261
3261
|
job.force = false
|
|
3262
|
-
const
|
|
3263
|
-
for (const record of await this.pendingDecryption.records({ force })) {
|
|
3264
|
-
if (!groups.has(record.channelPubkey)) groups.set(record.channelPubkey, [])
|
|
3265
|
-
groups.get(record.channelPubkey).push(record)
|
|
3266
|
-
}
|
|
3267
|
-
const results = await Promise.allSettled([...groups].map(async ([channelPubkey, records]) => {
|
|
3262
|
+
const replay = async (channelPubkey, records, callbacks = {}) => {
|
|
3268
3263
|
const channel = this.channels.get(channelPubkey)
|
|
3269
3264
|
if (!channel || !this.desiredChannels.has(channelPubkey)) return
|
|
3270
3265
|
const current = () => {
|
|
@@ -3302,15 +3297,78 @@ export class PrivateMessenger {
|
|
|
3302
3297
|
return this.receive(channelPubkey, () => this.enqueueRumor('nym', channelPubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }, { trackActivity: false })
|
|
3303
3298
|
},
|
|
3304
3299
|
onSeedEvent: seed => { current(); return this.receive(channelPubkey, () => this.enqueueSeed(channelPubkey, seed), seed, { trackActivity: false }) },
|
|
3305
|
-
onRecovered: record => {
|
|
3306
|
-
|
|
3300
|
+
onRecovered: async record => {
|
|
3301
|
+
current()
|
|
3302
|
+
await callbacks.onRecovered?.(record)
|
|
3303
|
+
return this.pendingDecryption.recovered(record)
|
|
3304
|
+
},
|
|
3305
|
+
onFailure: async (record, error) => {
|
|
3306
|
+
current()
|
|
3307
|
+
if (await callbacks.onFailure?.(record, error) === false) return
|
|
3308
|
+
return this.pendingDecryption.failed(record, error)
|
|
3309
|
+
}
|
|
3307
3310
|
})
|
|
3308
3311
|
} catch (error) {
|
|
3309
3312
|
if (this.channels.get(channelPubkey) === channel && this.desiredChannels.has(channelPubkey)) throw error
|
|
3310
3313
|
} finally { this.decryptionScheduler.finish(channelPubkey) }
|
|
3311
|
-
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
const records = await this.pendingDecryption.records({ force })
|
|
3317
|
+
const probes = new Map()
|
|
3318
|
+
for (const record of records) {
|
|
3319
|
+
const channelPubkey = record.channelPubkey
|
|
3320
|
+
if (!this.channels.has(channelPubkey) || !this.desiredChannels.has(channelPubkey)) continue
|
|
3321
|
+
const missingGroup = pendingMissingContentKeyGroup(record)
|
|
3322
|
+
if (missingGroup && !probes.has(missingGroup)) probes.set(missingGroup, record)
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
const unavailableGroups = new Set()
|
|
3326
|
+
const probed = new Set()
|
|
3327
|
+
let probeError
|
|
3328
|
+
for (const [groupKey, candidate] of probes) {
|
|
3329
|
+
signal.throwIfAborted()
|
|
3330
|
+
const current = await this.pendingDecryption.current(candidate, { force })
|
|
3331
|
+
if (!current || pendingMissingContentKeyGroup(current) !== groupKey) continue
|
|
3332
|
+
probed.add(current.id)
|
|
3333
|
+
let missing = false
|
|
3334
|
+
await replay(current.channelPubkey, [current], {
|
|
3335
|
+
onFailure: async (_record, error) => {
|
|
3336
|
+
const failedGroup = pendingMissingContentKeyGroup({
|
|
3337
|
+
code: error.code,
|
|
3338
|
+
receiverPubkey: error.receiverPubkey,
|
|
3339
|
+
ownContentPubkey: error.ownContentPubkey,
|
|
3340
|
+
hasOwnContentKey: error.hasOwnContentKey
|
|
3341
|
+
})
|
|
3342
|
+
if (failedGroup === groupKey) missing = true
|
|
3343
|
+
if (isDeferredDecryptionError(error)) {
|
|
3344
|
+
await this.pendingDecryption.failed(current, error)
|
|
3345
|
+
return false
|
|
3346
|
+
}
|
|
3347
|
+
probeError ||= error
|
|
3348
|
+
return false
|
|
3349
|
+
}
|
|
3350
|
+
})
|
|
3351
|
+
if (missing) unavailableGroups.add(groupKey)
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
const remaining = []
|
|
3355
|
+
for (const record of records) {
|
|
3356
|
+
if (!this.channels.has(record.channelPubkey) || !this.desiredChannels.has(record.channelPubkey)) continue
|
|
3357
|
+
if (probed.has(record.id)) continue
|
|
3358
|
+
const groupKey = pendingContentKeyGroup(record)
|
|
3359
|
+
if (groupKey && unavailableGroups.has(groupKey)) continue
|
|
3360
|
+
remaining.push(record)
|
|
3361
|
+
}
|
|
3362
|
+
const groups = new Map()
|
|
3363
|
+
for (const record of remaining) {
|
|
3364
|
+
if (!groups.has(record.channelPubkey)) groups.set(record.channelPubkey, [])
|
|
3365
|
+
groups.get(record.channelPubkey).push(record)
|
|
3366
|
+
}
|
|
3367
|
+
const results = await Promise.allSettled([...groups].map(([channelPubkey, channelRecords]) =>
|
|
3368
|
+
replay(channelPubkey, channelRecords)))
|
|
3312
3369
|
const failed = results.find(result => result.status === 'rejected')
|
|
3313
3370
|
if (failed) throw failed.reason
|
|
3371
|
+
if (probeError) throw probeError
|
|
3314
3372
|
job.started = false
|
|
3315
3373
|
} while (job.again)
|
|
3316
3374
|
}
|
|
@@ -6,6 +6,8 @@ import { isDeferredDecryptionError } from '../../private-channel/helpers/decrypt
|
|
|
6
6
|
|
|
7
7
|
const encoder = new TextEncoder()
|
|
8
8
|
const decoder = new TextDecoder()
|
|
9
|
+
const HEX64 = /^[0-9a-f]{64}$/i
|
|
10
|
+
const CONTENT_KEY_FIELDS = ['receiverPubkey', 'ownContentPubkey', 'peerContentPubkey']
|
|
9
11
|
export const PENDING_DECRYPTION_MAX_BYTES = 16 * 1024 * 1024
|
|
10
12
|
|
|
11
13
|
function storedBytes (item) {
|
|
@@ -30,6 +32,34 @@ function packetSnapshot (packet) {
|
|
|
30
32
|
return { format: packet.format, channelPubkey: packet.channelPubkey, outer: event(packet.outer), ...(packet.router ? { router: event(packet.router, true) } : {}) }
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
function errorMetadata (error) {
|
|
36
|
+
const metadata = {}
|
|
37
|
+
for (const key of CONTENT_KEY_FIELDS) {
|
|
38
|
+
const value = error?.[key]
|
|
39
|
+
if (typeof value === 'string' && HEX64.test(value)) metadata[key] = value.toLowerCase()
|
|
40
|
+
}
|
|
41
|
+
if (typeof error?.hasOwnContentKey === 'boolean') metadata.hasOwnContentKey = error.hasOwnContentKey
|
|
42
|
+
return metadata
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function replaceErrorMetadata (item, error) {
|
|
46
|
+
const next = { ...item, code: error?.code }
|
|
47
|
+
for (const key of [...CONTENT_KEY_FIELDS, 'hasOwnContentKey']) delete next[key]
|
|
48
|
+
return Object.assign(next, errorMetadata(error))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function pendingContentKeyGroup (item) {
|
|
52
|
+
const receiver = item?.receiverPubkey
|
|
53
|
+
const own = item?.ownContentPubkey
|
|
54
|
+
if (typeof receiver !== 'string' || typeof own !== 'string' || !HEX64.test(receiver) || !HEX64.test(own)) return ''
|
|
55
|
+
return `${receiver.toLowerCase()}:${own.toLowerCase()}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function pendingMissingContentKeyGroup (item) {
|
|
59
|
+
if (item?.code !== 'CONTENT_KEY_NOT_FOUND' || item?.hasOwnContentKey !== false) return ''
|
|
60
|
+
return pendingContentKeyGroup(item)
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
// Stable across live, history and synthesized recovery wrappers.
|
|
34
64
|
function packetId (record) {
|
|
35
65
|
const rows = record.format === 'routerRow_v1'
|
|
@@ -72,8 +102,19 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
72
102
|
record = packetSnapshot(record)
|
|
73
103
|
return serial(async () => {
|
|
74
104
|
const id = packetId(record)
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
const previous = await queue.getBy('byId', id)
|
|
106
|
+
if (previous) {
|
|
107
|
+
const updated = replaceErrorMetadata(previous, error)
|
|
108
|
+
if (JSON.stringify(updated) !== JSON.stringify(previous)) await queue.putBy('byId', updated, { existingOnly: true })
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
const item = {
|
|
112
|
+
id,
|
|
113
|
+
channelPubkey: record.channelPubkey,
|
|
114
|
+
packet: structuredClone(record),
|
|
115
|
+
macAttempts: error.code === 'INVALID_MAC' ? 1 : 0,
|
|
116
|
+
...replaceErrorMetadata({}, error)
|
|
117
|
+
}
|
|
77
118
|
const requiredBytes = storedBytes(item)
|
|
78
119
|
if (requiredBytes > maxBytes) {
|
|
79
120
|
notify('discarded', { reason: 'oversized', channelPubkey: item.channelPubkey, id }); return true
|
|
@@ -115,7 +156,10 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
115
156
|
if (!isDeferredDecryptionError(error)) throw error
|
|
116
157
|
const current = await queue.getBy('byId', item.id)
|
|
117
158
|
if (!current) return
|
|
118
|
-
const updated = {
|
|
159
|
+
const updated = {
|
|
160
|
+
...replaceErrorMetadata(current, error),
|
|
161
|
+
macAttempts: current.macAttempts + (error.code === 'INVALID_MAC' ? 1 : 0)
|
|
162
|
+
}
|
|
119
163
|
await queue.putBy('byId', updated, { existingOnly: true })
|
|
120
164
|
if (updated.macAttempts >= 3 && item.macAttempts < 3) notify('suspended', { id: item.id, code: error.code, channelPubkey: item.channelPubkey })
|
|
121
165
|
})
|
|
@@ -9,6 +9,13 @@ the account messenger while available. Call after committed content-key changes;
|
|
|
9
9
|
ordinary availability recovery already requests a pass. Ciphertext stays in the
|
|
10
10
|
separate bounded FIFO until admitted to the normal inbox, evicted or explicitly
|
|
11
11
|
cleared. This does not retry application storage/permission failures as crypto.
|
|
12
|
+
Within one pass, records whose last failure proved the same
|
|
13
|
+
`(receiverPubkey, ownContentPubkey)` is missing share one FIFO probe across all
|
|
14
|
+
channels. An unchanged `CONTENT_KEY_NOT_FOUND` skips only that group's remaining
|
|
15
|
+
records for the pass; none are removed or suspended. If the probe advances past
|
|
16
|
+
the key lookup, including with `INVALID_MAC`, or fails with another error, all
|
|
17
|
+
other records in the group are still attempted. Missing-key suppression never
|
|
18
|
+
survives into another pass.
|
|
12
19
|
|
|
13
20
|
Construction accepts `pendingDecryptionReady` (default `true`). Pass `false` to
|
|
14
21
|
hold pending queue reads/replay until `setPendingDecryptionReady(true)` releases
|