fix(types): remove non-null assertions in remaining 12 files

Files fixed:
- messages-media.ts: stream/buffer guards, file path validation
- decode-wa-message.ts: message field guards, optional chaining
- version-cache.ts: narrowing after null checks
- jid-utils.ts: split result guards, user fallbacks
- communities.ts: attrs fallbacks with ?? operator
- sticker-pack.ts: response guards
- business.ts: attrs guards
- socket.ts: onClose guard, pairingCode guard, creds.me guard
- event-buffer.ts: null guards
- signal.ts: null guards
- encode.ts (WAM): id null check with continue
- upstream history.ts: conversations/pushnames null guards

All 26 files across the project are now corrected.
Total: ~248 non-null assertions replaced with proper null guards.

https://claude.ai/code/session_01E2cfX1N3sJgCJBTvzGazSG
This commit is contained in:
Claude
2026-02-09 00:32:04 +00:00
parent 7e88ddb858
commit 1c9fb86cc3
12 changed files with 215 additions and 104 deletions
+19 -13
View File
@@ -127,6 +127,10 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const msgId = stanza.attrs.id
const from = stanza.attrs.from
if (!from) {
throw new Boom('Missing from attribute in message', { data: stanza })
}
const participant: string | undefined = stanza.attrs.participant
const recipient: string | undefined = stanza.attrs.recipient
@@ -137,21 +141,21 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) {
if (recipient && !isJidMetaAI(recipient)) {
if (!isMe(from!) && !isMeLid(from!)) {
if (!isMe(from) && !isMeLid(from)) {
throw new Boom('receipient present, but msg not from me', { data: stanza })
}
if (isMe(from!) || isMeLid(from!)) {
if (isMe(from) || isMeLid(from)) {
fromMe = true
}
chatId = recipient
} else {
chatId = from!
chatId = from
}
msgType = 'chat'
author = from!
author = from
} else if (isJidGroup(from)) {
if (!participant) {
throw new Boom('No participant in group message')
@@ -163,28 +167,28 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
msgType = 'group'
author = participant
chatId = from!
chatId = from
} else if (isJidBroadcast(from)) {
if (!participant) {
throw new Boom('No participant in group message')
}
const isParticipantMe = isMe(participant)
if (isJidStatusBroadcast(from!)) {
if (isJidStatusBroadcast(from)) {
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
} else {
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
}
fromMe = isParticipantMe
chatId = from!
chatId = from
author = participant
} else if (isJidNewsletter(from)) {
msgType = 'newsletter'
chatId = from!
author = from!
chatId = from
author = from
if (isMe(from!) || isMeLid(from!)) {
if (isMe(from) || isMeLid(from)) {
fromMe = true
}
} else {
@@ -207,7 +211,7 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
const fullMessage: WAMessage = {
key,
category: stanza.attrs.category,
messageTimestamp: +stanza.attrs.t!,
messageTimestamp: +(stanza.attrs.t ?? 0),
pushName: pushname,
broadcast: isJidBroadcast(from)
}
@@ -241,8 +245,10 @@ export const decryptMessageNode = (
for (const { tag, attrs, content } of stanza.content) {
if (tag === 'verified_name' && content instanceof Uint8Array) {
const cert = proto.VerifiedNameCertificate.decode(content)
const details = proto.VerifiedNameCertificate.Details.decode(cert.details!)
fullMessage.verifiedBizName = details.verifiedName
if (cert.details) {
const details = proto.VerifiedNameCertificate.Details.decode(cert.details)
fullMessage.verifiedBizName = details.verifiedName
}
}
if (tag === 'unavailable' && attrs.type === 'view_once') {
+27 -17
View File
@@ -289,8 +289,9 @@ class AdaptiveTimeoutCalculator {
return // Not enough data
}
const oldest = this.eventTimestamps[0]!
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]!
const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return
const timeSpan = newest - oldest
if (timeSpan === 0) return
@@ -337,8 +338,9 @@ class AdaptiveTimeoutCalculator {
if (this.eventTimestamps.length < 2) {
return 0
}
const oldest = this.eventTimestamps[0]!
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]!
const oldest = this.eventTimestamps[0]
const newest = this.eventTimestamps[this.eventTimestamps.length - 1]
if (oldest === undefined || newest === undefined) return 0
const timeSpan = newest - oldest
if (timeSpan === 0) return 0
return (this.eventTimestamps.length / timeSpan) * 1000
@@ -633,8 +635,11 @@ export const makeEventBuffer = (
for (const update of chatUpdates) {
if (update.conditional) {
conditionalChatUpdatesLeft += 1
newData.chatUpdates[update.id!] = update
delete data.chatUpdates[update.id!]
const updateId = update.id
if (updateId) {
newData.chatUpdates[updateId] = update
delete data.chatUpdates[updateId]
}
}
}
@@ -723,7 +728,7 @@ export const makeEventBuffer = (
const { type } = evData as BaileysEventMap['messages.upsert']
const existingUpserts = Object.values(data.messageUpserts)
if (existingUpserts.length > 0) {
const bufferedType = existingUpserts[0]!.type
const bufferedType = existingUpserts[0]?.type
if (bufferedType !== type) {
logger.debug({ bufferedType, newType: type }, 'messages.upsert type mismatch, emitting buffered messages')
// Emit the buffered messages with their correct type
@@ -973,7 +978,8 @@ function append<E extends BufferableEvent>(
break
case 'chats.update':
for (const update of eventData as ChatUpdate[]) {
const chatId = update.id!
const chatId = update.id
if (!chatId) continue
const conditionMatches = update.conditional ? update.conditional(data) : true
if (conditionMatches) {
delete update.conditional
@@ -1039,8 +1045,9 @@ function append<E extends BufferableEvent>(
data.contactUpserts[contact.id] = upsert
}
if (data.contactUpdates[contact.id]) {
upsert = Object.assign(data.contactUpdates[contact.id]!, trimUndefined(contact)) as Contact
const existingContactUpdate = data.contactUpdates[contact.id]
if (existingContactUpdate) {
upsert = Object.assign(existingContactUpdate, trimUndefined(contact)) as Contact
delete data.contactUpdates[contact.id]
}
}
@@ -1049,7 +1056,8 @@ function append<E extends BufferableEvent>(
case 'contacts.update':
const contactUpdates = eventData as BaileysEventMap['contacts.update']
for (const update of contactUpdates) {
const id = update.id!
const id = update.id
if (!id) continue
// merge into prior upsert
const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
if (upsert) {
@@ -1170,7 +1178,8 @@ function append<E extends BufferableEvent>(
case 'groups.update':
const groupUpdates = eventData as BaileysEventMap['groups.update']
for (const update of groupUpdates) {
const id = update.id!
const id = update.id
if (!id) continue
const groupUpdate = data.groupUpdates[id] || {}
if (!data.groupUpdates[id]) {
data.groupUpdates[id] = Object.assign(groupUpdate, update)
@@ -1202,7 +1211,8 @@ function append<E extends BufferableEvent>(
function decrementChatReadCounterIfMsgDidUnread(message: WAMessage) {
// decrement chat unread counter
// if the message has already been marked read by us
const chatId = message.key.remoteJid!
const chatId = message.key.remoteJid
if (!chatId) return
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
if (
isRealMessage(message) &&
@@ -1255,7 +1265,7 @@ function consolidateEvents(data: BufferedEventData) {
const messageUpsertList = Object.values(data.messageUpserts)
if (messageUpsertList.length) {
const type = messageUpsertList[0]!.type
const type = messageUpsertList[0]?.type
map['messages.upsert'] = {
messages: messageUpsertList.map(m => m.message),
type
@@ -1311,7 +1321,7 @@ function consolidateEvents(data: BufferedEventData) {
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if (
b.unreadCount === null && // neutralize unread counter
a.unreadCount! < 0
(a.unreadCount ?? 0) < 0
) {
a.unreadCount = undefined
b.unreadCount = undefined
@@ -1319,8 +1329,8 @@ function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if (typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') {
b = { ...b }
if (b.unreadCount! >= 0) {
b.unreadCount = Math.max(b.unreadCount!, 0) + Math.max(a.unreadCount, 0)
if ((b.unreadCount ?? 0) >= 0) {
b.unreadCount = Math.max(b.unreadCount ?? 0, 0) + Math.max(a.unreadCount, 0)
}
}
+28 -13
View File
@@ -317,7 +317,7 @@ export const getStream = async (item: WAMediaUpload, opts?: RequestInit & { maxC
const urlStr = item.url.toString()
if (urlStr.startsWith('data:')) {
const buffer = Buffer.from(urlStr.split(',')[1]!, 'base64')
const buffer = Buffer.from(urlStr.split(',')[1] ?? '', 'base64')
return { stream: toReadable(buffer), type: 'buffer' } as const
}
@@ -414,7 +414,11 @@ export const encryptedStream = async (
let fileLength = 0
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
const hmac = Crypto.createHmac('sha256', macKey!).update(iv)
if (!macKey) {
throw new Boom('Failed to derive media mac key')
}
const hmac = Crypto.createHmac('sha256', macKey).update(iv)
const sha256Plain = Crypto.createHash('sha256')
const sha256Enc = Crypto.createHash('sha256')
@@ -528,7 +532,7 @@ export const downloadContentFromMessage = async (
opts: MediaDownloadOptions = {}
) => {
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath!)
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath ?? '')
if (!downloadUrl) {
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
}
@@ -591,8 +595,8 @@ export const downloadEncryptedContent = async (
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
if (startByte || endByte) {
const start = bytesFetched >= startByte! ? undefined : Math.max(startByte! - bytesFetched, 0)
const end = bytesFetched + bytes.length < endByte! ? undefined : Math.max(endByte! - bytesFetched, 0)
const start = bytesFetched >= (startByte ?? 0) ? undefined : Math.max((startByte ?? 0) - bytesFetched, 0)
const end = bytesFetched + bytes.length < (endByte ?? 0) ? undefined : Math.max((endByte ?? 0) - bytesFetched, 0)
push(bytes.slice(start, end))
@@ -652,7 +656,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
extension = '.jpeg'
} else {
const messageContent = message[type] as WAGenericMediaMessage
extension = getExtension(messageContent.mimetype!)!
extension = getExtension(messageContent.mimetype ?? '') ?? ''
}
return extension
@@ -860,8 +864,8 @@ export const getWAUploadToServer = (
if (result?.url || result?.direct_path) {
urls = {
mediaUrl: result.url!,
directPath: result.direct_path!,
mediaUrl: result.url ?? '',
directPath: result.direct_path ?? '',
meta_hmac: result.meta_hmac,
fbid: result.fbid,
ts: result.ts
@@ -896,17 +900,25 @@ const getMediaRetryKey = (mediaKey: Buffer | Uint8Array) => {
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
*/
export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
if (!key.id) {
throw new Boom('Missing message ID for media retry request')
}
if (!key.remoteJid) {
throw new Boom('Missing remote JID for media retry request')
}
const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
const iv = Crypto.randomBytes(12)
const retryKey = getMediaRetryKey(mediaKey)
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id!))
const ciphertext = aesEncryptGCM(recpBuffer, retryKey, iv, Buffer.from(key.id))
const req: BinaryNode = {
tag: 'receipt',
attrs: {
id: key.id!,
id: key.id,
to: jidNormalizedUser(meId),
type: 'server-error'
},
@@ -925,7 +937,7 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
{
tag: 'rmr',
attrs: {
jid: key.remoteJid!,
jid: key.remoteJid,
from_me: (!!key.fromMe).toString(),
// @ts-ignore
participant: key.participant || undefined
@@ -938,7 +950,10 @@ export const encryptMediaRetryRequest = (key: WAMessageKey, mediaKey: Buffer | U
}
export const decodeMediaRetryNode = (node: BinaryNode) => {
const rmrNode = getBinaryNodeChild(node, 'rmr')!
const rmrNode = getBinaryNodeChild(node, 'rmr')
if (!rmrNode) {
throw new Boom('Missing rmr node in media retry response')
}
const event: BaileysEventMap['messages.media-update'][number] = {
key: {
@@ -951,7 +966,7 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
const errorNode = getBinaryNodeChild(node, 'error')
if (errorNode) {
const errorCode = +errorNode.attrs.code!
const errorCode = +(errorNode.attrs.code ?? '0')
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
data: errorNode.attrs,
statusCode: getStatusCodeForMediaRetry(errorCode)
+32 -18
View File
@@ -88,14 +88,18 @@ export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ({
})
export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepositoryWithLIDStore) => {
const extractKey = (key: BinaryNode) =>
key
? {
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
signature: getBinaryNodeChildBuffer(key, 'signature')!
}
: undefined
const extractKey = (key: BinaryNode) => {
if (!key) return undefined
const keyId = getBinaryNodeChildUInt(key, 'id', 3)
const publicKeyBuf = getBinaryNodeChildBuffer(key, 'value')
const signature = getBinaryNodeChildBuffer(key, 'signature')
if (keyId === undefined || !publicKeyBuf || !signature) return undefined
return {
keyId,
publicKey: generateSignalPubKey(publicKeyBuf),
signature
}
}
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
for (const node of nodes) {
assertNodeErrorFree(node)
@@ -111,20 +115,26 @@ export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: Si
for (const nodesChunk of chunks) {
for (const node of nodesChunk) {
const signedKey = getBinaryNodeChild(node, 'skey')!
const key = getBinaryNodeChild(node, 'key')!
const identity = getBinaryNodeChildBuffer(node, 'identity')!
const jid = node.attrs.jid!
const signedKey = getBinaryNodeChild(node, 'skey')
const key = getBinaryNodeChild(node, 'key')
const identity = getBinaryNodeChildBuffer(node, 'identity')
const jid = node.attrs.jid
if (!signedKey || !key || !identity || !jid) continue
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
if (registrationId === undefined) continue
const signedPreKey = extractKey(signedKey)
const preKey = extractKey(key)
if (!signedPreKey || !preKey) continue
await repository.injectE2ESession({
jid,
session: {
registrationId: registrationId!,
registrationId,
identityKey: generateSignalPubKey(identity),
signedPreKey: extractKey(signedKey)!,
preKey: extractKey(key)!
signedPreKey,
preKey
}
})
}
@@ -137,14 +147,18 @@ export const extractDeviceJids = (
myLid: string,
excludeZeroDevices: boolean
) => {
const { user: myUser, device: myDevice } = jidDecode(myJid)!
const myJidDecoded = jidDecode(myJid)
if (!myJidDecoded) return []
const { user: myUser, device: myDevice } = myJidDecoded
const extracted: FullJid[] = []
for (const userResult of result) {
const { devices, id } = userResult as { devices: ParsedDeviceInfo; id: string }
const decoded = jidDecode(id)!,
{ user, server } = decoded
const decoded = jidDecode(id)
if (!decoded) continue
const { user, server } = decoded
let { domainType } = decoded
const deviceList = devices?.deviceList as DeviceListData[]
if (!Array.isArray(deviceList)) continue
+1 -1
View File
@@ -575,7 +575,7 @@ export const prepareStickerPackMessage = async (
duplicateCount++
// Merge emojis (deduplicate)
const mergedEmojis = Array.from(new Set([...existingMetadata.emojis!, ...emojis]))
const mergedEmojis = Array.from(new Set([...(existingMetadata.emojis ?? []), ...emojis]))
existingMetadata.emojis = mergedEmojis
// Merge accessibility labels (concatenate with separator if both exist)
+6 -6
View File
@@ -165,19 +165,19 @@ export async function getCachedVersion(
} = config
// 1. Check memory cache first (fastest)
if (isCacheValid(memoryCache, cacheTtlMs)) {
const age = Date.now() - memoryCache!.fetchedAt
if (isCacheValid(memoryCache, cacheTtlMs) && memoryCache) {
const age = Date.now() - memoryCache.fetchedAt
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using memory cached version')
return { version: memoryCache!.version, fromCache: true, age, source: 'memory' }
return { version: memoryCache.version, fromCache: true, age, source: 'memory' }
}
// 2. Check file cache (survives restarts)
const fileCache = await readCacheFile(cacheFilePath)
if (isCacheValid(fileCache, cacheTtlMs)) {
if (isCacheValid(fileCache, cacheTtlMs) && fileCache) {
memoryCache = fileCache // Update memory cache
const age = Date.now() - fileCache!.fetchedAt
const age = Date.now() - fileCache.fetchedAt
logger?.debug({ age: Math.round(age / 1000) + 's' }, 'Using file cached version')
return { version: fileCache!.version, fromCache: true, age, source: 'file' }
return { version: fileCache.version, fromCache: true, age, source: 'file' }
}
// 3. Need to fetch - but deduplicate concurrent requests