Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba1c1b0fdb | |||
| 876af2e96c | |||
| 5bb18da6bf | |||
| 52d8ddae32 | |||
| 8c8c5a312e | |||
| 52756fb50d |
+26
-39
@@ -1213,7 +1213,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
return await query(stanza)
|
return await query(stanza)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => {
|
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false, decryptionError?: string) => {
|
||||||
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
||||||
const { key: msgKey } = fullMessage
|
const { key: msgKey } = fullMessage
|
||||||
const msgId = msgKey.id!
|
const msgId = msgKey.id!
|
||||||
@@ -1311,39 +1311,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
||||||
const fromJid = node.attrs.from!
|
const fromJid = node.attrs.from!
|
||||||
|
|
||||||
// Check if we should recreate the session
|
// Derive the Signal error code from the actual decryption failure message.
|
||||||
let shouldRecreateSession = false
|
// This is sent in the retry receipt so the peer (even another InfiniteAPI instance)
|
||||||
let recreateReason = ''
|
// knows the exact reason and can recreate the session immediately instead of waiting
|
||||||
|
// for the 1-hour timeout fallback.
|
||||||
if (enableAutoSessionRecreation && messageRetryManager && retryCount >= 1) {
|
//
|
||||||
try {
|
// Codes mirror RetryReason enum in message-retry-manager.ts:
|
||||||
// Check if we have a session with this JID
|
// 0 = UnknownError | 1 = NoSession | 2 = InvalidKey
|
||||||
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
|
// 3 = InvalidKeyId | 7 = BadMac (= SignalErrorInvalidMessage/InvalidCipherKey)
|
||||||
const hasSession = await signalRepository.validateSession(fromJid)
|
//
|
||||||
|
// NOTE: We do NOT delete the session here (receiver side). The Signal Protocol
|
||||||
// Extract error code from retry node if present (for MAC error detection)
|
// recovers automatically when the sender's pkmsg arrives — it overwrites the
|
||||||
const retryNode = getBinaryNodeChild(node, 'retry')
|
// corrupted session. Deleting prematurely creates a race window where no session
|
||||||
const errorAttr = retryNode?.attrs?.error
|
// exists, which can cause "No Session" errors on concurrent messages.
|
||||||
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr)
|
const retryErrorCode = (() => {
|
||||||
|
if (!decryptionError) return 0
|
||||||
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode)
|
if (/bad\s*mac/i.test(decryptionError)) return 7 // SignalErrorBadMac
|
||||||
shouldRecreateSession = result.recreate
|
if (/no\s*session/i.test(decryptionError)) return 1 // SignalErrorNoSession
|
||||||
recreateReason = result.reason
|
if (/pre\s*key/i.test(decryptionError)) return 3 // SignalErrorInvalidKeyId
|
||||||
|
if (/invalid\s*key/i.test(decryptionError)) return 2 // SignalErrorInvalidKey
|
||||||
if (shouldRecreateSession) {
|
return 0
|
||||||
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry')
|
})()
|
||||||
// Delete existing session to force recreation
|
|
||||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
|
||||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
|
||||||
await authState.keys.transaction(async () => {
|
|
||||||
await authState.keys.set({ session: { [sessionId]: null } })
|
|
||||||
}, authState.creds.me?.id || 'session-operation')
|
|
||||||
forceIncludeKeys = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn({ error, fromJid }, 'failed to check session recreation')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retryCount <= 2) {
|
if (retryCount <= 2) {
|
||||||
// Use new retry manager for phone requests if available
|
// Use new retry manager for phone requests if available
|
||||||
@@ -1384,8 +1372,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
id: node.attrs.id!,
|
id: node.attrs.id!,
|
||||||
t: node.attrs.t!,
|
t: node.attrs.t!,
|
||||||
v: '1',
|
v: '1',
|
||||||
// ADD ERROR FIELD
|
error: retryErrorCode.toString()
|
||||||
error: '0'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1404,7 +1391,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
receipt.attrs.participant = node.attrs.participant
|
receipt.attrs.participant = node.attrs.participant
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
if (retryCount > 1 || forceIncludeKeys) {
|
||||||
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
||||||
|
|
||||||
const [keyId] = Object.keys(preKeys)
|
const [keyId] = Object.keys(preKeys)
|
||||||
@@ -2507,7 +2494,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
if (retryRequestDelayMs) {
|
if (retryRequestDelayMs) {
|
||||||
await delay(retryRequestDelayMs)
|
await delay(retryRequestDelayMs)
|
||||||
}
|
}
|
||||||
@@ -2516,7 +2503,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
// Still attempt retry even if pre-key upload failed
|
// Still attempt retry even if pre-key upload failed
|
||||||
try {
|
try {
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -701,10 +701,12 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent multiple concurrent uploads
|
// Prevent multiple concurrent uploads — if one is already running, wait for it and return:
|
||||||
|
// the concurrent upload already replenished the pool, so there is nothing left to do.
|
||||||
if (uploadPreKeysPromise) {
|
if (uploadPreKeysPromise) {
|
||||||
logger.debug('Pre-key upload already in progress, waiting for completion')
|
logger.debug('Pre-key upload already in progress, waiting for completion')
|
||||||
await uploadPreKeysPromise
|
await uploadPreKeysPromise
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadLogic = async () => {
|
const uploadLogic = async () => {
|
||||||
@@ -784,14 +786,15 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
try {
|
try {
|
||||||
let count = 0
|
let count = 0
|
||||||
const preKeyCount = await getAvailablePreKeysOnServer()
|
const preKeyCount = await getAvailablePreKeysOnServer()
|
||||||
if (preKeyCount === 0) count = INITIAL_PREKEY_COUNT
|
// How many to upload: top-up to INITIAL_PREKEY_COUNT from whatever remains on server
|
||||||
else count = Math.max(0, INITIAL_PREKEY_COUNT - preKeyCount)
|
count = Math.max(0, INITIAL_PREKEY_COUNT - preKeyCount)
|
||||||
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
|
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
|
||||||
|
|
||||||
logger.info(`${preKeyCount} pre-keys found on server`)
|
logger.info(`${preKeyCount} pre-keys found on server`)
|
||||||
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
|
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
|
||||||
|
|
||||||
const lowServerCount = preKeyCount <= count
|
// Trigger upload when below the replenishment threshold, not when count < topUp amount
|
||||||
|
const lowServerCount = preKeyCount < MIN_PREKEY_COUNT
|
||||||
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
|
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
|
||||||
|
|
||||||
const shouldUpload = lowServerCount || missingCurrentPreKey
|
const shouldUpload = lowServerCount || missingCurrentPreKey
|
||||||
@@ -1307,7 +1310,10 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
logger.warn('keep alive called when WS not open')
|
logger.warn('keep alive called when WS not open')
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleNextKeepAlive()
|
// Do not reschedule once shutdown has started (closed set by end() on any concurrent path)
|
||||||
|
if (!closed) {
|
||||||
|
scheduleNextKeepAlive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleNextKeepAlive()
|
scheduleNextKeepAlive()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { toNumber } from './generics'
|
import { toNumber } from './generics'
|
||||||
import type { ILogger } from './logger.js'
|
import type { ILogger } from './logger.js'
|
||||||
import { normalizeMessageContent } from './messages'
|
import { normalizeMessageContent } from './messages'
|
||||||
|
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||||
import { downloadContentFromMessage, getUrlFromDirectPath } from './messages-media'
|
import { downloadContentFromMessage, getUrlFromDirectPath } from './messages-media'
|
||||||
|
|
||||||
const inflatePromise = promisify(inflate)
|
const inflatePromise = promisify(inflate)
|
||||||
@@ -430,7 +431,11 @@ export const downloadAndProcessHistorySyncNotification = async (
|
|||||||
// processing throws — the server copy would be gone and retry after reconnect would fail.
|
// processing throws — the server copy would be gone and retry after reconnect would fail.
|
||||||
if (msg.directPath) {
|
if (msg.directPath) {
|
||||||
const cdnUrl = getUrlFromDirectPath(msg.directPath)
|
const cdnUrl = getUrlFromDirectPath(msg.directPath)
|
||||||
fetch(cdnUrl, { ...options, method: 'DELETE' }).catch(() => {
|
fetch(cdnUrl, {
|
||||||
|
...options,
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { ...((options as RequestInit).headers ?? {}), Origin: DEFAULT_ORIGIN }
|
||||||
|
}).catch(() => {
|
||||||
// non-fatal — server will expire it anyway
|
// non-fatal — server will expire it anyway
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user