fix: address PR review comments for tctoken lifecycle

- Wire onNewJidStored callback in messages-send.ts so proactively
  fetched tokens are registered in the pruning index
- Batch keystore reads in pruneExpiredTcTokens (1 query instead of N)
- Persist lastTcTokenPruneTs in keystore so 24h throttle survives restarts
- Replace parseInt() with Number() for safer timestamp parsing
- Normalize JIDs via jidNormalizedUser() before LID lookup in
  resolveTcTokenJid to prevent device-suffix mismatches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Renato Alcara
2026-02-15 21:35:45 -03:00
parent 08793904e2
commit 39de1f0582
3 changed files with 56 additions and 14 deletions
+26 -9
View File
@@ -153,14 +153,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// ======= tctoken index tracking for cross-session pruning =======
const TC_TOKEN_INDEX_KEY = '__index'
const TC_TOKEN_PRUNE_TS_KEY = '__prune_ts'
const tcTokenKnownJids = new Set<string>()
let tcTokenIndexSaveTimer: ReturnType<typeof setTimeout> | undefined
let lastTcTokenPruneTs = 0
// Load persisted JID index on startup
// Load persisted JID index and last prune timestamp on startup
const tcTokenIndexLoaded = (async () => {
try {
const data = await authState.keys.get('tctoken', [TC_TOKEN_INDEX_KEY])
const data = await authState.keys.get('tctoken', [TC_TOKEN_INDEX_KEY, TC_TOKEN_PRUNE_TS_KEY])
const entry = data[TC_TOKEN_INDEX_KEY]
if(entry?.token) {
const stored = JSON.parse(Buffer.from(entry.token).toString('utf8'))
@@ -168,6 +169,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
for(const jid of stored) tcTokenKnownJids.add(jid)
}
}
const pruneEntry = data[TC_TOKEN_PRUNE_TS_KEY]
if(pruneEntry?.timestamp) {
lastTcTokenPruneTs = Number(pruneEntry.timestamp)
}
} catch { /* first run or corrupt index — start fresh */ }
})()
@@ -197,19 +203,21 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const pruneSet: Record<string, null> = {}
const survivingJids: string[] = []
for(const jid of tcTokenKnownJids) {
if(jid === TC_TOKEN_INDEX_KEY) continue
try {
const data = await authState.keys.get('tctoken', [jid])
const entry = data[jid]
const jidsToCheck = Array.from(tcTokenKnownJids).filter(j => j !== TC_TOKEN_INDEX_KEY)
if(!jidsToCheck.length) return
try {
const allData = await authState.keys.get('tctoken', jidsToCheck)
for(const jid of jidsToCheck) {
const entry = allData[jid]
if(!entry?.token || isTcTokenExpired(entry.timestamp)) {
pruneSet[jid] = null
} else {
survivingJids.push(jid)
}
} catch {
pruneSet[jid] = null
}
} catch {
return // batch read failed — skip this pruning cycle
}
const pruneCount = Object.keys(pruneSet).length
@@ -2050,6 +2058,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const ONE_DAY_MS = 86400000
if(now - lastTcTokenPruneTs > ONE_DAY_MS) {
lastTcTokenPruneTs = now
// Persist prune timestamp so it survives restarts
Promise.resolve(authState.keys.set({
tctoken: {
[TC_TOKEN_PRUNE_TS_KEY]: {
token: Buffer.alloc(0),
timestamp: now.toString()
}
}
})).catch(() => { /* non-critical */ })
pruneExpiredTcTokens().catch(err => {
logger.debug({ err: err?.message }, 'tctoken pruning failed')
})
+25 -1
View File
@@ -1519,7 +1519,31 @@ export const makeMessagesSocket = (config: SocketConfig) => {
result: fetchResult,
fallbackJid: destinationJid,
keys: authState.keys,
getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping),
onNewJidStored: (storedJid) => {
// Fire-and-forget: persist JID into tctoken index for pruning
(async () => {
try {
const TC_IDX = '__index'
const idxData = await authState.keys.get('tctoken', [TC_IDX])
const idxEntry = idxData[TC_IDX]
const existing: string[] = idxEntry?.token
? JSON.parse(Buffer.from(idxEntry.token).toString('utf8'))
: []
if(!existing.includes(storedJid)) {
existing.push(storedJid)
await authState.keys.set({
tctoken: {
[TC_IDX]: {
token: Buffer.from(JSON.stringify(existing), 'utf8'),
timestamp: unixTimestampSeconds().toString()
}
}
})
}
} catch { /* non-critical — index rebuilt on next pruning cycle */ }
})()
}
})
// Re-read from key store — the notification handler or inline
+5 -4
View File
@@ -19,7 +19,7 @@ const TC_TOKEN_NUM_BUCKETS = 4
*/
export function isTcTokenExpired(timestamp: number | string | null | undefined): boolean {
if(timestamp === null || timestamp === undefined) return true
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
const ts = typeof timestamp === 'string' ? Number(timestamp) : timestamp
if(isNaN(ts)) return true
const now = Math.floor(Date.now() / 1000)
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
@@ -57,9 +57,10 @@ export async function resolveTcTokenJid(
jid: string,
getLIDForPN: (pn: string) => Promise<string | null>
): Promise<string> {
if(isLidUser(jid)) return jid
const lid = await getLIDForPN(jid)
return lid ?? jid
const normalized = jidNormalizedUser(jid)
if(isLidUser(normalized)) return normalized
const lid = await getLIDForPN(normalized)
return lid ?? normalized
}
type TcTokenParams = {