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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.30",
3
+ "version": "0.11.31",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -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 groups = new Map()
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 => { current(); return this.pendingDecryption.recovered(record) },
3306
- onFailure: (record, error) => { current(); return this.pendingDecryption.failed(record, error) }
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
- if (await queue.someBy('byId', id)) return true
76
- const item = { id, channelPubkey: record.channelPubkey, packet: structuredClone(record), code: error.code, macAttempts: error.code === 'INVALID_MAC' ? 1 : 0 }
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 = { ...current, code: error.code, macAttempts: current.macAttempts + (error.code === 'INVALID_MAC' ? 1 : 0) }
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