From 5ebc66d5a8c56a695c7cf6580db3c4ce6de04cb8 Mon Sep 17 00:00:00 2001 From: Alpes Digital Date: Sat, 20 Jun 2026 15:12:40 -0300 Subject: [PATCH 01/16] feat(group): member-add-mode, join-approval-mode, participant requests + message forward MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes Baileys group/message capabilities that already exist in the underlying library but had no REST endpoints: - POST /group/memberAddMode -> groupMemberAddMode (admin_add | all_member_add) - POST /group/joinApprovalMode -> groupJoinApprovalMode (on | off) - GET /group/participantRequests -> groupRequestParticipantsList (pending join requests) - POST /group/updateParticipantRequests -> groupRequestParticipantsUpdate (approve | reject) - POST /message/forwardMessage -> sendMessage(jid, { forward }) (keeps isForwarded) Each follows the existing pattern (DTO + JSONSchema + router + controller + baileys service). No new dependencies — baileys 7.0.0-rc.9 already ships these. Co-Authored-By: Claude Opus 4.8 --- src/api/controllers/group.controller.ts | 19 +++++++ src/api/controllers/sendMessage.controller.ts | 5 ++ src/api/dto/group.dto.ts | 13 +++++ src/api/dto/sendMessage.dto.ts | 4 ++ .../whatsapp/whatsapp.baileys.service.ts | 55 +++++++++++++++++++ src/api/routes/group.router.ts | 46 ++++++++++++++++ src/api/routes/sendMessage.router.ts | 12 ++++ src/validate/group.schema.ts | 39 +++++++++++++ src/validate/message.schema.ts | 11 ++++ 9 files changed, 204 insertions(+) diff --git a/src/api/controllers/group.controller.ts b/src/api/controllers/group.controller.ts index ebe7c03671..f2e9a2eb8e 100644 --- a/src/api/controllers/group.controller.ts +++ b/src/api/controllers/group.controller.ts @@ -5,11 +5,14 @@ import { GroupDescriptionDto, GroupInvite, GroupJid, + GroupJoinApprovalModeDto, + GroupMemberAddModeDto, GroupPictureDto, GroupSendInvite, GroupSubjectDto, GroupToggleEphemeralDto, GroupUpdateParticipantDto, + GroupUpdateParticipantRequestDto, GroupUpdateSettingDto, } from '@api/dto/group.dto'; import { InstanceDto } from '@api/dto/instance.dto'; @@ -78,6 +81,22 @@ export class GroupController { return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update); } + public async updateMemberAddMode(instance: InstanceDto, update: GroupMemberAddModeDto) { + return await this.waMonitor.waInstances[instance.instanceName].updateMemberAddMode(update); + } + + public async updateJoinApprovalMode(instance: InstanceDto, update: GroupJoinApprovalModeDto) { + return await this.waMonitor.waInstances[instance.instanceName].updateJoinApprovalMode(update); + } + + public async findParticipantRequests(instance: InstanceDto, groupJid: GroupJid) { + return await this.waMonitor.waInstances[instance.instanceName].findParticipantRequests(groupJid); + } + + public async updateParticipantRequests(instance: InstanceDto, update: GroupUpdateParticipantRequestDto) { + return await this.waMonitor.waInstances[instance.instanceName].updateParticipantRequests(update); + } + public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); } diff --git a/src/api/controllers/sendMessage.controller.ts b/src/api/controllers/sendMessage.controller.ts index 64aa1c8468..68c55b0a73 100644 --- a/src/api/controllers/sendMessage.controller.ts +++ b/src/api/controllers/sendMessage.controller.ts @@ -1,5 +1,6 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { + ForwardMessageDto, SendAudioDto, SendButtonsDto, SendContactDto, @@ -39,6 +40,10 @@ export class SendMessageController { return await this.waMonitor.waInstances[instanceName].textMessage(data); } + public async forwardMessage({ instanceName }: InstanceDto, data: ForwardMessageDto) { + return await this.waMonitor.waInstances[instanceName].forwardMessage(data); + } + public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto, file?: any) { if (isBase64(data?.media) && !data?.fileName && data?.mediatype === 'document') { throw new BadRequestException('For base64 the file name must be informed.'); diff --git a/src/api/dto/group.dto.ts b/src/api/dto/group.dto.ts index 293329d2ed..b9975d3f12 100644 --- a/src/api/dto/group.dto.ts +++ b/src/api/dto/group.dto.ts @@ -54,3 +54,16 @@ export class GroupUpdateSettingDto extends GroupJid { export class GroupToggleEphemeralDto extends GroupJid { expiration: 0 | 86400 | 604800 | 7776000; } + +export class GroupMemberAddModeDto extends GroupJid { + mode: 'admin_add' | 'all_member_add'; +} + +export class GroupJoinApprovalModeDto extends GroupJid { + mode: 'on' | 'off'; +} + +export class GroupUpdateParticipantRequestDto extends GroupJid { + action: 'approve' | 'reject'; + participants: string[]; +} diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index ba9ecf527c..1ba725dfbb 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -50,6 +50,10 @@ export class Metadata { export class SendTextDto extends Metadata { text: string; } +export class ForwardMessageDto { + number: string; + messageId: string; +} export class SendPresence extends Metadata { text: string; } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc1..e717544420 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -22,11 +22,14 @@ import { GroupDescriptionDto, GroupInvite, GroupJid, + GroupJoinApprovalModeDto, + GroupMemberAddModeDto, GroupPictureDto, GroupSendInvite, GroupSubjectDto, GroupToggleEphemeralDto, GroupUpdateParticipantDto, + GroupUpdateParticipantRequestDto, GroupUpdateSettingDto, } from '@api/dto/group.dto'; import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto'; @@ -34,6 +37,7 @@ import { HandleLabelDto, LabelDto } from '@api/dto/label.dto'; import { Button, ContactMessage, + ForwardMessageDto, KeyType, MediaMessage, Options, @@ -553,6 +557,20 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async forwardMessage(data: ForwardMessageDto) { + try { + const fullMsg = (await this.getMessage({ id: data.messageId }, true)) as unknown as WAMessage; + if (!fullMsg?.message) { + throw new BadRequestException('Message not found'); + } + const number = data.number.replace(/\D/g, ''); + const jid = data.number.includes('@') ? data.number : `${number}@s.whatsapp.net`; + return await this.client.sendMessage(jid, { forward: fullMsg }); + } catch (error) { + throw new BadRequestException('Error forwarding message', error.toString()); + } + } + private async defineAuthState() { const db = this.configService.get('DATABASE'); const cache = this.configService.get('CACHE'); @@ -4597,6 +4615,43 @@ export class BaileysStartupService extends ChannelStartupService { } } + public async updateMemberAddMode(update: GroupMemberAddModeDto) { + try { + await this.client.groupMemberAddMode(update.groupJid, update.mode); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating member add mode', error.toString()); + } + } + + public async updateJoinApprovalMode(update: GroupJoinApprovalModeDto) { + try { + await this.client.groupJoinApprovalMode(update.groupJid, update.mode); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating join approval mode', error.toString()); + } + } + + public async findParticipantRequests(id: GroupJid) { + try { + const requests = await this.client.groupRequestParticipantsList(id.groupJid); + return { requests: requests || [] }; + } catch (error) { + throw new BadRequestException('Error fetching participant requests', error.toString()); + } + } + + public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) { + try { + const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`)); + const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action); + return { updateParticipantRequests: result }; + } catch (error) { + throw new BadRequestException('Error updating participant requests', error.toString()); + } + } + public async leaveGroup(id: GroupJid) { try { await this.client.groupLeave(id.groupJid); diff --git a/src/api/routes/group.router.ts b/src/api/routes/group.router.ts index 7086b11769..c16d951d18 100644 --- a/src/api/routes/group.router.ts +++ b/src/api/routes/group.router.ts @@ -6,11 +6,14 @@ import { GroupDescriptionDto, GroupInvite, GroupJid, + GroupJoinApprovalModeDto, + GroupMemberAddModeDto, GroupPictureDto, GroupSendInvite, GroupSubjectDto, GroupToggleEphemeralDto, GroupUpdateParticipantDto, + GroupUpdateParticipantRequestDto, GroupUpdateSettingDto, } from '@api/dto/group.dto'; import { groupController } from '@api/server.module'; @@ -21,10 +24,13 @@ import { groupInviteSchema, groupJidSchema, groupSendInviteSchema, + joinApprovalModeSchema, + memberAddModeSchema, toggleEphemeralSchema, updateGroupDescriptionSchema, updateGroupPictureSchema, updateGroupSubjectSchema, + updateParticipantRequestSchema, updateParticipantsSchema, updateSettingsSchema, } from '@validate/validate.schema'; @@ -186,6 +192,46 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('memberAddMode'), ...guards, async (req, res) => { + const response = await this.groupValidate({ + request: req, + schema: memberAddModeSchema, + ClassRef: GroupMemberAddModeDto, + execute: (instance, data) => groupController.updateMemberAddMode(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('joinApprovalMode'), ...guards, async (req, res) => { + const response = await this.groupValidate({ + request: req, + schema: joinApprovalModeSchema, + ClassRef: GroupJoinApprovalModeDto, + execute: (instance, data) => groupController.updateJoinApprovalMode(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('participantRequests'), ...guards, async (req, res) => { + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.findParticipantRequests(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('updateParticipantRequests'), ...guards, async (req, res) => { + const response = await this.groupValidate({ + request: req, + schema: updateParticipantRequestSchema, + ClassRef: GroupUpdateParticipantRequestDto, + execute: (instance, data) => groupController.updateParticipantRequests(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { const response = await this.groupValidate({ request: req, diff --git a/src/api/routes/sendMessage.router.ts b/src/api/routes/sendMessage.router.ts index cd073dba3d..dee71cbf08 100644 --- a/src/api/routes/sendMessage.router.ts +++ b/src/api/routes/sendMessage.router.ts @@ -1,5 +1,6 @@ import { RouterBroker } from '@api/abstract/abstract.router'; import { + ForwardMessageDto, SendAudioDto, SendButtonsDto, SendContactDto, @@ -19,6 +20,7 @@ import { audioMessageSchema, buttonsMessageSchema, contactMessageSchema, + forwardMessageSchema, listMessageSchema, locationMessageSchema, mediaMessageSchema, @@ -61,6 +63,16 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('forwardMessage'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: forwardMessageSchema, + ClassRef: ForwardMessageDto, + execute: (instance, data) => sendMessageController.forwardMessage(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) .post(this.routerPath('sendMedia'), ...guards, upload.single('file'), async (req, res) => { const bodyData = req.body; diff --git a/src/validate/group.schema.ts b/src/validate/group.schema.ts index 8da188d87f..75db868953 100644 --- a/src/validate/group.schema.ts +++ b/src/validate/group.schema.ts @@ -159,6 +159,45 @@ export const toggleEphemeralSchema: JSONSchema7 = { ...isNotEmpty('groupJid', 'expiration'), }; +export const memberAddModeSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + mode: { type: 'string', enum: ['admin_add', 'all_member_add'] }, + }, + required: ['groupJid', 'mode'], + ...isNotEmpty('groupJid', 'mode'), +}; + +export const joinApprovalModeSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + mode: { type: 'string', enum: ['on', 'off'] }, + }, + required: ['groupJid', 'mode'], + ...isNotEmpty('groupJid', 'mode'), +}; + +export const updateParticipantRequestSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { type: 'string', enum: ['approve', 'reject'] }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'string' }, + }, + }, + required: ['groupJid', 'action', 'participants'], + ...isNotEmpty('groupJid', 'action'), +}; + export const updateGroupPictureSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index d514c6199e..fcb138e4d0 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -65,6 +65,17 @@ export const offerCallSchema: JSONSchema7 = { required: ['number', 'callDuration'], }; +export const forwardMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + messageId: { type: 'string' }, + }, + required: ['number', 'messageId'], + ...isNotEmpty('number', 'messageId'), +}; + export const textMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', From 3efa13ce549e3f356493f361ee38213a2ef4b19b Mon Sep 17 00:00:00 2001 From: root Date: Mon, 6 Jul 2026 23:40:17 +0000 Subject: [PATCH 02/16] fix(groups): throttle groupMetadata (evita 429 em contas com centenas de grupos) + baileys rc13 --- package-lock.json | 768 +++++++----------- package.json | 4 +- .../whatsapp/whatsapp.baileys.service.ts | 37 +- 3 files changed, 311 insertions(+), 498 deletions(-) diff --git a/package-lock.json b/package-lock.json index c45e8fef38..8eda484379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", + "baileys": "^7.0.0-rc13", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -829,9 +829,9 @@ } }, "node_modules/@borewit/text-codec": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.0.tgz", - "integrity": "sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", "license": "MIT", "funding": { "type": "github", @@ -2372,17 +2372,17 @@ } }, "node_modules/@jimp/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.0.tgz", - "integrity": "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", "license": "MIT", "dependencies": { - "@jimp/file-ops": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", - "file-type": "^16.0.0", + "file-type": "^21.3.3", "mime": "3" }, "engines": { @@ -2402,14 +2402,14 @@ } }, "node_modules/@jimp/diff": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.0.tgz", - "integrity": "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", "license": "MIT", "dependencies": { - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" }, "engines": { @@ -2417,23 +2417,23 @@ } }, "node_modules/@jimp/file-ops": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.0.tgz", - "integrity": "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@jimp/js-bmp": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.0.tgz", - "integrity": "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "bmp-ts": "^1.0.9" }, "engines": { @@ -2441,13 +2441,13 @@ } }, "node_modules/@jimp/js-gif": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.0.tgz", - "integrity": "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "gifwrap": "^0.10.1", "omggif": "^1.0.10" }, @@ -2456,13 +2456,13 @@ } }, "node_modules/@jimp/js-jpeg": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.0.tgz", - "integrity": "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "jpeg-js": "^0.4.4" }, "engines": { @@ -2470,13 +2470,13 @@ } }, "node_modules/@jimp/js-png": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.0.tgz", - "integrity": "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "pngjs": "^7.0.0" }, "engines": { @@ -2484,13 +2484,13 @@ } }, "node_modules/@jimp/js-tiff": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.0.tgz", - "integrity": "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "utif2": "^4.1.0" }, "engines": { @@ -2498,13 +2498,13 @@ } }, "node_modules/@jimp/plugin-blit": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.0.tgz", - "integrity": "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2512,25 +2512,25 @@ } }, "node_modules/@jimp/plugin-blur": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.0.tgz", - "integrity": "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/utils": "1.6.0" + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" }, "engines": { "node": ">=18" } }, "node_modules/@jimp/plugin-circle": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.0.tgz", - "integrity": "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2538,14 +2538,14 @@ } }, "node_modules/@jimp/plugin-color": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.0.tgz", - "integrity": "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "tinycolor2": "^1.6.0", "zod": "^3.23.8" }, @@ -2554,16 +2554,16 @@ } }, "node_modules/@jimp/plugin-contain": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.0.tgz", - "integrity": "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2571,15 +2571,15 @@ } }, "node_modules/@jimp/plugin-cover": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.0.tgz", - "integrity": "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2587,14 +2587,14 @@ } }, "node_modules/@jimp/plugin-crop": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.0.tgz", - "integrity": "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2602,13 +2602,13 @@ } }, "node_modules/@jimp/plugin-displace": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.0.tgz", - "integrity": "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2616,25 +2616,25 @@ } }, "node_modules/@jimp/plugin-dither": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.0.tgz", - "integrity": "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0" + "@jimp/types": "1.6.1" }, "engines": { "node": ">=18" } }, "node_modules/@jimp/plugin-fisheye": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.0.tgz", - "integrity": "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2642,12 +2642,12 @@ } }, "node_modules/@jimp/plugin-flip": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.0.tgz", - "integrity": "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2655,20 +2655,20 @@ } }, "node_modules/@jimp/plugin-hash": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.0.tgz", - "integrity": "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==", - "license": "MIT", - "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/js-bmp": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/js-tiff": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "any-base": "^1.1.0" }, "engines": { @@ -2676,12 +2676,12 @@ } }, "node_modules/@jimp/plugin-mask": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.0.tgz", - "integrity": "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2689,16 +2689,16 @@ } }, "node_modules/@jimp/plugin-print": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.0.tgz", - "integrity": "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", @@ -2710,9 +2710,9 @@ } }, "node_modules/@jimp/plugin-quantize": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.0.tgz", - "integrity": "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", "license": "MIT", "dependencies": { "image-q": "^4.0.0", @@ -2723,13 +2723,13 @@ } }, "node_modules/@jimp/plugin-resize": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz", - "integrity": "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2737,16 +2737,16 @@ } }, "node_modules/@jimp/plugin-rotate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.0.tgz", - "integrity": "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2754,16 +2754,16 @@ } }, "node_modules/@jimp/plugin-threshold": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.0.tgz", - "integrity": "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-hash": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -2771,9 +2771,9 @@ } }, "node_modules/@jimp/types": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.0.tgz", - "integrity": "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", "license": "MIT", "dependencies": { "zod": "^3.23.8" @@ -2783,12 +2783,12 @@ } }, "node_modules/@jimp/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "tinycolor2": "^1.6.0" }, "engines": { @@ -3578,25 +3578,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -3605,12 +3604,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -3624,9 +3617,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@redis/bloom": { @@ -4758,34 +4751,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@tokenizer/inflate/node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/token-types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", - "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.1.0", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -4895,12 +4860,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, "node_modules/@types/mime": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-4.0.0.tgz", @@ -5814,21 +5773,22 @@ } }, "node_modules/baileys": { - "version": "7.0.0-rc.9", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc.9.tgz", - "integrity": "sha512-Txd2dZ9MHbojvsHckeuCnAKPO/bQjKxua/0tQSJwOKXffK5vpS82k4eA/Nb46K0cK0Bx+fyY0zhnQHYMBriQcw==", + "version": "7.0.0-rc13", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", + "integrity": "sha512-v8k74K8B5R7WNYGa26MyJAYEu3Wc4BSuK01QaK8lr30lhE8Nga31nWNu8KN0NDDt+Fsvkq4SQFFI8Q13ghjKmA==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@cacheable/node-cache": "^1.4.0", "@hapi/boom": "^9.1.3", "async-mutex": "^0.5.0", - "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", + "libsignal": "^6.0.0", "lru-cache": "^11.1.0", - "music-metadata": "^11.7.0", + "music-metadata": "^11.12.3", "p-queue": "^9.0.0", "pino": "^9.6", - "protobufjs": "^7.2.4", + "protobufjs": "^7.5.6", + "whatsapp-rust-bridge": "0.5.4", "ws": "^8.13.0" }, "engines": { @@ -5836,7 +5796,7 @@ }, "peerDependencies": { "audio-decode": "^2.1.3", - "jimp": "^1.6.0", + "jimp": "^1.6.1", "link-preview-js": "^3.0.0", "sharp": "*" }, @@ -5877,6 +5837,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -8336,15 +8297,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", @@ -8629,17 +8581,18 @@ } }, "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "license": "MIT", "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -10351,38 +10304,38 @@ "license": "ISC" }, "node_modules/jimp": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", - "integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==", - "license": "MIT", - "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/diff": "1.6.0", - "@jimp/js-bmp": "1.6.0", - "@jimp/js-gif": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/js-tiff": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/plugin-blur": "1.6.0", - "@jimp/plugin-circle": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-contain": "1.6.0", - "@jimp/plugin-cover": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-displace": "1.6.0", - "@jimp/plugin-dither": "1.6.0", - "@jimp/plugin-fisheye": "1.6.0", - "@jimp/plugin-flip": "1.6.0", - "@jimp/plugin-hash": "1.6.0", - "@jimp/plugin-mask": "1.6.0", - "@jimp/plugin-print": "1.6.0", - "@jimp/plugin-quantize": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/plugin-rotate": "1.6.0", - "@jimp/plugin-threshold": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" }, "engines": { "node": ">=18" @@ -10610,51 +10563,13 @@ "license": "MIT" }, "node_modules/libsignal": { - "name": "@whiskeysockets/libsignal-node", - "version": "2.0.1", - "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", + "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", "license": "GPL-3.0", "dependencies": { "curve25519-js": "^0.0.4", - "protobufjs": "6.8.8" - } - }, - "node_modules/libsignal/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "license": "MIT" - }, - "node_modules/libsignal/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "license": "Apache-2.0" - }, - "node_modules/libsignal/node_modules/protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" + "protobufjs": "^7.5.5" } }, "node_modules/lilconfig": { @@ -11232,12 +11147,16 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-2.0.0.tgz", + "integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mediainfo.js": { @@ -11656,9 +11575,9 @@ } }, "node_modules/music-metadata": { - "version": "11.10.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.10.3.tgz", - "integrity": "sha512-j0g/x4cNNZW6I5gdcPAY+GFkJY9WHTpkFDMBJKQLxJQyvSfQbXm57fTE3haGFFuOzCgtsTd4Plwc49Sn9RacDQ==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.13.0.tgz", + "integrity": "sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==", "funding": [ { "type": "github", @@ -11671,80 +11590,32 @@ ], "license": "MIT", "dependencies": { - "@borewit/text-codec": "^0.2.0", + "@borewit/text-codec": "^0.2.2", "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "file-type": "^21.1.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.5.0" + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" }, "engines": { "node": ">=18" } }, - "node_modules/music-metadata/node_modules/file-type": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.1.tgz", - "integrity": "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/music-metadata/node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "node_modules/music-metadata/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, "engines": { "node": ">=18" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", - "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.1.0", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/music-metadata/node_modules/token-types/node_modules/@borewit/text-codec": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", - "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mute-stream": { @@ -12576,19 +12447,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -12957,15 +12815,6 @@ } } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -12983,24 +12832,23 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -13411,62 +13259,6 @@ "node": ">= 6" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", - "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -14176,9 +13968,9 @@ "license": "ISC" }, "node_modules/simple-xml-to-json": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz", - "integrity": "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", "license": "MIT", "engines": { "node": ">=20.12.2" @@ -14640,16 +14432,15 @@ "license": "MIT" }, "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" + "@tokenizer/token": "^0.3.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "type": "github", @@ -14914,16 +14705,17 @@ } }, "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", "license": "MIT", "dependencies": { + "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "type": "github", @@ -15944,6 +15736,12 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatsapp-rust-bridge": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.4.tgz", + "integrity": "sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -16064,6 +15862,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 56e32fcc8a..7fb7f03b51 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "7.0.0-rc.9", + "baileys": "^7.0.0-rc13", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -87,10 +87,10 @@ "eventemitter2": "^6.4.9", "express": "^4.21.2", "express-async-errors": "^3.1.1", + "fetch-socks": "^1.3.2", "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.1", "https-proxy-agent": "^7.0.6", - "fetch-socks": "^1.3.2", "i18next": "^23.7.19", "jimp": "^1.6.0", "json-schema": "^0.4.0", diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index e717544420..be97c90a01 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -228,6 +228,12 @@ async function getVideoDuration(input: Buffer | string | Readable): Promise = Promise.resolve(); +let __groupMetaLastAt = 0; +const __GROUP_META_MIN_INTERVAL_MS = 2000; + export class BaileysStartupService extends ChannelStartupService { private messageProcessor = new BaileysMessageProcessor(); @@ -4303,21 +4309,24 @@ export class BaileysStartupService extends ChannelStartupService { // Group private async updateGroupMetadataCache(groupJid: string) { - try { - const meta = await this.client.groupMetadata(groupJid); - - const cacheConf = this.configService.get('CACHE'); - - if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { - this.logger.verbose(`Updating cache for group: ${groupJid}`); - await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta }); + const task = __groupMetaChain.then(async () => { + const wait = __GROUP_META_MIN_INTERVAL_MS - (Date.now() - __groupMetaLastAt); + if (wait > 0) await new Promise((r) => setTimeout(r, wait)); + __groupMetaLastAt = Date.now(); + try { + const meta = await this.client.groupMetadata(groupJid); + const cacheConf = this.configService.get('CACHE'); + if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) { + await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta }); + } + return meta; + } catch (error) { + this.logger.error(error); + return null; } - - return meta; - } catch (error) { - this.logger.error(error); - return null; - } + }); + __groupMetaChain = task.then(() => undefined, () => undefined); + return task; } private getGroupMetadataCache = async (groupJid: string) => { From 6d3ed8a9c622e6863c74f8a767c201779c5bbb1d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 6 Jul 2026 23:58:24 +0000 Subject: [PATCH 03/16] =?UTF-8?q?feat(groups):=20modo=20lite=20no=20fetchA?= =?UTF-8?q?llGroups=20(getParticipants=3Dfalse=20pula=20profilePicture=20?= =?UTF-8?q?=E2=80=94=20listagem=20sem=20rajada)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index be97c90a01..7f37a4ed9b 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4477,7 +4477,8 @@ export class BaileysStartupService extends ChannelStartupService { let groups = []; for (const group of fetch) { - const picture = await this.profilePicture(group.id); + const wantDetails = getParticipants.getParticipants == 'true'; + const picture = wantDetails ? await this.profilePicture(group.id) : null; const result = { id: group.id, From d771f48c307e386263d05f91a424adcd30307817 Mon Sep 17 00:00:00 2001 From: alpesdigital-adm Date: Wed, 9 Sep 2026 09:21:45 -0300 Subject: [PATCH 04/16] fix(groups): expose observed group metadata settings (#1) Co-authored-by: root --- .../whatsapp/whatsapp.baileys.service.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 7f37a4ed9b..010e5deb8d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4459,6 +4459,15 @@ export class BaileysStartupService extends ChannelStartupService { descId: group.descId, restrict: group.restrict, announce: group.announce, + joinApprovalMode: group.joinApprovalMode, + memberAddMode: group.memberAddMode, + // In complete Baileys metadata, an absent ephemeral node means disabled. + ephemeralDuration: + typeof group.joinApprovalMode === 'boolean' && typeof group.memberAddMode === 'boolean' + ? group.ephemeralDuration === undefined + ? 0 + : group.ephemeralDuration + : group.ephemeralDuration, participants: group.participants, isCommunity: group.isCommunity, isCommunityAnnounce: group.isCommunityAnnounce, @@ -4493,6 +4502,15 @@ export class BaileysStartupService extends ChannelStartupService { descId: group.descId, restrict: group.restrict, announce: group.announce, + joinApprovalMode: group.joinApprovalMode, + memberAddMode: group.memberAddMode, + // In complete Baileys metadata, an absent ephemeral node means disabled. + ephemeralDuration: + typeof group.joinApprovalMode === 'boolean' && typeof group.memberAddMode === 'boolean' + ? group.ephemeralDuration === undefined + ? 0 + : group.ephemeralDuration + : group.ephemeralDuration, isCommunity: group.isCommunity, isCommunityAnnounce: group.isCommunityAnnounce, linkedParent: group.linkedParent, From ab7854382b201b9a3f91a69f19c274b2b22544ef Mon Sep 17 00:00:00 2001 From: Alpes Digital Date: Mon, 28 Sep 2026 14:58:55 -0300 Subject: [PATCH 05/16] feat(chat): synchronize linked-device read state --- .github/workflows/release-read-state.yml | 72 +++++++++++ src/api/dto/chat.dto.ts | 1 + .../channel/whatsapp/read-state.ts | 74 +++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 25 ++-- src/validate/chat.schema.ts | 20 ++- test/read-state.test.ts | 117 ++++++++++++++++++ 6 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release-read-state.yml create mode 100644 src/api/integrations/channel/whatsapp/read-state.ts create mode 100644 test/read-state.test.ts diff --git a/.github/workflows/release-read-state.yml b/.github/workflows/release-read-state.yml new file mode 100644 index 0000000000..97bb2986a3 --- /dev/null +++ b/.github/workflows/release-read-state.yml @@ -0,0 +1,72 @@ +name: Read-state release candidate + +on: + push: + branches: + - codex/read-state-sync + - feat/group-admin-controls-and-forward + workflow_dispatch: + +permissions: + contents: read + +jobs: + release-image: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: '24' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npm run db:generate + env: + DATABASE_PROVIDER: postgresql + + - name: Test read-state contract + run: node --import tsx --test test/read-state.test.ts + + - name: Check code quality + run: npm run lint:check + + - name: Build application + run: npm run build + + - name: Set image names + id: image + shell: bash + run: | + echo "tag=evolution-api-fork:release-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + echo "archive=evolution-api-fork-release-${GITHUB_SHA}.tar.gz" >> "$GITHUB_OUTPUT" + + - name: Build Docker candidate + run: docker build --tag "${{ steps.image.outputs.tag }}" . + + - name: Export image and checksum + shell: bash + run: | + set -euo pipefail + docker save "${{ steps.image.outputs.tag }}" | gzip -1 > "${{ steps.image.outputs.archive }}" + sha256sum "${{ steps.image.outputs.archive }}" > "${{ steps.image.outputs.archive }}.sha256" + + - name: Upload release candidate + uses: actions/upload-artifact@v4 + with: + name: release-image-${{ github.sha }} + path: | + ${{ steps.image.outputs.archive }} + ${{ steps.image.outputs.archive }}.sha256 + if-no-files-found: error + retention-days: 1 + compression-level: 0 diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index b11f32b054..0c12071956 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -68,6 +68,7 @@ class Key { } export class ReadMessageDto { readMessages: Key[]; + lastMessage?: { key: Key; messageTimestamp: number }; } export class LastMessage { diff --git a/src/api/integrations/channel/whatsapp/read-state.ts b/src/api/integrations/channel/whatsapp/read-state.ts new file mode 100644 index 0000000000..a579f98501 --- /dev/null +++ b/src/api/integrations/channel/whatsapp/read-state.ts @@ -0,0 +1,74 @@ +import type { ReadMessageDto } from '@api/dto/chat.dto'; +import { isJidGroup, isLidUser, isPnUser, type proto, type WASocket } from 'baileys'; + +export class ReadStateValidationError extends Error {} + +type ReadStateClient = Pick; + +function isChatJid(jid: string): boolean { + return !!(isJidGroup(jid) || isPnUser(jid) || isLidUser(jid)); +} + +/** The legacy request still sends receipts alone. A lastMessage also syncs chat state to linked devices. */ +export async function syncMessageReadState(client: ReadStateClient, data: ReadMessageDto) { + if (!Array.isArray(data.readMessages)) { + throw new ReadStateValidationError('readMessages must be an array'); + } + + const lastMessage = data.lastMessage; + if (lastMessage) { + const { key, messageTimestamp } = lastMessage; + if ( + !key || + typeof key.remoteJid !== 'string' || + !isChatJid(key.remoteJid) || + typeof key.id !== 'string' || + !key.id.trim() || + typeof key.fromMe !== 'boolean' || + !Number.isSafeInteger(messageTimestamp) || + messageTimestamp <= 0 + ) { + throw new ReadStateValidationError('lastMessage requires a valid chat key and epoch-seconds messageTimestamp'); + } + if ( + data.readMessages.some( + (read) => + !read || + !isChatJid(read.remoteJid) || + read.remoteJid !== key.remoteJid || + !read.id || + typeof read.fromMe !== 'boolean', + ) + ) { + throw new ReadStateValidationError('readMessages and lastMessage must belong to the same chat'); + } + } else if (data.readMessages.length === 0) { + throw new ReadStateValidationError('readMessages cannot be empty without lastMessage'); + } + + const keys: proto.IMessageKey[] = data.readMessages + .filter((read) => isChatJid(read.remoteJid)) + .map((read) => ({ remoteJid: read.remoteJid, fromMe: read.fromMe, id: read.id })); + + // Run validation before either external call. A failure in either call propagates to the HTTP request. + if (lastMessage) { + await client.chatModify({ markRead: true, lastMessages: [lastMessage] }, lastMessage.key.remoteJid); + } + if (keys.length > 0 || !lastMessage) { + await client.readMessages(keys); + } + return { message: 'Read messages', read: 'success' }; +} + +export function chatReadStateUpdates( + chats: Array>, + instanceId: string, + readStateObservedAt: string, +) { + return chats.map((chat) => ({ + remoteJid: chat.id, + instanceId, + readStateObservedAt, + ...(Number.isInteger(chat.unreadCount) ? { unreadCount: chat.unreadCount } : {}), + })); +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 010e5deb8d..796536aed5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -157,6 +157,7 @@ import { PassThrough, Readable } from 'stream'; import { v4 } from 'uuid'; import { BaileysMessageProcessor } from './baileysMessage.processor'; +import { chatReadStateUpdates, ReadStateValidationError, syncMessageReadState } from './read-state'; import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; export interface ExtendedIMessageKey extends proto.IMessageKey { @@ -805,9 +806,8 @@ export class BaileysStartupService extends ChannelStartupService { } >[], ) => { - const chatsRaw = chats.map((chat) => { - return { remoteJid: chat.id, instanceId: this.instanceId }; - }); + const readStateObservedAt = new Date().toISOString(); + const chatsRaw = chatReadStateUpdates(chats, this.instanceId, readStateObservedAt); this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); @@ -3699,15 +3699,11 @@ export class BaileysStartupService extends ChannelStartupService { public async markMessageAsRead(data: ReadMessageDto) { try { - const keys: proto.IMessageKey[] = []; - data.readMessages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isPnUser(read.remoteJid)) { - keys.push({ remoteJid: read.remoteJid, fromMe: read.fromMe, id: read.id }); - } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; + return await syncMessageReadState(this.client, data); } catch (error) { + if (error instanceof ReadStateValidationError) { + throw new BadRequestException(error.message); + } throw new InternalServerErrorException('Read messages fail', error.toString()); } } @@ -3773,7 +3769,7 @@ export class BaileysStartupService extends ChannelStartupService { last_message = await this.getLastMessage(number); } else { last_message = data.lastMessage; - last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now(); + last_message.messageTimestamp = last_message?.messageTimestamp ?? Math.floor(Date.now() / 1000); number = last_message?.key?.remoteJid; } @@ -4325,7 +4321,10 @@ export class BaileysStartupService extends ChannelStartupService { return null; } }); - __groupMetaChain = task.then(() => undefined, () => undefined); + __groupMetaChain = task.then( + () => undefined, + () => undefined, + ); return task; } diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index 7dae44539b..fc219bdd63 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -47,7 +47,6 @@ export const readMessageSchema: JSONSchema7 = { properties: { readMessages: { type: 'array', - minItems: 1, uniqueItems: true, items: { properties: { @@ -59,8 +58,25 @@ export const readMessageSchema: JSONSchema7 = { ...isNotEmpty('id', 'remoteJid'), }, }, + lastMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string', minLength: 1 }, + fromMe: { type: 'boolean' }, + remoteJid: { type: 'string', minLength: 1 }, + }, + required: ['id', 'fromMe', 'remoteJid'], + }, + messageTimestamp: { type: 'integer', minimum: 1 }, + }, + required: ['key', 'messageTimestamp'], + }, }, required: ['readMessages'], + anyOf: [{ properties: { readMessages: { minItems: 1 } } }, { required: ['lastMessage'] }], }; export const archiveChatSchema: JSONSchema7 = { @@ -109,7 +125,7 @@ export const markChatUnreadSchema: JSONSchema7 = { required: ['id', 'fromMe', 'remoteJid'], ...isNotEmpty('id', 'remoteJid'), }, - messageTimestamp: { type: 'integer', minLength: 1 }, + messageTimestamp: { type: 'integer', minimum: 1 }, }, required: ['key'], ...isNotEmpty('messageTimestamp'), diff --git a/test/read-state.test.ts b/test/read-state.test.ts new file mode 100644 index 0000000000..cf6c91ca70 --- /dev/null +++ b/test/read-state.test.ts @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { ReadMessageDto } from '../src/api/dto/chat.dto'; +import { + chatReadStateUpdates, + ReadStateValidationError, + syncMessageReadState, +} from '../src/api/integrations/channel/whatsapp/read-state'; +import { readMessageSchema } from '../src/validate/chat.schema'; +import { Validator } from 'jsonschema'; + +const jid = '5511999999999@s.whatsapp.net'; +const otherJid = '5511888888888@s.whatsapp.net'; +const lastMessage = { key: { remoteJid: jid, fromMe: true, id: 'outbound-1' }, messageTimestamp: 1_790_000_000 }; +const receipt = { remoteJid: jid, fromMe: false, id: 'inbound-1' }; + +function mockClient(options: { failModify?: boolean; failReceipts?: boolean } = {}) { + const calls: Array<{ command: string; payload: unknown; jid?: string }> = []; + const client = { + async chatModify(payload: unknown, chatJid: string) { + calls.push({ command: 'chatModify', payload, jid: chatJid }); + if (options.failModify) throw new Error('modify failed'); + }, + async readMessages(keys: unknown) { + calls.push({ command: 'readMessages', payload: keys }); + if (options.failReceipts) throw new Error('receipts failed'); + }, + } as Parameters[0]; + return { client, calls }; +} + +test('legacy readMessages sends receipts without modifying chat state', async () => { + const { client, calls } = mockClient(); + const result = await syncMessageReadState(client, { readMessages: [receipt] }); + assert.deepEqual(result, { message: 'Read messages', read: 'success' }); + assert.deepEqual(calls, [{ command: 'readMessages', payload: [receipt] }]); +}); + +test('lastMessage synchronizes linked-device read state and sends inbound receipts', async () => { + const { client, calls } = mockClient(); + await syncMessageReadState(client, { readMessages: [receipt], lastMessage }); + assert.deepEqual(calls, [ + { command: 'chatModify', payload: { markRead: true, lastMessages: [lastMessage] }, jid }, + { command: 'readMessages', payload: [receipt] }, + ]); +}); + +test('outbound lastMessage can clear a manually unread chat without inbound receipts', async () => { + const { client, calls } = mockClient(); + await syncMessageReadState(client, { readMessages: [], lastMessage }); + assert.deepEqual(calls, [{ command: 'chatModify', payload: { markRead: true, lastMessages: [lastMessage] }, jid }]); +}); + +test('mixed chats and missing timestamps fail before SDK effects', async () => { + const { client, calls } = mockClient(); + await assert.rejects( + syncMessageReadState(client, { readMessages: [{ ...receipt, remoteJid: otherJid }], lastMessage }), + ReadStateValidationError, + ); + await assert.rejects( + syncMessageReadState(client, { readMessages: [receipt], lastMessage: { key: lastMessage.key } } as ReadMessageDto), + ReadStateValidationError, + ); + assert.deepEqual(calls, []); +}); + +test('request schema requires an epoch-seconds timestamp and allows empty receipts only with lastMessage', () => { + const validator = new Validator(); + const valid = (value: unknown) => validator.validate(value, readMessageSchema).valid; + assert.equal(valid({ readMessages: [receipt] }), true); + assert.equal(valid({ readMessages: [], lastMessage }), true); + assert.equal(valid({ readMessages: [] }), false); + assert.equal(valid({ readMessages: [receipt], lastMessage: { key: lastMessage.key } }), false); + assert.equal(valid({ readMessages: [receipt], lastMessage: { ...lastMessage, messageTimestamp: 0 } }), false); +}); + +test('LID chats are accepted when the installed Baileys SDK recognizes them', async () => { + const lid = '123456789@lid'; + const { client, calls } = mockClient(); + await syncMessageReadState(client, { + readMessages: [{ remoteJid: lid, fromMe: false, id: 'lid-inbound' }], + lastMessage: { key: { remoteJid: lid, fromMe: false, id: 'lid-inbound' }, messageTimestamp: 1_790_000_001 }, + }); + assert.equal(calls[0].command, 'chatModify'); + assert.equal(calls[0].jid, lid); + assert.equal(calls[1].command, 'readMessages'); +}); + +test('partial SDK failure never reports success', async () => { + const modify = mockClient({ failModify: true }); + await assert.rejects(syncMessageReadState(modify.client, { readMessages: [receipt], lastMessage }), /modify failed/); + assert.deepEqual(modify.calls.map((call) => call.command), ['chatModify']); + + const receipts = mockClient({ failReceipts: true }); + await assert.rejects(syncMessageReadState(receipts.client, { readMessages: [receipt], lastMessage }), /receipts failed/); + assert.deepEqual(receipts.calls.map((call) => call.command), ['chatModify', 'readMessages']); +}); + +test('chats.update retains zero, manual-unread marker, and positive delta with observation time', () => { + const observedAt = '2026-09-28T12:34:56.000Z'; + const updates = chatReadStateUpdates( + [ + { id: jid, unreadCount: 0 }, + { id: otherJid, unreadCount: -1 }, + { id: '123@g.us', unreadCount: 3 }, + { id: '456@g.us', unreadCount: 1.5 }, + ], + 'instance-1', + observedAt, + ); + assert.deepEqual(updates, [ + { remoteJid: jid, instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: 0 }, + { remoteJid: otherJid, instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: -1 }, + { remoteJid: '123@g.us', instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: 3 }, + { remoteJid: '456@g.us', instanceId: 'instance-1', readStateObservedAt: observedAt }, + ]); +}); From b24f9d0d73ca72d7c1dd67aac6aa94c56d7d003c Mon Sep 17 00:00:00 2001 From: Alpes Digital Date: Mon, 28 Sep 2026 15:09:03 -0300 Subject: [PATCH 06/16] fix(test): isolate read-state tests from Baileys runtime --- src/api/integrations/channel/whatsapp/read-state.ts | 6 ++++-- test/read-state.test.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/read-state.ts b/src/api/integrations/channel/whatsapp/read-state.ts index a579f98501..d8eb89edee 100644 --- a/src/api/integrations/channel/whatsapp/read-state.ts +++ b/src/api/integrations/channel/whatsapp/read-state.ts @@ -1,12 +1,14 @@ import type { ReadMessageDto } from '@api/dto/chat.dto'; -import { isJidGroup, isLidUser, isPnUser, type proto, type WASocket } from 'baileys'; +import type { proto, WASocket } from 'baileys'; export class ReadStateValidationError extends Error {} type ReadStateClient = Pick; function isChatJid(jid: string): boolean { - return !!(isJidGroup(jid) || isPnUser(jid) || isLidUser(jid)); + // Baileys' isPnUser/isJidGroup/isLidUser check these suffixes. Keep this + // predicate local so focused tests need not load the full Baileys runtime. + return typeof jid === 'string' && /[^@\s]+@(s\.whatsapp\.net|g\.us|lid)$/.test(jid); } /** The legacy request still sends receipts alone. A lastMessage also syncs chat state to linked devices. */ diff --git a/test/read-state.test.ts b/test/read-state.test.ts index cf6c91ca70..771f72f94f 100644 --- a/test/read-state.test.ts +++ b/test/read-state.test.ts @@ -74,7 +74,7 @@ test('request schema requires an epoch-seconds timestamp and allows empty receip assert.equal(valid({ readMessages: [receipt], lastMessage: { ...lastMessage, messageTimestamp: 0 } }), false); }); -test('LID chats are accepted when the installed Baileys SDK recognizes them', async () => { +test('LID chats are accepted by the same JID suffix contract', async () => { const lid = '123456789@lid'; const { client, calls } = mockClient(); await syncMessageReadState(client, { From 1d92ff002647a7d7cc88793a041e8bf500f0513b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 10:51:11 +0000 Subject: [PATCH 07/16] fix(chat): re-key LID read-state updates to the phone JID chats.update arrives keyed by LID on accounts migrated to LID addressing, while inbound messages are already normalized to the phone JID. Webhook consumers could not match the read state to the conversation, so reading on the phone never cleared unread counts downstream. Co-Authored-By: Claude Opus 5.5 --- .../channel/whatsapp/read-state.ts | 28 +++++++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 12 +++++-- test/read-state.test.ts | 34 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/read-state.ts b/src/api/integrations/channel/whatsapp/read-state.ts index d8eb89edee..10b393887e 100644 --- a/src/api/integrations/channel/whatsapp/read-state.ts +++ b/src/api/integrations/channel/whatsapp/read-state.ts @@ -74,3 +74,31 @@ export function chatReadStateUpdates( ...(Number.isInteger(chat.unreadCount) ? { unreadCount: chat.unreadCount } : {}), })); } + +type ChatReadStateUpdate = ReturnType[number]; + +/** + * Chats are keyed by LID on recent accounts, but inbound messages are already + * normalized to the phone JID (key.remoteJidAlt). Emit the phone JID too so + * webhook consumers can match the read state to the same conversation. + * The original LID is kept in `lid`; unresolved LIDs pass through unchanged. + */ +export async function resolveReadStateLids( + updates: ChatReadStateUpdate[], + resolvePn: (lid: string) => Promise, +) { + return Promise.all( + updates.map(async (update) => { + if (typeof update.remoteJid !== 'string' || !update.remoteJid.endsWith('@lid')) return update; + let pn: string | null | undefined; + try { + pn = await resolvePn(update.remoteJid); + } catch { + pn = null; + } + const user = typeof pn === 'string' ? pn.split('@')[0].split(':')[0] : ''; + if (!/^\d{8,15}$/.test(user)) return update; + return { ...update, remoteJid: `${user}@s.whatsapp.net`, lid: update.remoteJid }; + }), + ); +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 796536aed5..dca5556bc8 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -157,7 +157,12 @@ import { PassThrough, Readable } from 'stream'; import { v4 } from 'uuid'; import { BaileysMessageProcessor } from './baileysMessage.processor'; -import { chatReadStateUpdates, ReadStateValidationError, syncMessageReadState } from './read-state'; +import { + chatReadStateUpdates, + ReadStateValidationError, + resolveReadStateLids, + syncMessageReadState, +} from './read-state'; import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; export interface ExtendedIMessageKey extends proto.IMessageKey { @@ -807,7 +812,10 @@ export class BaileysStartupService extends ChannelStartupService { >[], ) => { const readStateObservedAt = new Date().toISOString(); - const chatsRaw = chatReadStateUpdates(chats, this.instanceId, readStateObservedAt); + const chatsRaw = await resolveReadStateLids( + chatReadStateUpdates(chats, this.instanceId, readStateObservedAt), + (lid) => this.client.signalRepository.lidMapping.getPNForLID(lid), + ); this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); diff --git a/test/read-state.test.ts b/test/read-state.test.ts index 771f72f94f..0a20298971 100644 --- a/test/read-state.test.ts +++ b/test/read-state.test.ts @@ -4,6 +4,7 @@ import type { ReadMessageDto } from '../src/api/dto/chat.dto'; import { chatReadStateUpdates, ReadStateValidationError, + resolveReadStateLids, syncMessageReadState, } from '../src/api/integrations/channel/whatsapp/read-state'; import { readMessageSchema } from '../src/validate/chat.schema'; @@ -115,3 +116,36 @@ test('chats.update retains zero, manual-unread marker, and positive delta with o { remoteJid: '456@g.us', instanceId: 'instance-1', readStateObservedAt: observedAt }, ]); }); + +test('chats.update keyed by LID is re-keyed to the phone JID, keeping the LID', async () => { + const observedAt = '2026-10-08T12:00:00.000Z'; + const resolved = await resolveReadStateLids( + chatReadStateUpdates( + [ + { id: '123456789012345@lid', unreadCount: 0 }, + { id: '999999999999999@lid', unreadCount: -1 }, + { id: '888888888888888@lid', unreadCount: 0 }, + { id: jid, unreadCount: 0 }, + ], + 'instance-1', + observedAt, + ), + async (lid) => { + if (lid === '123456789012345@lid') return '5548991554955:12@s.whatsapp.net'; + if (lid === '888888888888888@lid') throw new Error('mapping store down'); + return null; + }, + ); + assert.deepEqual(resolved, [ + { + remoteJid: '5548991554955@s.whatsapp.net', + lid: '123456789012345@lid', + instanceId: 'instance-1', + readStateObservedAt: observedAt, + unreadCount: 0, + }, + { remoteJid: '999999999999999@lid', instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: -1 }, + { remoteJid: '888888888888888@lid', instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: 0 }, + { remoteJid: jid, instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: 0 }, + ]); +}); From e2469c868f6a6cab7e062b4eaf28cd1c0ec04c56 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 11:04:35 +0000 Subject: [PATCH 08/16] fix(chat): address read/unread commands to the LID the phone uses Webhook consumers only see the phone JID, but LID-addressed chats are indexed by LID on the primary device. Receipts and app-state patches sent to the phone JID target a chat the phone does not know. Co-Authored-By: Claude Opus 5.5 --- .../channel/whatsapp/read-state.ts | 46 +++++++++++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 17 ++++++- test/read-state.test.ts | 21 +++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/read-state.ts b/src/api/integrations/channel/whatsapp/read-state.ts index 10b393887e..06d94e5105 100644 --- a/src/api/integrations/channel/whatsapp/read-state.ts +++ b/src/api/integrations/channel/whatsapp/read-state.ts @@ -102,3 +102,49 @@ export async function resolveReadStateLids( }), ); } + +/** + * The inverse of resolveReadStateLids. Webhook consumers only ever see the + * phone JID, but chats migrated to LID addressing are indexed by LID on the + * phone: receipts and app-state patches must address the LID or they target a + * chat the primary device does not know. + */ +export async function toChatAddressingJid( + jid: string, + resolveLid: (pn: string) => Promise, +): Promise { + if (typeof jid !== 'string' || !jid.endsWith('@s.whatsapp.net')) return jid; + let lid: string | null | undefined; + try { + lid = await resolveLid(jid); + } catch { + lid = null; + } + const user = typeof lid === 'string' && lid.endsWith('@lid') ? lid.split('@')[0].split(':')[0] : ''; + return /^\d{8,20}$/.test(user) ? `${user}@lid` : jid; +} + +export async function readRequestToChatAddressing( + data: ReadMessageDto, + resolveLid: (pn: string) => Promise, +): Promise { + const jids = new Set(); + if (data.lastMessage?.key?.remoteJid) jids.add(data.lastMessage.key.remoteJid); + if (Array.isArray(data.readMessages)) for (const read of data.readMessages) if (read?.remoteJid) jids.add(read.remoteJid); + const mapped = new Map(); + for (const jid of jids) mapped.set(jid, await toChatAddressingJid(jid, resolveLid)); + return { + ...data, + readMessages: Array.isArray(data.readMessages) + ? data.readMessages.map((read) => (read?.remoteJid ? { ...read, remoteJid: mapped.get(read.remoteJid) } : read)) + : data.readMessages, + ...(data.lastMessage?.key + ? { + lastMessage: { + ...data.lastMessage, + key: { ...data.lastMessage.key, remoteJid: mapped.get(data.lastMessage.key.remoteJid) ?? data.lastMessage.key.remoteJid }, + }, + } + : {}), + } as ReadMessageDto; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index dca5556bc8..b95aa0a3ec 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -160,8 +160,10 @@ import { BaileysMessageProcessor } from './baileysMessage.processor'; import { chatReadStateUpdates, ReadStateValidationError, + readRequestToChatAddressing, resolveReadStateLids, syncMessageReadState, + toChatAddressingJid, } from './read-state'; import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; @@ -3707,7 +3709,10 @@ export class BaileysStartupService extends ChannelStartupService { public async markMessageAsRead(data: ReadMessageDto) { try { - return await syncMessageReadState(this.client, data); + const addressed = await readRequestToChatAddressing(data, (pn) => + this.client.signalRepository.lidMapping.getLIDForPN(pn), + ); + return await syncMessageReadState(this.client, addressed); } catch (error) { if (error instanceof ReadStateValidationError) { throw new BadRequestException(error.message); @@ -3785,7 +3790,15 @@ export class BaileysStartupService extends ChannelStartupService { throw new NotFoundException('Last message not found'); } - await this.client.chatModify({ markRead: false, lastMessages: [last_message] }, createJid(number)); + const resolveLid = (pn: string) => this.client.signalRepository.lidMapping.getLIDForPN(pn); + const chatJid = await toChatAddressingJid(createJid(number), resolveLid); + if (last_message?.key?.remoteJid) { + last_message = { + ...last_message, + key: { ...last_message.key, remoteJid: await toChatAddressingJid(last_message.key.remoteJid, resolveLid) }, + }; + } + await this.client.chatModify({ markRead: false, lastMessages: [last_message] }, chatJid); return { chatId: number, markedChatUnread: true }; } catch (error) { diff --git a/test/read-state.test.ts b/test/read-state.test.ts index 0a20298971..5151b1da53 100644 --- a/test/read-state.test.ts +++ b/test/read-state.test.ts @@ -4,7 +4,9 @@ import type { ReadMessageDto } from '../src/api/dto/chat.dto'; import { chatReadStateUpdates, ReadStateValidationError, + readRequestToChatAddressing, resolveReadStateLids, + toChatAddressingJid, syncMessageReadState, } from '../src/api/integrations/channel/whatsapp/read-state'; import { readMessageSchema } from '../src/validate/chat.schema'; @@ -149,3 +151,22 @@ test('chats.update keyed by LID is re-keyed to the phone JID, keeping the LID', { remoteJid: jid, instanceId: 'instance-1', readStateObservedAt: observedAt, unreadCount: 0 }, ]); }); + +test('commands from webhook consumers are re-addressed to the LID the phone uses', async () => { + const resolveLid = async (pn: string) => (pn === jid ? '55761694654630:3@lid' : null); + assert.equal(await toChatAddressingJid(jid, resolveLid), '55761694654630@lid'); + assert.equal(await toChatAddressingJid(otherJid, resolveLid), otherJid); + assert.equal(await toChatAddressingJid('123@g.us', resolveLid), '123@g.us'); + assert.equal(await toChatAddressingJid(jid, async () => { throw new Error('down'); }), jid); + + const addressed = await readRequestToChatAddressing( + { readMessages: [receipt], lastMessage } as unknown as ReadMessageDto, + resolveLid, + ); + assert.equal(addressed.readMessages[0].remoteJid, '55761694654630@lid'); + assert.equal(addressed.lastMessage?.key.remoteJid, '55761694654630@lid'); + assert.equal(addressed.lastMessage?.key.id, 'outbound-1'); + const { client, calls } = mockClient(); + await syncMessageReadState(client, addressed); + assert.equal(calls[0].jid, '55761694654630@lid'); +}); From de9f4491cc1381a48b43d10bc3f587eddfc2a642 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 12:04:06 +0000 Subject: [PATCH 09/16] fix(auth): revive app-state sync keys with fromObject, not create The auth-state adapters rebuilt AppStateSyncKeyData with create(), which keeps keyData as the stored base64 string. Baileys then derived the app-state keys from the 44-char text instead of the 32 key bytes: every incoming patch failed to decrypt and was skipped silently (MAC checks are off by default), and every outgoing chatModify patch was encrypted with the wrong keys, which the server answers with 401 device_removed. fromObject decodes base64 into bytes, as Baileys useMultiFileAuthState. Co-Authored-By: Claude Opus 5.5 --- src/utils/use-multi-file-auth-state-prisma.ts | 2 +- .../use-multi-file-auth-state-provider-files.ts | 2 +- src/utils/use-multi-file-auth-state-redis-db.ts | 2 +- test/auth-state-key.test.ts | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 test/auth-state-key.test.ts diff --git a/src/utils/use-multi-file-auth-state-prisma.ts b/src/utils/use-multi-file-auth-state-prisma.ts index d090780234..bd11afa2fe 100644 --- a/src/utils/use-multi-file-auth-state-prisma.ts +++ b/src/utils/use-multi-file-auth-state-prisma.ts @@ -182,7 +182,7 @@ export default async function useMultiFileAuthStatePrisma( ids.map(async (id) => { let value = await readData(`${type}-${id}`); if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.create(value); + value = proto.Message.AppStateSyncKeyData.fromObject(value); } data[id] = value; diff --git a/src/utils/use-multi-file-auth-state-provider-files.ts b/src/utils/use-multi-file-auth-state-provider-files.ts index eecc3100e5..157918169d 100644 --- a/src/utils/use-multi-file-auth-state-provider-files.ts +++ b/src/utils/use-multi-file-auth-state-provider-files.ts @@ -116,7 +116,7 @@ export class AuthStateProvider { ids.map(async (id) => { let value = await readData(`${type}-${id}`); if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.create(value); + value = proto.Message.AppStateSyncKeyData.fromObject(value); } data[id] = value; diff --git a/src/utils/use-multi-file-auth-state-redis-db.ts b/src/utils/use-multi-file-auth-state-redis-db.ts index e0981c700c..4d5d5a5457 100644 --- a/src/utils/use-multi-file-auth-state-redis-db.ts +++ b/src/utils/use-multi-file-auth-state-redis-db.ts @@ -61,7 +61,7 @@ export async function useMultiFileAuthStateRedisDb( ids.map(async (id) => { let value = await readData(`${type}-${id}`); if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.create(value); + value = proto.Message.AppStateSyncKeyData.fromObject(value); } data[id] = value; diff --git a/test/auth-state-key.test.ts b/test/auth-state-key.test.ts new file mode 100644 index 0000000000..ed2fd88bf9 --- /dev/null +++ b/test/auth-state-key.test.ts @@ -0,0 +1,12 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +test("stored app-state keys are revived as bytes, not as their base64 text", async () => { + const { proto } = await import("baileys"); + const stored = JSON.parse(JSON.stringify({ keyData: Buffer.alloc(32, 7).toString("base64"), timestamp: "1791458000000" })); + const keyData = proto.Message.AppStateSyncKeyData.fromObject(stored).keyData as Uint8Array; + assert.equal(keyData.length, 32); + assert.equal(Buffer.from(keyData).equals(Buffer.alloc(32, 7)), true); + // create() keeps the 44-char base64 string: HKDF over it derives the wrong keys. + assert.equal(typeof proto.Message.AppStateSyncKeyData.create(stored).keyData, "string"); +}); From 96d47dd0e3165238b8ecba2f5ca3a638025da198 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 19 May 2026 12:30:26 -0300 Subject: [PATCH 10/16] security: prevent cross-instance auth bypass via query/body override (CVE pending, #2435) (#2549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * security: prevent cross-instance auth bypass via query/body override (#2435) The auth guard in src/api/guards/auth.guard.ts validates instance ownership using req.params.instanceName. The abstract router then merged req.query (and, on /instance/create, req.body) into the instance object via Object.assign — which silently overwrote the already-authenticated instanceName. An attacker with one valid token could send: GET /chat/findMessages/MY_INSTANCE?instanceName=VICTIM_INSTANCE Auth passed for MY_INSTANCE, but dataValidate() then replaced the instance with VICTIM_INSTANCE before execute() ran — giving the caller full access to read/send messages, modify settings, and delete other tenants' instances. CWE-639: Authorization Bypass Through User-Controlled Key Fix: introduce sanitizeUntrustedInput() that strips PROTECTED_INSTANCE_FIELDS (instanceName, instanceId) from any untrusted source before merging into the instance object. Logs a warning on attempts so abuse is auditable. Closes #2435 Reported by @lighthousekeeper1212 via static analysis. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(lint): remove extra blank line introduced by recent merge Pre-push lint flagged whatsapp.baileys.service.ts:531 (double blank line after the stream:error 515 fix merged via #2509). Trivial prettier fix. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) (cherry picked from commit 7a55a2bfcca5bff7df48a76893398d7b5aed5d56) --- src/api/abstract/abstract.router.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index e8449a8c8a..2a64296ded 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -17,6 +17,21 @@ type DataValidate = { const logger = new Logger('Validate'); +const PROTECTED_INSTANCE_FIELDS = ['instanceName', 'instanceId'] as const; + +function sanitizeUntrustedInput(source: Record | undefined): Record { + if (!source || typeof source !== 'object') return {}; + const sanitized: Record = {}; + for (const [key, value] of Object.entries(source)) { + if ((PROTECTED_INSTANCE_FIELDS as readonly string[]).includes(key)) { + logger.warn(`Ignoring attempt to override protected field "${key}" via untrusted input`); + continue; + } + sanitized[key] = value; + } + return sanitized; +} + export abstract class RouterBroker { constructor() {} public routerPath(path: string, param = true) { @@ -34,11 +49,11 @@ export abstract class RouterBroker { const instance = request.params as unknown as InstanceDto; if (request?.query && Object.keys(request.query).length > 0) { - Object.assign(instance, request.query); + Object.assign(instance, sanitizeUntrustedInput(request.query as Record)); } if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, body); + Object.assign(instance, sanitizeUntrustedInput(body)); } Object.assign(ref, body); From ddd36b9d1363279227b86ff7992e222f5d226cb9 Mon Sep 17 00:00:00 2001 From: Octopus Date: Tue, 19 May 2026 22:42:28 +0800 Subject: [PATCH 11/16] fix(baileys): prevent healthy instances from being killed after stream:error 515 (#2509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(baileys): prevent healthy instances from being killed after stream:error 515 When WhatsApp sends stream:error code=515 (Connection Replaced), Baileys handles the reconnect correctly and fires connection.update with state='open'. However, WhatsApp then sends a 401 (loggedOut) to clean up the old session slot, which Evolution API incorrectly treated as a real logout, killing the newly-connected healthy instance. The fix tracks when a stream:error 515 node arrives via the CB:stream:error WebSocket event. If a loggedOut (401) close event fires within 30 seconds of a 515, it is treated as a transient reconnect rather than a real logout. Fixes #2498 * fix(baileys): name the 515 reconnect grace + tighten stream-error types Addresses sourcery-ai review feedback on the previous commit: - Extract the 30 000ms reconnect grace window into a named class constant STREAM_515_RECONNECT_GRACE_MS so future tuning is self-documenting rather than a literal scattered through the close handler. - Extract the magic '515' string into STREAM_ERROR_CODE_RECONNECT. - Replace the loose 'node: any' on the 'CB:stream:error' handler with a minimal structural type ({ attrs?: { code?: string | number } }) so the payload shape is documented and type-checked. - Compare the code via String(...) so a numeric 515 from the underlying socket library still triggers the grace window — the original literal '515' check would have silently broken on a type change. * fix(baileys): satisfy prettier — collapse 515 reconnect guard onto one line Check Code Quality lint failed on prettier/prettier (the recentStream515 expression fits within the project's 120-col printWidth on a single line, while shouldReconnect still needs to break across two). Co-authored-by: Octopus --------- Co-authored-by: octo-patch Co-authored-by: octo-patch (cherry picked from commit 1d38d4f6c5c1b17caddf6e420d7f8379960b0e86) --- .../whatsapp/whatsapp.baileys.service.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index b95aa0a3ec..7feb0e9d73 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -274,6 +274,15 @@ export class BaileysStartupService extends ChannelStartupService { private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates + private _lastStream515At = 0; + + // Reconnect behaviour for the Baileys "stream:error 515" sequence. + // After WhatsApp emits 515 it usually closes with `loggedOut`; that close is *not* a real logout + // and we should reconnect. We treat any close arriving within this grace window as 515-driven. + private static readonly STREAM_515_RECONNECT_GRACE_MS = 30_000; + // The numeric WhatsApp stream-error code that triggers the grace-period reconnect above. + private static readonly STREAM_ERROR_CODE_RECONNECT = '515'; + public stateConnection: wa.StateConnection = { state: 'close' }; public phoneNumber: string; @@ -444,7 +453,12 @@ export class BaileysStartupService extends ChannelStartupService { if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; - const shouldReconnect = !codesToNotReconnect.includes(statusCode); + // If a stream:error 515 (Baileys' "restart needed" handshake) just fired, + // a follow-up loggedOut is the expected restart signal — not an actual + // logout — so reconnect anyway. + const recentStream515 = Date.now() - this._lastStream515At < BaileysStartupService.STREAM_515_RECONNECT_GRACE_MS; + const shouldReconnect = + !codesToNotReconnect.includes(statusCode) || (statusCode === DisconnectReason.loggedOut && recentStream515); if (shouldReconnect) { await this.connectToWhatsapp(this.phoneNumber); } else { @@ -735,6 +749,12 @@ export class BaileysStartupService extends ChannelStartupService { this.eventHandler(); + this.client.ws.on('CB:stream:error', (node: { attrs?: { code?: string | number } }) => { + if (String(node?.attrs?.code) === BaileysStartupService.STREAM_ERROR_CODE_RECONNECT) { + this._lastStream515At = Date.now(); + } + }); + this.client.ws.on('CB:call', (packet) => { console.log('CB:call', packet); const payload = { event: 'CB:call', packet: packet }; From 9e8e3474ecf7a8c259578524109f40ee80235ce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 12:25:41 +0000 Subject: [PATCH 12/16] fix(router): keep the create body intact after the cross-instance guard sanitizeUntrustedInput on /instance/create removed instanceName from the body, which is the only source of the new instance name. Create has no authenticated instance in the path, so there is nothing to override there. Co-Authored-By: Claude Opus 5.5 --- src/api/abstract/abstract.router.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index 2a64296ded..68fdb5c6a4 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -53,7 +53,10 @@ export abstract class RouterBroker { } if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, sanitizeUntrustedInput(body)); + // No authenticated instance exists in the path on create, so there is + // nothing to override; the body IS the instance (its name included). + // Sanitizing it here stripped instanceName and broke instance creation. + Object.assign(instance, body); } Object.assign(ref, body); From 032878c2c725cfa6680bdae62f7fae1d422336f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 12:35:43 +0000 Subject: [PATCH 13/16] chore(deps): baileys 7.0.0-rc14 (pinned) Nests the tc token under the picture query in profilePictureUrl, so profile pictures of privacy-restricted contacts resolve. The Android browser mode is opt-in by browser name and stays off here. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8eda484379..b25f0f7c97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc13", + "baileys": "7.0.0-rc14", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", @@ -5773,9 +5773,9 @@ } }, "node_modules/baileys": { - "version": "7.0.0-rc13", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", - "integrity": "sha512-v8k74K8B5R7WNYGa26MyJAYEu3Wc4BSuK01QaK8lr30lhE8Nga31nWNu8KN0NDDt+Fsvkq4SQFFI8Q13ghjKmA==", + "version": "7.0.0-rc14", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc14.tgz", + "integrity": "sha512-pewtrljhWx5JTUBvvkXZz1fL3JPiwzjBsnhx/DWf2LWBx1cZNH7J/sF22xDehba5mySWUQU4a1Pwt7lWARUxaA==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7fb7f03b51..369296c3cd 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "amqplib": "^0.10.5", "audio-decode": "^2.2.3", "axios": "^1.7.9", - "baileys": "^7.0.0-rc13", + "baileys": "7.0.0-rc14", "class-validator": "^0.14.1", "compression": "^1.7.5", "cors": "^2.8.5", From 56603b0421159c07b5199c19f9ef4e5f3a30350c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 12:56:33 +0000 Subject: [PATCH 14/16] fix(messages): fill remoteJidAlt from the LID mapping when it is missing History sync after a fresh pairing (and some live messages) deliver LID-only keys. Without the phone JID, webhook consumers cannot identify the sender and drop the message. Resolve it with getPNForLID, live and in messaging-history.set; unknown mappings leave the key untouched. Co-Authored-By: Claude Opus 5.5 --- .../channel/whatsapp/read-state.ts | 21 ++++++++++++++++ .../whatsapp/whatsapp.baileys.service.ts | 4 ++++ test/read-state.test.ts | 24 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/read-state.ts b/src/api/integrations/channel/whatsapp/read-state.ts index 06d94e5105..034162451d 100644 --- a/src/api/integrations/channel/whatsapp/read-state.ts +++ b/src/api/integrations/channel/whatsapp/read-state.ts @@ -148,3 +148,24 @@ export async function readRequestToChatAddressing( : {}), } as ReadMessageDto; } + +/** + * Messages addressed by LID sometimes arrive without remoteJidAlt (history sync + * after a fresh pairing, contacts not yet resolved by the server). Without the + * phone JID, webhook consumers cannot tell who sent the message. Fill it from + * Baileys' LID mapping; leave the key untouched when the mapping is unknown. + */ +export async function fillRemoteJidAlt( + key: { remoteJid?: string | null; remoteJidAlt?: string | null } | null | undefined, + resolvePn: (lid: string) => Promise, +): Promise { + if (!key || typeof key.remoteJid !== 'string' || !key.remoteJid.endsWith('@lid') || key.remoteJidAlt) return; + let pn: string | null | undefined; + try { + pn = await resolvePn(key.remoteJid); + } catch { + return; + } + const user = typeof pn === 'string' ? pn.split('@')[0].split(':')[0] : ''; + if (/^\d{8,15}$/.test(user)) key.remoteJidAlt = `${user}@s.whatsapp.net`; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 7feb0e9d73..321b98183f 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -159,6 +159,7 @@ import { v4 } from 'uuid'; import { BaileysMessageProcessor } from './baileysMessage.processor'; import { chatReadStateUpdates, + fillRemoteJidAlt, ReadStateValidationError, readRequestToChatAddressing, resolveReadStateLids, @@ -1088,6 +1089,8 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + await fillRemoteJidAlt(m.key, (lid) => this.client.signalRepository.lidMapping.getPNForLID(lid)); + if (!m.pushName && !m.key.fromMe) { const participantJid = m.participant || m.key.participant || m.key.remoteJid; if (participantJid && contactsMap.has(participantJid)) { @@ -1529,6 +1532,7 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.verbose(messageRaw); sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`); + await fillRemoteJidAlt(messageRaw.key, (lid) => this.client.signalRepository.lidMapping.getPNForLID(lid)); if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; } diff --git a/test/read-state.test.ts b/test/read-state.test.ts index 5151b1da53..f5eeda5778 100644 --- a/test/read-state.test.ts +++ b/test/read-state.test.ts @@ -3,6 +3,7 @@ import { test } from 'node:test'; import type { ReadMessageDto } from '../src/api/dto/chat.dto'; import { chatReadStateUpdates, + fillRemoteJidAlt, ReadStateValidationError, readRequestToChatAddressing, resolveReadStateLids, @@ -170,3 +171,26 @@ test('commands from webhook consumers are re-addressed to the LID the phone uses await syncMessageReadState(client, addressed); assert.equal(calls[0].jid, '55761694654630@lid'); }); + +test('LID-only message keys get the phone JID from the LID mapping', async () => { + const resolve = async (lid: string) => (lid === '202860566425607@lid' ? '5511964242104:0@s.whatsapp.net' : null); + const known = { remoteJid: '202860566425607@lid' } as { remoteJid: string; remoteJidAlt?: string }; + await fillRemoteJidAlt(known, resolve); + assert.equal(known.remoteJidAlt, '5511964242104@s.whatsapp.net'); + + const unknown = { remoteJid: '999999999999999@lid' } as { remoteJid: string; remoteJidAlt?: string }; + await fillRemoteJidAlt(unknown, resolve); + assert.equal(unknown.remoteJidAlt, undefined); + + const already = { remoteJid: '202860566425607@lid', remoteJidAlt: '5500000000000@s.whatsapp.net' }; + await fillRemoteJidAlt(already, resolve); + assert.equal(already.remoteJidAlt, '5500000000000@s.whatsapp.net'); + + const phone = { remoteJid: jid } as { remoteJid: string; remoteJidAlt?: string }; + await fillRemoteJidAlt(phone, resolve); + assert.equal(phone.remoteJidAlt, undefined); + + const failing = { remoteJid: '202860566425607@lid' } as { remoteJid: string; remoteJidAlt?: string }; + await fillRemoteJidAlt(failing, async () => { throw new Error('down'); }); + assert.equal(failing.remoteJidAlt, undefined); +}); From 7c799c39187181435b9184ea7a75f87c5a501894 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 13:14:47 +0000 Subject: [PATCH 15/16] fix(baileys): verify app-state MACs (patch and snapshot) Co-Authored-By: Claude Opus 5.5 --- .../channel/whatsapp/whatsapp.baileys.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 321b98183f..07026653e4 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -693,6 +693,11 @@ export class BaileysStartupService extends ChannelStartupService { }, msgRetryCounterCache: this.msgRetryCounterCache, generateHighQualityLinkPreview: true, + // Baileys ships with app-state MAC checks off. With them off, an app-state + // patch decoded with the wrong key is dropped silently and our own patches + // can go out inconsistent (the server answers 401 device_removed). Fail + // closed: an invalid patch MAC makes Baileys refetch a snapshot instead. + appStateMacVerification: { patch: true, snapshot: true }, getMessage: async (key) => (await this.getMessage(key)) as Promise, ...browserOptions, markOnlineOnConnect: this.localSettings.alwaysOnline, From 2765f4288561d9da55f4e3e9c1f9c41a72dc138b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 13:20:38 +0000 Subject: [PATCH 16/16] fix(history): never use the JID digits as pushName History messages without a known contact got pushName = JID user part (LID digits included), which CRMs then stored as the contact name. Look the contact up by the phone JID as well and otherwise leave it empty. Co-Authored-By: Claude Opus 5.5 --- .../channel/whatsapp/whatsapp.baileys.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 07026653e4..667ab94768 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1098,11 +1098,12 @@ export class BaileysStartupService extends ChannelStartupService { if (!m.pushName && !m.key.fromMe) { const participantJid = m.participant || m.key.participant || m.key.remoteJid; - if (participantJid && contactsMap.has(participantJid)) { - m.pushName = contactsMap.get(participantJid).name; - } else if (participantJid) { - m.pushName = participantJid.split('@')[0]; - } + // Contacts may be keyed by the phone JID while the message is LID-addressed. + const known = [participantJid, m.key.participantAlt, m.key.remoteJidAlt].find( + (jid) => jid && contactsMap.has(jid) && contactsMap.get(jid).name, + ); + // No digits fallback: a JID is not a name, and consumers would store it as one. + if (known) m.pushName = contactsMap.get(known).name; } messagesRaw.push(this.prepareMessage(m));