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:
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user