diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 3d4bceb5..32c7dc04 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -69,7 +69,7 @@ import { recordMessageReceived, recordMessageRetry } from '../Utils/prometheus-metrics.js' -import { isTcTokenExpired, resolveTcTokenJid } from '../Utils/tc-token-utils' +import { isTcTokenExpired, resolveTcTokenJid, storeTcTokensFromIqResult } from '../Utils/tc-token-utils' import { areJidsSameUser, type BinaryNode, @@ -1453,6 +1453,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { }) // When a session is refreshed (identity change), re-issue tctoken fire-and-forget + // WABA Android: reissue stores senderTimestamp + realIssueTimestamp after IQ success if (result.action === 'session_refreshed') { const normalizedJid = jidNormalizedUser(from) resolveTcTokenJid(normalizedJid, getLIDForPN) @@ -1462,9 +1463,36 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { if (entry?.token?.length && !isTcTokenExpired(entry.timestamp)) { const senderTs = unixTimestampSeconds() logTcToken('reissue', { jid: normalizedJid, reason: 'session_refreshed' }) - getPrivacyTokens([normalizedJid], senderTs).catch(err => { - logTcToken('reissue_fail', { jid: normalizedJid, error: err?.message }) - }) + getPrivacyTokens([normalizedJid], senderTs) + .then(async (iqResult) => { + await storeTcTokensFromIqResult({ + result: iqResult, + fallbackJid: normalizedJid, + keys: authState.keys, + getLIDForPN, + onNewJidStored: (storedJid) => { + tcTokenKnownJids.add(storedJid) + scheduleTcTokenIndexSave() + } + }) + // Persist senderTimestamp + realIssueTimestamp after IQ success + const currentData = await authState.keys.get('tctoken', [tcJid]) + const currentEntry = currentData[tcJid] + await authState.keys.set({ + tctoken: { + [tcJid]: { + ...currentEntry, + token: currentEntry?.token ?? Buffer.alloc(0), + senderTimestamp: senderTs, + realIssueTimestamp: 0 + } + } + }) + logTcToken('reissue_ok', { jid: normalizedJid, reason: 'session_refreshed' }) + }) + .catch(err => { + logTcToken('reissue_fail', { jid: normalizedJid, error: err?.message }) + }) } }) .catch(() => { @@ -1912,7 +1940,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { [storageJid]: { ...existing, token: Buffer.from(content), - timestamp + timestamp, + // WABA Android: resets real_issue_timestamp when a new incoming token arrives + // (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null) + realIssueTimestamp: null } } }) @@ -2823,6 +2854,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const jid = jidNormalizedUser(attrs.from) logTcToken('error_463', { jid, msgId }) + // WABA Android: error 463 triggers getPrivacyTokens() fire-and-forget + // to ensure token is available for the retry below + getPrivacyTokens([jid]) + .then(async (result) => { + await storeTcTokensFromIqResult({ + result, + fallbackJid: jid, + keys: authState.keys, + getLIDForPN, + onNewJidStored: (storedJid) => { + tcTokenKnownJids.add(storedJid) + scheduleTcTokenIndexSave() + } + }) + logTcToken('fetched', { jid, reason: 'error_463' }) + }) + .catch(() => { /* fire-and-forget */ }) + // Single-retry: wait 1.5s for the server's tctoken notification to arrive, // then resend. A Set prevents infinite retry loops. // Composite key (jid:msgId) ensures retries are isolated per destination. @@ -2858,7 +2907,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { return } } else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) { - logTcToken('error_479', { jid: attrs.from, msgId: attrs.id }) + const jid479 = jidNormalizedUser(attrs.from) + logTcToken('error_479', { jid: jid479, msgId: attrs.id }) + // WABA Android: error 479 (SmaxInvalid) also triggers token re-fetch + getPrivacyTokens([jid479]) + .then(async (result) => { + await storeTcTokensFromIqResult({ + result, + fallbackJid: jid479, + keys: authState.keys, + getLIDForPN, + onNewJidStored: (storedJid) => { + tcTokenKnownJids.add(storedJid) + scheduleTcTokenIndexSave() + } + }) + logTcToken('fetched', { jid: jid479, reason: 'error_479' }) + }) + .catch(() => { /* fire-and-forget */ }) } else { logger.warn({ attrs }, 'received error in ack') } diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index c638841f..df0e9727 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1611,6 +1611,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Persist senderTimestamp unconditionally — WA Web stores it in the chat table // regardless of whether a token exists. Spread preserves token+timestamp if present. + // WABA Android: INSERT INTO wa_trusted_contacts_send (jid, sent_tc_token_timestamp, real_issue_timestamp) + // VALUES (?, ?, 0) — realIssueTimestamp=0 means issued but not yet confirmed by server const currentData = await authState.keys.get('tctoken', [tcTokenJid]) const currentEntry = currentData[tcTokenJid] await authState.keys.set({ @@ -1618,7 +1620,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { [tcTokenJid]: { ...currentEntry, token: currentEntry?.token ?? Buffer.alloc(0), - senderTimestamp: issueTimestamp + senderTimestamp: issueTimestamp, + realIssueTimestamp: 0 } } }) diff --git a/src/Types/Auth.ts b/src/Types/Auth.ts index 0f9bf2ac..5268c205 100644 --- a/src/Types/Auth.ts +++ b/src/Types/Auth.ts @@ -80,7 +80,7 @@ export type SignalDataTypeMap = { 'app-state-sync-version': LTHashState 'lid-mapping': string 'device-list': string[] - tctoken: { token: Buffer; timestamp?: string; senderTimestamp?: number } + tctoken: { token: Buffer; timestamp?: string; senderTimestamp?: number; realIssueTimestamp?: number | null } /** Identity key for Signal Protocol - used for detecting contact reinstalls */ 'identity-key': Uint8Array } diff --git a/src/Utils/tc-token-utils.ts b/src/Utils/tc-token-utils.ts index 9f680bcd..bec77c18 100644 --- a/src/Utils/tc-token-utils.ts +++ b/src/Utils/tc-token-utils.ts @@ -159,7 +159,10 @@ export async function storeTcTokensFromIqResult({ [storageJid]: { ...existingEntry, token: Buffer.from(tokenNode.content), - timestamp: tokenNode.attrs.t + timestamp: tokenNode.attrs.t, + // WABA Android: resets real_issue_timestamp to null when storing a new token + // (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null) + realIssueTimestamp: null } } })