feat: integrate tctoken lifecycle with expiration, pruning and re-issuance (PR #2339)
Surgical integration of WhiskeySockets/Baileys PR #2339 preserving all InfiniteAPI customizations (biz nodes, DSM skip, carousel, Prometheus, circuit breaker, LID mapping, CTWA recovery, identity debounce). Changes: - tc-token-utils.ts: rolling bucket expiration (28d/4 buckets), LID resolution, monotonicity guard, storeTcTokensFromIqResult parser - messages-send.ts: proactive fetch if missing/expired, fire-and-forget re-issuance on bucket boundary, getPrivacyTokens timestamp param - messages-recv.ts: persistent JID index for cross-session pruning, pruneExpiredTcTokens on connect (max 1x/24h), session_refreshed re-issuance, error 463/479 handling in handleBadAck - chats.ts: self-detection in profilePictureUrl, LID resolver in presenceSubscribe - socket.ts: granular stream error logging (device_removed, xml, ack) - Auth.ts: senderTimestamp field on tctoken type - Types/index.ts: sessionInvalidated = 516 disconnect reason - decode-wa-message.ts: SERVER_ERROR_CODES constant (463, 479, 421, 475) - generics.ts: enhanced getErrorCodeFromStreamError with device_removed - baileys-logger.ts: logTcToken function following [BAILEYS] prefix pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+27
-4
@@ -50,6 +50,8 @@ import {
|
||||
type BinaryNode,
|
||||
getBinaryNodeChild,
|
||||
getBinaryNodeChildren,
|
||||
isLidUser,
|
||||
isPnUser,
|
||||
jidDecode,
|
||||
jidNormalizedUser,
|
||||
reduceBinaryNodeToDictionary,
|
||||
@@ -695,9 +697,26 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
const profilePictureUrl = async (jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => {
|
||||
const baseContent: BinaryNode[] = [{ tag: 'picture', attrs: { type, query: 'url' } }]
|
||||
|
||||
const tcTokenContent = await buildTcTokenFromJid({ authState, jid, baseContent })
|
||||
// WA Web only includes tctoken for user JIDs (not groups/newsletters)
|
||||
// and never for own profile pic (Chat model for self has no tcToken).
|
||||
// Including tctoken for own JID causes the server to never respond.
|
||||
const normalizedJid = jidNormalizedUser(jid)
|
||||
const isUserJid = isPnUser(normalizedJid) || isLidUser(normalizedJid)
|
||||
const me = authState.creds.me
|
||||
const isSelf =
|
||||
me && (normalizedJid === jidNormalizedUser(me.id) || (me.lid && normalizedJid === jidNormalizedUser(me.lid)))
|
||||
let content: BinaryNode[] | undefined = baseContent
|
||||
|
||||
jid = jidNormalizedUser(jid)
|
||||
if(isUserJid && !isSelf) {
|
||||
content = await buildTcTokenFromJid({
|
||||
authState,
|
||||
jid: normalizedJid,
|
||||
baseContent,
|
||||
getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
})
|
||||
}
|
||||
|
||||
jid = normalizedJid
|
||||
const result = await query(
|
||||
{
|
||||
tag: 'iq',
|
||||
@@ -707,7 +726,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
type: 'get',
|
||||
xmlns: 'w:profile:picture'
|
||||
},
|
||||
content: tcTokenContent
|
||||
content
|
||||
},
|
||||
timeoutMs
|
||||
)
|
||||
@@ -799,7 +818,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
* @param tcToken token for subscription, use if present
|
||||
*/
|
||||
const presenceSubscribe = async (toJid: string) => {
|
||||
const tcTokenContent = await buildTcTokenFromJid({ authState, jid: toJid })
|
||||
const tcTokenContent = await buildTcTokenFromJid({
|
||||
authState,
|
||||
jid: toJid,
|
||||
getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
})
|
||||
|
||||
return sendNode({
|
||||
tag: 'presence',
|
||||
|
||||
+148
-32
@@ -47,13 +47,14 @@ import {
|
||||
MISSING_KEYS_ERROR_TEXT,
|
||||
NACK_REASONS,
|
||||
NO_MESSAGE_FOUND_ERROR_TEXT,
|
||||
SERVER_ERROR_CODES,
|
||||
normalizeMessageJids,
|
||||
toNumber,
|
||||
unixTimestampSeconds,
|
||||
xmppPreKey,
|
||||
xmppSignedPreKey
|
||||
} from '../Utils'
|
||||
import { logMessageReceived } from '../Utils/baileys-logger'
|
||||
import { logMessageReceived, logTcToken } from '../Utils/baileys-logger'
|
||||
import { makeMutex } from '../Utils/make-mutex'
|
||||
import {
|
||||
metrics,
|
||||
@@ -80,6 +81,7 @@ import {
|
||||
jidNormalizedUser,
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import { isTcTokenExpired, resolveTcTokenJid } from '../Utils/tc-token-utils'
|
||||
import { extractGroupMetadata } from './groups'
|
||||
import { makeMessagesSocket } from './messages-send'
|
||||
|
||||
@@ -117,7 +119,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
sendReceipt,
|
||||
uploadPreKeys,
|
||||
sendPeerDataOperationMessage,
|
||||
messageRetryManager
|
||||
messageRetryManager,
|
||||
getPrivacyTokens
|
||||
} = sock
|
||||
|
||||
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
||||
@@ -148,6 +151,78 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
let sendActiveReceipts = false
|
||||
|
||||
// ======= tctoken index tracking for cross-session pruning =======
|
||||
const TC_TOKEN_INDEX_KEY = '__index'
|
||||
const tcTokenKnownJids = new Set<string>()
|
||||
let tcTokenIndexSaveTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let lastTcTokenPruneTs = 0
|
||||
|
||||
// Load persisted JID index on startup
|
||||
const tcTokenIndexLoaded = (async () => {
|
||||
try {
|
||||
const data = await authState.keys.get('tctoken', [TC_TOKEN_INDEX_KEY])
|
||||
const entry = data[TC_TOKEN_INDEX_KEY]
|
||||
if(entry?.token) {
|
||||
const stored = JSON.parse(Buffer.from(entry.token).toString('utf8'))
|
||||
if(Array.isArray(stored)) {
|
||||
for(const jid of stored) tcTokenKnownJids.add(jid)
|
||||
}
|
||||
}
|
||||
} catch { /* first run or corrupt index — start fresh */ }
|
||||
})()
|
||||
|
||||
/** Debounced save of the tctoken JID index (5s) */
|
||||
const scheduleTcTokenIndexSave = () => {
|
||||
if(tcTokenIndexSaveTimer) clearTimeout(tcTokenIndexSaveTimer)
|
||||
tcTokenIndexSaveTimer = setTimeout(async () => {
|
||||
try {
|
||||
const arr = Array.from(tcTokenKnownJids)
|
||||
await authState.keys.set({
|
||||
tctoken: {
|
||||
[TC_TOKEN_INDEX_KEY]: {
|
||||
token: Buffer.from(JSON.stringify(arr), 'utf8'),
|
||||
timestamp: unixTimestampSeconds().toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch(err) {
|
||||
logger.debug({ err }, 'failed to persist tctoken index')
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
/** Delete expired tctokens — runs at most once per 24h when coming online */
|
||||
const pruneExpiredTcTokens = async () => {
|
||||
await tcTokenIndexLoaded
|
||||
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]
|
||||
if(!entry?.token || isTcTokenExpired(entry.timestamp)) {
|
||||
pruneSet[jid] = null
|
||||
} else {
|
||||
survivingJids.push(jid)
|
||||
}
|
||||
} catch {
|
||||
pruneSet[jid] = null
|
||||
}
|
||||
}
|
||||
|
||||
const pruneCount = Object.keys(pruneSet).length
|
||||
if(pruneCount > 0) {
|
||||
await authState.keys.set({ tctoken: pruneSet })
|
||||
tcTokenKnownJids.clear()
|
||||
for(const jid of survivingJids) tcTokenKnownJids.add(jid)
|
||||
scheduleTcTokenIndexSave()
|
||||
logTcToken('prune', { pruned: pruneCount, remaining: survivingJids.length })
|
||||
}
|
||||
}
|
||||
// ======= END tctoken index tracking =======
|
||||
|
||||
const fetchMessageHistory = async (
|
||||
count: number,
|
||||
oldestMsgKey: WAMessageKey,
|
||||
@@ -633,7 +708,26 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
logger
|
||||
})
|
||||
|
||||
if (result.action === 'no_identity_node') {
|
||||
// When a session is refreshed (identity change), re-issue tctoken fire-and-forget
|
||||
if(result.action === 'session_refreshed') {
|
||||
const normalizedJid = jidNormalizedUser(from)
|
||||
resolveTcTokenJid(
|
||||
normalizedJid,
|
||||
signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
).then(async (tcJid) => {
|
||||
const tcData = await authState.keys.get('tctoken', [tcJid])
|
||||
const entry = tcData[tcJid]
|
||||
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 })
|
||||
})
|
||||
}
|
||||
}).catch(() => { /* ignore resolution errors */ })
|
||||
}
|
||||
|
||||
if(result.action === 'no_identity_node') {
|
||||
logger.info({ node }, 'unknown encrypt notification')
|
||||
}
|
||||
}
|
||||
@@ -958,28 +1052,45 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const tokensNode = getBinaryNodeChild(node, 'tokens')
|
||||
const from = jidNormalizedUser(node.attrs.from)
|
||||
|
||||
if (!tokensNode) return
|
||||
if(!tokensNode) return
|
||||
|
||||
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token')
|
||||
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
|
||||
for (const tokenNode of tokenNodes) {
|
||||
for(const tokenNode of tokenNodes) {
|
||||
const { attrs, content } = tokenNode
|
||||
const type = attrs.type
|
||||
const timestamp = attrs.t
|
||||
|
||||
if (type === 'trusted_contact' && content instanceof Buffer) {
|
||||
logger.debug(
|
||||
{
|
||||
from,
|
||||
timestamp,
|
||||
tcToken: content
|
||||
},
|
||||
'received trusted contact token'
|
||||
)
|
||||
if(type === 'trusted_contact' && content instanceof Uint8Array) {
|
||||
// Resolve to LID for consistent storage key
|
||||
const senderLid = attrs.sender_lid ? jidNormalizedUser(attrs.sender_lid) : undefined
|
||||
const storageJid = senderLid || await resolveTcTokenJid(from, getLIDForPN)
|
||||
|
||||
// Timestamp monotonicity guard — only store if incoming >= existing
|
||||
const existingData = await authState.keys.get('tctoken', [storageJid])
|
||||
const existing = existingData[storageJid]
|
||||
const existingTs = existing?.timestamp ? Number(existing.timestamp) : 0
|
||||
const incomingTs = timestamp ? Number(timestamp) : 0
|
||||
if(existingTs > 0 && incomingTs > 0 && existingTs > incomingTs) {
|
||||
continue
|
||||
}
|
||||
|
||||
await authState.keys.set({
|
||||
tctoken: { [from]: { token: content, timestamp } }
|
||||
tctoken: {
|
||||
[storageJid]: {
|
||||
...existing,
|
||||
token: Buffer.from(content),
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
logTcToken('stored', { jid: storageJid, from })
|
||||
|
||||
// Track JID for cross-session pruning
|
||||
tcTokenKnownJids.add(storageJid)
|
||||
scheduleTcTokenIndexSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1755,8 +1866,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
// error in acknowledgement,
|
||||
// device could not display the message
|
||||
if (attrs.error) {
|
||||
logger.warn({ attrs }, 'received error in ack')
|
||||
if(attrs.error) {
|
||||
if(attrs.error === SERVER_ERROR_CODES.MissingTcToken) {
|
||||
logTcToken('error_463', { jid: attrs.from, msgId: attrs.id })
|
||||
} else if(attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
||||
logTcToken('error_479', { jid: attrs.from, msgId: attrs.id })
|
||||
} else {
|
||||
logger.warn({ attrs }, 'received error in ack')
|
||||
}
|
||||
|
||||
ev.emit('messages.update', [
|
||||
{
|
||||
key,
|
||||
@@ -1766,20 +1884,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// resend the message with device_fanout=false, use at your own risk
|
||||
// if (attrs.error === '475') {
|
||||
// const msg = await getMessage(key)
|
||||
// if (msg) {
|
||||
// await relayMessage(key.remoteJid!, msg, {
|
||||
// messageId: key.id!,
|
||||
// useUserDevicesCache: false,
|
||||
// additionalAttributes: {
|
||||
// device_fanout: 'false'
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1936,9 +2040,21 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
})
|
||||
|
||||
ev.on('connection.update', ({ isOnline }) => {
|
||||
if (typeof isOnline !== 'undefined') {
|
||||
if(typeof isOnline !== 'undefined') {
|
||||
sendActiveReceipts = isOnline
|
||||
logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
|
||||
|
||||
// Prune expired tctokens when coming online (max once per 24h)
|
||||
if(isOnline) {
|
||||
const now = Date.now()
|
||||
const ONE_DAY_MS = 86400000
|
||||
if(now - lastTcTokenPruneTs > ONE_DAY_MS) {
|
||||
lastTcTokenPruneTs = now
|
||||
pruneExpiredTcTokens().catch(err => {
|
||||
logger.debug({ err: err?.message }, 'tctoken pruning failed')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+107
-8
@@ -39,11 +39,17 @@ import {
|
||||
parseAndInjectE2ESessions,
|
||||
unixTimestampSeconds
|
||||
} from '../Utils'
|
||||
import { logMessageSent } from '../Utils/baileys-logger'
|
||||
import { logMessageSent, logTcToken } from '../Utils/baileys-logger'
|
||||
import { getUrlInfo } from '../Utils/link-preview'
|
||||
import { makeKeyedMutex } from '../Utils/make-mutex'
|
||||
import { metrics, recordMessageFailure, recordMessageSent } from '../Utils/prometheus-metrics'
|
||||
import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils'
|
||||
import {
|
||||
isTcTokenExpired,
|
||||
resolveTcTokenJid,
|
||||
shouldSendNewTcToken,
|
||||
storeTcTokensFromIqResult
|
||||
} from '../Utils/tc-token-utils'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
type BinaryNode,
|
||||
@@ -1475,18 +1481,79 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Working carousel includes tctoken in stanza
|
||||
const contactTcTokenData =
|
||||
!isGroup && !isRetryResend && !isStatus ? await authState.keys.get('tctoken', [destinationJid]) : {}
|
||||
// tctoken lifecycle: fetch, validate expiry, proactive re-fetch if missing/expired
|
||||
const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter
|
||||
let didFetchTcToken = false
|
||||
|
||||
const tcTokenBuffer = contactTcTokenData[destinationJid]?.token
|
||||
// Resolve destination to LID for tctoken storage — matches Signal session key pattern
|
||||
const tcTokenJid = is1on1Send
|
||||
? await resolveTcTokenJid(
|
||||
destinationJid,
|
||||
signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
)
|
||||
: destinationJid
|
||||
const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {}
|
||||
const existingTokenEntry = contactTcTokenData[tcTokenJid]
|
||||
let tcTokenBuffer = existingTokenEntry?.token
|
||||
|
||||
if (tcTokenBuffer) {
|
||||
// Treat expired tokens the same as missing — re-fetch from server
|
||||
if(tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
|
||||
logTcToken('expired', { jid: destinationJid, timestamp: existingTokenEntry?.timestamp })
|
||||
tcTokenBuffer = undefined
|
||||
// Opportunistic cleanup: remove expired token from store
|
||||
try {
|
||||
await authState.keys.set({ tctoken: { [tcTokenJid]: null } })
|
||||
} catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
|
||||
// If tctoken is missing or expired for a 1:1 send, proactively fetch it from the server
|
||||
if(!tcTokenBuffer?.length && is1on1Send) {
|
||||
try {
|
||||
logTcToken('fetch', { jid: destinationJid })
|
||||
didFetchTcToken = true
|
||||
const fetchResult = await getPrivacyTokens([destinationJid])
|
||||
|
||||
// Parse inline tokens from IQ result using the shared parser
|
||||
// (includes monotonicity guard)
|
||||
await storeTcTokensFromIqResult({
|
||||
result: fetchResult,
|
||||
fallbackJid: destinationJid,
|
||||
keys: authState.keys,
|
||||
getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
||||
})
|
||||
|
||||
// Re-read from key store — the notification handler or inline
|
||||
// parsing above may have stored the token
|
||||
const refreshed = await authState.keys.get('tctoken', [tcTokenJid])
|
||||
const refreshedEntry = refreshed[tcTokenJid]
|
||||
tcTokenBuffer = refreshedEntry?.token
|
||||
|
||||
// The getPrivacyTokens IQ (type='set') also acts as issuance,
|
||||
// so record senderTimestamp to prevent redundant fire-and-forget
|
||||
// on the next message to this contact.
|
||||
if(refreshedEntry?.token?.length) {
|
||||
logTcToken('fetched', { jid: destinationJid })
|
||||
await authState.keys.set({
|
||||
tctoken: {
|
||||
[tcTokenJid]: {
|
||||
...refreshedEntry,
|
||||
senderTimestamp: unixTimestampSeconds()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch(err: any) {
|
||||
logger.warn({ jid: destinationJid, trace: err?.stack }, 'failed to fetch privacy token before send')
|
||||
}
|
||||
}
|
||||
|
||||
if(tcTokenBuffer?.length) {
|
||||
;(stanza.content as BinaryNode[]).push({
|
||||
tag: 'tctoken',
|
||||
attrs: {},
|
||||
content: tcTokenBuffer
|
||||
})
|
||||
logTcToken('attached', { jid: destinationJid })
|
||||
}
|
||||
|
||||
if (additionalNodes && additionalNodes.length > 0) {
|
||||
@@ -1568,6 +1635,38 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
|
||||
await sendNode(stanza)
|
||||
|
||||
// Fire-and-forget: issue our token to the contact (like WA Web's sendTcToken)
|
||||
// Only for 1:1 sends where we didn't already fetch, and only when bucket boundary crossed
|
||||
if(is1on1Send && !didFetchTcToken && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp)) {
|
||||
const issueTimestamp = unixTimestampSeconds()
|
||||
logTcToken('reissue', { jid: destinationJid })
|
||||
// WA Web writes senderTimestamp only AFTER the IQ succeeds
|
||||
// (WAWebSendTcTokenChatAction.sendTcToken).
|
||||
// This ensures failed issuance allows re-issuance on the next message
|
||||
// rather than blocking it for up to 7 days (one bucket duration).
|
||||
getPrivacyTokens([destinationJid], issueTimestamp)
|
||||
.then(async () => {
|
||||
// Re-read entry to avoid overwriting concurrent notification handler updates
|
||||
const currentData = await authState.keys.get('tctoken', [tcTokenJid])
|
||||
const currentEntry = currentData[tcTokenJid]
|
||||
if(currentEntry?.token?.length) {
|
||||
await authState.keys.set({
|
||||
tctoken: {
|
||||
[tcTokenJid]: {
|
||||
...currentEntry,
|
||||
senderTimestamp: issueTimestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logTcToken('reissue_ok', { jid: destinationJid })
|
||||
})
|
||||
.catch(err => {
|
||||
logTcToken('reissue_fail', { jid: destinationJid, error: err?.message })
|
||||
})
|
||||
}
|
||||
|
||||
// Log with [BAILEYS] prefix
|
||||
logMessageSent(msgId, destinationJid)
|
||||
|
||||
@@ -1677,8 +1776,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const getPrivacyTokens = async (jids: string[]) => {
|
||||
const t = unixTimestampSeconds().toString()
|
||||
const getPrivacyTokens = async (jids: string[], timestamp?: number) => {
|
||||
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
|
||||
+10
-2
@@ -1529,10 +1529,18 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
|
||||
ws.on('CB:stream:error', (node: BinaryNode) => {
|
||||
const [reasonNode] = getAllBinaryNodeChildren(node)
|
||||
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out')
|
||||
|
||||
const { reason, statusCode } = getErrorCodeFromStreamError(node)
|
||||
|
||||
if(reason === 'device_removed') {
|
||||
logger.error({ node }, 'stream error: device removed — logging out')
|
||||
} else if(reason === 'xml-not-well-formed') {
|
||||
logger.warn({ node }, 'stream error: sent malformed stanza (xml-not-well-formed)')
|
||||
} else if(reason === 'ack') {
|
||||
logger.warn({ ackId: reasonNode?.attrs?.id, node }, 'stream error: ack-based error')
|
||||
} else {
|
||||
logger.error({ reason, statusCode, node }, 'stream errored out')
|
||||
}
|
||||
|
||||
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }))
|
||||
})
|
||||
// stream fail, possible logout
|
||||
|
||||
Reference in New Issue
Block a user